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 (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