Optional
This PR adds an Optional type class that allows one to have an optional element in the row-types. It also adds a convenience function for records that allows one to turn an optional field into a field of an option.
Resolves https://github.com/target/row-types/issues/57.
Design Notes
See the related issue for a more full accounting of the details of Optional. In short, due to GHC's inability to perform type equality at the value level without requiring Typeable (which I really didn't want to add), an Optional constraint must additionally require that if the label exists in the row-type, it must have the optional type. In other words, a constraint like Optional "x" Int r will be satisfied by a row-type r that does not contain "x", but will not type check with a row-type r that has an entry where "x" is String.
Also, I've introduced the function cmpSymbol, which should be upstreamed if possible.
I think I wanted something like this just today.
I have a type Settings, which is a Rec r where all field have type Maybe a (for different a).
Now I want a function
fromPartialSettings :: forall r. PartialSettings r => Rec r -> Settings
that takes a record with any subset of the labels of Settings, and without the maybes. It then map’s Just over the record, and adds all missing fields as Nothing.
I ended up writing this code:
-- The following line noise is me getting out of my way
-- to be able to use `ic_create` etc. by passing a record that contains
-- a subset of settings, without Maybe
type family UnRec r where UnRec (R.Rec r) = r
class DefaultSettings a where defaultSetting :: a
instance DefaultSettings (Maybe a) where defaultSetting = Nothing
type PartialSettings r =
( R.Subset (R.Map Maybe r) (UnRec Settings)
, R.Map Maybe r .+ (UnRec Settings .\\ R.Map Maybe r) ≈ UnRec Settings
, WellBehaved r
, Disjoint (R.Map Maybe r) (UnRec Settings .\\ R.Map Maybe r)
, Forall (UnRec Settings .\\ R.Map Maybe r) DefaultSettings
)
fromPartialSettings :: forall r. PartialSettings r => Rec r -> Settings
fromPartialSettings r =
R.map' Just r .+
(R.default' @DefaultSettings defaultSetting :: Rec (UnRec Settings .\\ R.Map Maybe r))
It kinda works, but I don’t get good type inference when using fromPartialSettings, e.g. I have to write fromPartialSettings (#foo .== (5 :: Natural) even though the compiler could know that the type of foo in Settings is Natural, and there it has to have that type here too.
@nomeata I think we can solve your problem (and simplify your code while we're at it) by using the record overwrite operator .//, which performs a left-biased merge of two records. In your case, you could do something like:
type SettingsRow = ...
type Settings = R.Rec SettingsRow
fromPartialSettings :: (R.FreeForall r, R.Map Maybe r .// SettingsRow ≈ SettingsRow) => R.Rec r -> Settings
fromPartialSettings = R.map' Just r .// (R.default' @DefaultSettings @SettingsRow defaultSetting)
The type signature for fromPartialSettings looks kind of silly, but it's actually exactly what we want to get good type inference.
Also, while the DefaultSettings class is perfectly sensible and concise, I'll point out this crazy IsA thing in Data.Row.Dictionaries that can do something similar:
defaultSettings :: Settings
defaultSettings = R.default' @(R.IsA R.Unconstrained1 Maybe) @SettingsRow d
where
d :: forall a. R.IsA R.Unconstrained1 Maybe a => a
d = case R.as @R.Unconstrained1 @Maybe @a of R.As -> Nothing
The idea here is that the IsA class captures the knowledge that every type in SettingsRow is a Maybe type as a constraint. This allows us to use IsA Unconstrained1 Maybe as the constraint in the Forall, which in turn allows us to call default'. It would be great if we could just replace the d with Nothing, but unfortunately, we need to pattern match on R.as, which leads to a lot of noise in the code.
I've actually been meaning to write a blog post showing how one can use row-types to do exactly this kind of thing. I got the idea from reading https://chrispenner.ca/posts/hkd-options, which is also what inspired me to add barbies instances to row-types.
I still have mixed feelings about this Optional thing, which is why I haven't merged it yet. It's neat, but I'm not sure how much it adds to the library given that one can already do tricks like record overwrite to achieve a similar result.
Indeed, that fixes the type inference problem! I played around with (.//) first, but somehow could not make it work, so thanks!