-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add record elimination, tests and docs #6
Conversation
src/Data/Variant.purs
Outdated
, RowToList record rlist | ||
, VariantMatch vlist rlist result | ||
, ListToRow vlist variant | ||
, ListToRow rlist record ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think these ListToRow
s are necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@LiamGoodacre was suggesting it helps with the bidirectional fundeps. I can play around with it, see what makes the compiler happy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think they are: in different situations either variant
or record
are being computed - you need something to produce what form the other should take (neither RowToList
nor VariantMatch
do this). If you only consider them both together as "input", then you won't need the ListToRow
. But then your type inference will suck.
src/Data/Variant.purs
Outdated
-- | that even `id` must be given a type annotation to be used in this way, | ||
-- | since it normally depends on `Category`: use `id :: forall a. a -> a` not | ||
-- | `id :: forall a c. Category c => c a a`. | ||
elim |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for documenting the limitation well. I don't think there's anything to be done about it right now. Maybe when we get instance chains and constraint kinds, there's something we could do about it, but I'm not sure.
Maybe fold
or foldVariant
would be a better name? We don't really use elim
in the ecosystem, but folds are quite common.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also write this for Data.Functor.Variant
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, for a canonical fold
, the eliminator should come first. This works much better with currying. I don't mind having a flipped variant, but just like case_
takes the variant last, fold
should take the variant last.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think my comments are actually incorrect, and the limitation mostly applies to let bindings and records written inline, which seem to follow different inference rules. Maybe I should reduce the note to, if you're getting an ambiguous type error, move the declaration up a level or two or give it an explicit type ...
I don't like fold
as it's just one value being applied to one function – got any other ideas? handleMany[Cases]
? match
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's like saying an uncurried form of foldr
isn't a list fold :) A record to handle all the possible cases is precisely the fold
for a given closed variant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm crossing wires a bit :) The signature for foldList
I gave is the fold for lists, whether or not it's represented with Variant
or data constructors or Church-encoding. It's not possible to define a definition of List
with Variant
without a newtype or VariantF
+ Mu
. If I were to use VariantF
, it might be:
type ListF a f = VariantF (nil :: FProxy (Const Unit), cons :: FProxy (Tuple a))) f
type List a = Mu (ListF a)
In which case, the proposed record eliminator would be the actual fold when run with cata
(a generalized folding operation).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get that Variant
encodes sum types, but I think that elim
is more of a matching operation, and in order to get a folding operation you have to add something extra – aka cata
(or explicit recursion) – so I wouldn't say that elim
itself is a fold. (Again, it only applies one function to one value, there's no default value equivalent to like fold Nothing == mempty
, and so on.)
So I guess I'm leaning towards match
or cases
as a name.
case _ of
Nil -> Nothing
Cons h t -> Just h
match
{ nil: \_ -> Nothing -- const Nothing
, cons: \(Tuple h t) -> Just h -- Just <<< fst
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A fold is just case analysis (which is why I suggested it as an alternative to elim
). foldr :: (a -> r -> r) -> r -> List a -> r
is merely case analysis for lists (an argument to handle Cons
and an argument to handle Nil
, its not about defaults). For real world examples of this, look at purescript-argonaut-core. That said, I'm fine with match
😛. It's as good a name as any, and probably more what people would expect :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anecdata: I'd call this fold
. I guess match
works too 😉.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anecdata.
Ooh, nice dime word. Anyways, let's call it match
(thanks @cryogenian) and I'll stop arguing 😂 .
src/Data/Variant.purs
Outdated
, rlist → vlist result | ||
instance variantMatchNil | ||
∷ VariantMatch R.Nil R.Nil r | ||
instance variantMatchCons |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anything that shows up in a public signature must be exported. I think we should move these to a Data.Variant.Internal
module, probably.
src/Data/Variant.purs
Outdated
elim v r = | ||
case coerceV v of | ||
Tuple tag a → | ||
a # unsafePartial fromJust |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to see what happens with doolse/purescript-records#1. I don't mind StrMap
for a proof of concept, but I don't like to rely on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay, I'll keep on top of releases of purescript-record
now, looking forward to it!
I was playing around with RowList and I think I found a nice abstraction: https://gist.github.com/MonoidMusician/fe7d35c437c23833a5ca5b67a5ca3744 We could maybe put the classes in typelevel and just use them in variant, what do you think about that? The next layer of abstraction would be a ZipWith kind of thing, for Grant and pickI decided to change the name of |
src/Data/Variant.purs
Outdated
(record ∷ # Type) | ||
result | ||
| variant result → record | ||
, record → variant result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these fundeps should be | variant -> record result, record -> variant result
as the instances (delegated to rowlists) only match on the variant and record - and only one of these is sufficient to infer the structure for the other. We never match on result
, so it can always be determined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this type class could have | -> variant record result
, but the other one should have | vlist -> rlist result, rlist -> vlist result
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason why | -> variant record result
makes sense here is that there is only ever one instance that doesn't even pattern match on it's parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I played around with this a bit, and I couldn't find any difference between the fundep signatures – the same examples seem to compile with either signature, so I've taken your suggestions, I just don't understand why it works (yet!).
Okay, I think this is ready to be merged – thanks for the |
src/Data/Variant/Internal.purs
Outdated
-- | Checks that a `RowList` matches the argument to be given to the function | ||
-- | in the other `RowList` with the same label, such that it will produce the | ||
-- | result type. | ||
class RLMatch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These typeclass names surface when there's a type error with one of the branches. Do you think we could change it to something more descriptive than RLMatch
?
Could not match type
Int
with type
String
while solving type class constraint
Data.Variant.Internal.RLMatch t3
t4
String
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like VariantMatchBranch
, maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, good point! It's a pity that this is overlapping though:
instance variantMatchAFail
∷ Fail (TypeConcat "Unhandled variant case " (TypeConcat sym (TypeConcat " of type " (TypeString a))))
⇒ VariantMatchEachCase (R.Cons sym a v) R.Nil res
[1/1 OverlappingInstances]
Overlapping type class instances found for
Data.Variant.Internal.VariantMatchEachCase (Cons "bar" String (Cons "baz" Boolean (Cons "foo" Int Nil)))
Nil
String
The following instances were found:
Data.Variant.Internal.variantMatchAFail (chosen)
Data.Variant.Internal.variantMatchCons
Data.Variant.Internal.variantMatchNil
[1/1 NoInstanceFound] test/Variant.purs:62:14
v
62 match' = match
63 { }
^
A custom type error occurred while solving type class constraints:
Unhandled variant case bar of type String
Looks nice, but it only seemed to work when the record is empty ... Hopefully the errors with Cons
are sufficiently obvious.
Thanks for the PR! |
Thank you! |
Okay here's my stab at it.
class VariantRecordElim, class VariantMatch
must be exported – is this true? it seems a bit ugly ...StrMap.lookup
, would you prefer FFI?And of course typeclass polymorphism doesn't work from inside records, but I can't even begin to think of how to accomplish that. Otherwise it seems to work well!