Skip to content
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

Merged
merged 12 commits into from
Jul 28, 2017

Conversation

MonoidMusician
Copy link
Collaborator

Okay here's my stab at it.

  • The compiler insists that class VariantRecordElim, class VariantMatch must be exported – is this true? it seems a bit ugly ...
  • I just used StrMap.lookup, would you prefer FFI?
  • Let me know if you want anything renamed, or if you want the helper classes merged, etc.

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!

@cryogenian cryogenian mentioned this pull request Jul 12, 2017
, RowToList record rlist
, VariantMatch vlist rlist result
, ListToRow vlist variant
, ListToRow rlist record )
Copy link
Owner

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 ListToRows are necessary.

Copy link
Collaborator Author

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.

Copy link
Contributor

@LiamGoodacre LiamGoodacre Jul 12, 2017

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.

-- | 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
Copy link
Owner

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.

Copy link
Owner

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?

Copy link
Owner

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.

Copy link
Collaborator Author

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?

Copy link
Owner

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.

Copy link
Owner

@natefaubion natefaubion Jul 17, 2017

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).

Copy link
Collaborator Author

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
  }

Copy link
Owner

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 :)

Copy link
Contributor

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 😉.

Copy link
Collaborator Author

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 😂 .

, rlist → vlist result
instance variantMatchNil
∷ VariantMatch R.Nil R.Nil r
instance variantMatchCons
Copy link
Owner

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.

elim v r =
case coerceV v of
Tuple tag a →
a # unsafePartial fromJust
Copy link
Owner

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.

Copy link
Collaborator Author

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!

@MonoidMusician
Copy link
Collaborator Author

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 applyRecord and such, since we really are just matching on corresponding rows and checking a relation.

Grant and pick

I decided to change the name of elim to grant, since the Variant is giving it's value to a Record, and now there's a dual(?) called pick where the Variant decides which row in the Record it gets applied to. Does that make sense? Basically the difference is where the function is – Variant or Record.

(record ∷ # Type)
result
| variant result → record
, record → variant result
Copy link
Contributor

@LiamGoodacre LiamGoodacre Jul 12, 2017

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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!).

@MonoidMusician
Copy link
Collaborator Author

Okay, I think this is ready to be merged – thanks for the unsafeGet PR!

-- | 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
Copy link
Owner

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

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like VariantMatchBranch, maybe?

Copy link
Collaborator Author

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.

@natefaubion natefaubion merged commit 2e3fd81 into natefaubion:master Jul 28, 2017
@natefaubion
Copy link
Owner

Thanks for the PR!

@MonoidMusician
Copy link
Collaborator Author

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants