Type class that has a Monoid instance seems to require all instances to also be monoids

We regularly need to construct records full of default values from a row of field names in our application. If any type has a Monoid instance, it’s good to go – we’ll use mempty. However, if a type doesn’t, then it’ll require an instance of the Default type class to produce a default value.

A simple version of the class is below: it has one implementation, def, which simply provides a value. I then created an instance for monoids, stating that any value with a monoid instance should defer to its mempty value.

class Default v where
  def :: v

instance defaultMonoid :: Monoid v => Default v where
  def = mempty

data Summer = Winter | Fall

instance defaultSummer :: Default Summer where
  def = Winter

mkDefault :: forall v. Default v => v
mkDefault = def

x = mkDefault :: Summer

While the class type checks fine, as soon as I define a Default instance for something that is not a monoid, I get a type error. It seems that every instance of Default is expected to be a monoid now, which I didn’t intend.

No type class instance was found for Data.Monoid.Monoid Summer while checking that type forall v. Default v => v is at least as general as type Summer while checking that mkDefault has type Summer in value declaration x

I’m assuming there’s something going on with potential overlapping instances (what if I define Default for a monoid that isn’t the same as the monoid instance?), but I’m curious what exactly this error is about.

If I want to implement this class, will I need to define Default directly for every type without being able to wholesale defer to Monoid?

1 Like

Overlapping instances like this are currently discouraged - they’re not really meant to be used like this - but they are resolved in alphabetical order currently. So you might be able to get it to do what you want by renaming the defaultSummer instance to something like a_defaultSummer. See https://github.com/purescript/documentation/blob/77745cf220945a177ac367cf085b41524e655898/language/Type-Classes.md:

If multiple instances are possible they are ordered based on their names and the first one is selected. Overlapping instances are currently permitted but not recommended. In simple cases the compiler will display a warning and list the instances it found and which was chosen. In the future, they may be disallowed completely.

In 0.12 there will be a sensible way of achieving this, with instance chains.

1 Like

@hdgarrood Thanks for the info! That makes sense why the error message provides that help.

Until 0.12, is this sort of implementation the correct way to go about things?

class Default v where
  def :: v

instance defaultUnit :: Default Unit where
  def = unit

instance defaultString :: Default String where
  def = mempty

instance defaultArray :: Default (Array a) where
  def = mempty

instance defaultMap :: Ord k => Default (Map k v) where
  def = mempty

instance defaultList :: Default (List a) where
  def = mempty

instance defaultOrdering :: Default Ordering where
  def = mempty

instance defaultFn :: Default b => Default (a -> b) where
  def = const def

instance defaultInt :: Default Int where
  def = 0

instance defaultNumber :: Default Number where
  def = 0.0

Note, that even in 0.12 you can’t dispatch on constraints in general because the typechecker does not backtrack after matching the instance head. In that sense the behavior will not change. It finds the case for all a and then looks for a Monoid. If it does not find a Monoid it won’t keep looking.

In 0.12 you will however be able to define an instance chain where the Monoid case is the fallback. But what that means is it will only accept instances defined with the chain, otherwise you will have to implement Monoid. So you still won’t be able to define new Default instances separately (ie, in other modules or even in a different instance declaration).

1 Like

Great! With that Default class, we can successfully generate a record of default values from a row of types:

class DefaultRecord (rl :: RowList) (r :: # Type) (o :: # Type) | rl -> o where
  defaultRecordImpl :: RLProxy rl -> RProxy r -> Record o

-- In the base case when we have an empty record, we'll return it.
instance nilDefaultRecord :: DefaultRecord Nil r () where
  defaultRecordImpl _ _ = {}

-- Otherwise we'll accumulate the value at the head of the list into
-- our base.
instance consDefaultRecord
  :: ( IsSymbol name
     , Default a
     , RowCons name { value :: a, shouldValidate :: Boolean } tail' o
     , RowCons name a t0 r
     , RowLacks name tail'
     , DefaultRecord tail r tail'
     )
  => DefaultRecord (Cons name a tail) r o
  where
    defaultRecordImpl _ r =
      let tail' = defaultRecordImpl (RLProxy :: RLProxy tail) (RProxy :: RProxy r)
       in insert (SProxy :: SProxy name) { value: def, shouldValidate: false } tail'

makeDefaultRecord
  :: ∀ r rl o
   . DefaultRecord rl r o
  => RowToList r rl
  => RProxy r
  -> Record o
makeDefaultRecord r = defaultRecordImpl (RLProxy :: RLProxy rl) r

That function can be used like this:

type Fields = ( email :: String, password :: Maybe String )

-- x = { email: "", password: Nothing }
x = makeDefaultRecord (RProxy :: RProxy Fields)

Thanks for the help!

3 Likes

Yeah, I think writing out the instances individually like that is probably the way to go here. Welcome!

1 Like

Continuing the discussion from Type class that has a Monoid instance seems to require all instances to also be monoids:

Actually, as I read back through this, I haven’t defined a Monoid instance for Summer, so there aren’t overlapping instances. There should only be one instance of Default for the type.

Is there another reason why this would result in a type error that Summer is not an instance of Monoid?

1 Like

Constraints aren’t taken into account when resolving instances, so (perhaps surprisingly) these do actually count as overlapping. GHC does the same thing, see https://downloads.haskell.org/~ghc/7.0.1/docs/html/users_guide/type-class-extensions.html#instance-overlap:

When matching, GHC takes no account of the context of the instance declaration (context1 etc).

I’m sure there are solid technical reasons as to why this is, but I couldn’t tell you what they are off the top of my head.

1 Like

Huh! Well, that’s great to know, thanks!

This is my spin on it (from Slack yesterday):

Constraints like that are assertions, not predicates, if that makes sense … you can use them to assert that some feature must be present, but as Nate said, the compiler won’t backtrack, so it won’t influence instance selection.

Nate also commented that it would be hard to say what constraints are more specific than others. But maybe possible, to some extent :wink: (I might work on lucidating issues like this (overlapping) soon.)

2 Likes

Am I correct in interpreting this as: once I’ve declared Monoid v => Default v as a possible instance, since the compiler ignores constraints, this ends up being treated as the instance for every single type? I’ve accidentally asserted that all Default members are also monoidal?

1 Like

Yeah, that’s accurate!

The compiler’s like, Oh, you defined an instance Default v for any v … oops, it happens to have a Monoid v constraint, I hope I can solve that …

FWIW, I think the simple way to see whether two instances may overlap is to ask whether they could unify: v will unify with Int, or any other type really, and Tuple a Int may sometimes unify with Tuple Int b, etc. (assuming these are “unification variables”/existentially quantified …)

1 Like