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 Apply instance for Map #16

Merged
merged 4 commits into from
Dec 23, 2020
Merged

Conversation

rhendric
Copy link
Member

No description provided.

@hdgarrood
Copy link
Contributor

hdgarrood commented Feb 26, 2019

Seems sensible, but there are a couple of questions I'd like to answer first:

  • Are there any other possible Apply instances for Map?
  • Can we add Bind too?

@rhendric
Copy link
Member Author

rhendric commented Feb 26, 2019

Adding Bind does seem like a good idea; I can toss that in.

I don't think there are any other possible Applys, though I don't have a proof off the top of my head right now. This is the one Haskell's Map uses, if that argument holds any weight.

Edit: There is a trivial apply: apply _ _ = empty does satisfy all of the Apply laws. There's probably an argument for the intersectionWith-based Apply being the universal Apply, in the category-theoretic sense (in other words, the hypothesis is that every legal Apply must be factorable into the intersectionWith-based Apply composed with some other function).

@rhendric
Copy link
Member Author

rhendric commented Feb 27, 2019

Having reflected on it a bit, the exact property that this Apply instance satisfies is something that I don't think I have a name for. I want to pretend that a Map is isomorphic to the partial application of lookup on it—in other words, constrain candidate Apply implementations to those that don't ‘look too closely’ at the key set of the map. In this pleasant fantasy, type Map a b = a -> Maybe b, and apply must have signature forall a b c. (a -> Maybe (b -> c)) -> (a -> Maybe b) -> a -> Maybe c. There are only two (terminating) inhabitants of that type:

trivialApply :: forall a b c. (a -> Maybe (b -> c)) -> (a -> Maybe b) -> a -> Maybe c
trivialApply _ _ _ = Nothing

apply :: forall a b c. (a -> Maybe (b -> c)) -> (a -> Maybe b) -> a -> Maybe c
apply m1 m2 a = m1 a <*> m2 a

(Sketch of proof: any non-Nothing results of apply must contain a c; the only available source of cs is the first map; the only available source of bs is the second map; the only way to retrieve from either map is to pass the one a we have to both; and if either retrieval fails, the only alternative is to return Nothing after all. Therefore, apply either always returns Nothing, or calls both maps and applys the results.)

Obviously trivialApply is useless. The other implementation is equivalent to the intersectWith-based apply from Haskell, when composed with lookup.

What I don't have an argument for is why I want to make the initial constraint that an Apply instance shouldn't look at the key set of the map. My instincts tell me that this is a good thing, and that an Apply instance that is key-set agnostic in this way is somehow ipso facto superior to one that isn't, but if you asked me to justify it, I'd look at the floor and mumble some vaguely mathematical words until you realize that I don't know what I'm talking about.

... Maybe that, coupled with the precedent that Haskell has set, is good enough for you?

@hdgarrood
Copy link
Contributor

Thanks! yeah that's some really good analysis, definitely good enough for me. Do you think a similar line of argument would work for Bind too?

@rhendric
Copy link
Member Author

Yeah, if the type Map a b = a -> Maybe b fiction is convincing to you, then Bind breaks down the same way. By following the types as before, you have

trivialBind :: forall a b c. (a -> Maybe b) -> (b -> a -> Maybe c) -> a -> Maybe c
trivialBind _ _ _ = Nothing

bind :: forall a b c. (a -> Maybe b) -> (b -> a -> Maybe c) -> a -> Maybe c
bind m f a = m a >>= flip f a

as the only two inhabitants, and the nontrivial one corresponds to the mapMaybeWithKey implementation. (This is a little more obscured by the fact that mapMaybeWithKey is a more complicated function, but after applying the type Map a b = a -> Maybe b transformation, its signature is practically bind's already, modulo a few flips. The key observation is that mapMaybeWithKey only uses the keys from the map it operates on to do two things: call the equivalent of flip f, and tabulate results in the map it produces, which makes it key-set agnostic enough for our pretend isomorphism, and then since it clearly doesn't do the trivial thing, and it terminates, it must do the right thing.)

Copy link
Contributor

@hdgarrood hdgarrood left a comment

Choose a reason for hiding this comment

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

Sounds good. Thanks! I'll just leave this open for a while in case anyone else wants to add any comments.

@masaeedu
Copy link
Contributor

masaeedu commented Dec 2, 2019

If we look at the equivalent minimal definition zip :: (f a, f b) -> f (a, b) for Apply, there's always the zip you define, and an extra zip' = fmap swap . zip . swap that is also lawful.

So just like there's no guarantee of there being some special canonical Semigroup for a given type (in fact every type has the trivial (<>) = const semigroup, plus the Dual semigroup of whatever it is you define), I don't think there's any guarantee of having a special canonical Apply for a given F :: * -> *.

All this to say that @rhendric's Apply instance looks good and lawful, and I don't think we should be too worried about ensuring it's the only lawful Apply instance for this type constructor.

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