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

maybe: implement Semigroup and Monoid #11

Merged
merged 1 commit into from
Feb 25, 2015
Merged

maybe: implement Semigroup and Monoid #11

merged 1 commit into from
Feb 25, 2015

Conversation

davidchambers
Copy link
Member

The immediate motivation for this change is to enable filter to be defined for Maybe as suggested by @joneshf in #10. Implementing these specifications for Maybe is a good idea regardless.

@joneshf
Copy link
Member

joneshf commented Feb 25, 2015

👍

davidchambers added a commit that referenced this pull request Feb 25, 2015
maybe: implement Semigroup and Monoid
@davidchambers davidchambers merged commit 893d2de into master Feb 25, 2015
@davidchambers davidchambers deleted the dc-maybe branch February 25, 2015 19:07
Just.prototype.concat = function(maybe) {
return maybe instanceof Just ? Just(this.value.concat(maybe.value)) : this;
};

Choose a reason for hiding this comment

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

Are you somehow expecting to enforce a stronger type than JS does for your Maybes? Do you have somehow a Maybe(String) type different from a Maybe([Number])?

If not, I don't understand your concat. How do you know that you can concat the value of this Maybe with the value of that Maybe? What happens with Maybe.of([1, 2, 3]).concat(Maybe.of("abc")); or vice versa?

Copy link
Member Author

Choose a reason for hiding this comment

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

How do you know that you can concat the value of this Maybe with the value of that Maybe?

We don't, but according to the spec

s.concat(b)
  1. b must be a value of the same Semigroup
  2. If b is not the same semigroup, behaviour of concat is unspecified.

Choose a reason for hiding this comment

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

But are you quoting that in regards to some inner Semigroup or to the Maybe-wrapped one? Are you trying to define types such as

Maybe (String);  
// Maybe('ab').concat (Maybe('cd')); //=> Maybe('abcd')
Maybe (List)
// Maybe([1, 2]).concat (Maybe(['foo'])); //=> Maybe([1, 2, 'foo'])
Maybe (Add)
// Maybe(Add (3)).concat(Maybe(Add (4))):  //=> Maybe(Add (7))
Maybe (Multiply)
// Maybe(Multiply(3)).concat (Maybe(Multiply(4))): // Maybe(Multiply(12))

It seems that is what you would want if you are doing this internal concat. But I see nothing in the implementation that attempts to enforce this sort of stronger typing.

Copy link
Member

Choose a reason for hiding this comment

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

The Semigroup here is Maybe a, so if someone attempts to concat with anything that is not Maybe a (the a must be the same as well), then it is unspecified what happens. There is nothing specifying that the a is also a Semigroup in the implementation, but I'm not sure what you'd do there either.

How should that case be handled? It will already throw a runtime error if concat doesn't exist, what else could you do? More descriptive error possibly?

Choose a reason for hiding this comment

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

(the a must be the same as well)

That's what I was getting at. I see nothing in the implementation to enforce this. Is there meant to be?

There is nothing specifying that the a is also a Semigroup in the implementation

I think you would need to enforce that for this to make any sense, or at least enforce a rule that the values have a concat function. The question is whether you try to enforce something more stringent, namely that the a's are of the same Semigroup. If you do so, you get a useful Semigroup and with either of several possible extensions, you can get useful Monoids out of it, as discussed on Gitter. But I don't see anything here that enforces it.

Choose a reason for hiding this comment

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

There are plenty of ways to shoot yourself in the foot with js. Is there a reason Semigroup/Monoid should be held to some stronger requirements?

No, but I was hoping that Maybe could be. My reason for adding Maybe to a system that otherwise doesn't have one is to prevent having to do all sorts of error-checking across the system, or, in failing to do that, allowing for the possibility of exceptions arising. So I want my Maybe to be as bullet-proof as possible.

But I finally went back and looked at the filter implementation involved, and perhaps I've been confused by the discussion. It looks as though filter has nothing to do with Semigroups or Monoids. There seems to be what might be seen to be a pun on empty here. There seems to be nothing in the definition of filter, explicit or implied, that requires empty be a lawful Monoid identity. filter only requires that such a method exists (and presumably requires that it returns a value of the same Maybe.) Moreover all my concerns above had to do with the definition of concat, and the fact that it could easily throw unexpected exceptions. But that's entirely irrelevant, since the definition of filter being discussed has nothing at all to do with the Maybe being a Semigroup or a Monoid.

So my take again, would be not to add the Semigroup or Monoid without good reason, but since they wouldn't actually get invoked during filter, I'm less bothered by them.

Copy link
Member

Choose a reason for hiding this comment

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

No, but I was hoping that Maybe could be. My reason for adding Maybe to a system that otherwise doesn't have one is to prevent having to do all sorts of error-checking across the system, or, in failing to do that, allowing for the possibility of exceptions arising. So I want my Maybe to be as bullet-proof as possible.

That's an admirable goal. I think the only way to really make that happen is to stop writing vanilla js, or use a different abstraction. Maybe is too low an abstraction to protect you from all the possible ways things can go wrong. That doesn't mean Maybe is worthless, just that it's not up to the task for js. Different languages can use it more powerfully than js though, for the exact reasons you pointed out above. If you have a computer checking your program, you can ensure that you only call concat on the right thing. In js, you can't have that.

But I finally went back and looked at the filter implementation involved, and perhaps I've been confused by the discussion. It looks as though filter has nothing to do with Semigroups or Monoids. There seems to be what might be seen to be a pun on empty here. There seems to be nothing in the definition of filter, explicit or implied, that requires empty be a lawful Monoid identity. filter only requires that such a method exists (and presumably requires that it returns a value of the same Maybe.) Moreover all my concerns above had to do with the definition of concat, and the fact that it could easily throw unexpected exceptions. But that's entirely irrelevant, since the definition of filter being discussed has nothing at all to do with the Maybe being a Semigroup or a Monoid.

This all sprung from my suggestion here: #10 (comment) When I said, "once and for all", I truly meant "once and for all". There shouldn't need to be an implementation of filter on each data type. If you have a data type that is a Monad and a Monoid, that interact properly, you should be able to use the a single filter implementation:

var filter = function(p, m) {
  return m.chain(function(x) {
    return p(x) ? m.of(x) : m.empty();
  });
}

That method doesn't mention Maybe a or [a], or any other data type. It talks only about Monad and Monoid. That implementation is the power of talking about these abstract concepts. We needn't think about, "Does it make sense to filter this data type?" or, "How do I implement a filter on this data type?" No, we only need to be concerned with some questions. does this data type implement Monad? Does this data type implement Monoid? Do they interact properly? If the answer to all of those questions is yes, then we get filter for free. You stick that implementation in some generalized library somewhere, and use it on your data type without even thinking about it.

But what does it mean to interact properly? There's some discussion here: purescript/purescript-control#6 (comment), but the idea is that it should satisfy some laws:

  • Left distributivity with Functor:
(x.concat(y)).map(f) == x.map(f).concat(y.map(f))
  • Right annihilation with Functor
M.empty().map(f) = M.empty()
  • Right distributivity with Apply:
(x.concat(y)).ap(z) == x.ap(z).concat(y.ap(z))
  • Right annihilation with Apply
M.empty().ap(z) = M.empty()
  • Right distributivity with Chain:
(x.concat(y)).chain(f) == x.chain(f).concat(y.chain(f))
  • Left annihilation with Chain
M.empty().chain(f) = M.empty()

Since Maybe a happens to satisfy these laws, it should get filter for free. It has nothing to do with HOW Maybe a implements these functions. Just that they're lawful.

Choose a reason for hiding this comment

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

I want my Maybe to be as bullet-proof as possible.

That's an admirable goal. I think the only way to really make that happen is to stop writing vanilla js, or use a different abstraction. Maybe is too low an abstraction to protect you from all the possible ways things can go wrong.

I wasn't expecting it to solve all my problems, mostly to reduce null-checking. Javascript code that's full of null-checks can be more nicely written with the help of some judiciously-placed Maybes.

I did understand the idea of writing an abstract implementation that would work for any conforming types. But my reference for the ADTs in Javascript has long been FantasyLand, which makes no mention of any interaction between Semigroup/Monoid laws and laws of the types in Monad's genealogy. While the laws you discuss here make a great deal of sense, this is the first I've heard of them.

And I still have significant reservations because although the filter function does not actually use concat, the concat proposed for Maybe seems designed for Monoid a => Monoid (Maybe a) (likely just Semigroup, but Monoid seems more likely useful.) However I see nothing in the Sanctuary implementation that says that these Maybes are more strongly typed than Maybe *. Declaring a Semigroup/Monoid on Maybe * doesn't make sense to me. And yet the notion of filter does make sense for Maybe *, with a fairly obvious implementation:

Nothing.prototype.filter = function(predicate) {return this;}
Just.prototype.filter = function(predicate) {
    return predicate(this.value) ? this : new Nothing();
}

I do understand that this loses out by not taking advantage of generic laws. But it seems one could only do so by pushing more of an external type system into Javascript.

Am I making sense here? Or is my lack of experience with languages like Haskell still making me miss something fundamental? I do have experience with Monoids, Semigroups, etc., but from a mathematical, not a programming point of view. I did take one course in Category Theory in grad school; it was my least favourite course and helped steer me to more discrete fields; my work was in combinatorics and graph theory. I really remember nothing of Monads or some of these other types. And I have less feel for them than I do for the sorts of types that make up an abstract algebra course.

Copy link
Member

Choose a reason for hiding this comment

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

I wasn't expecting it to solve all my problems, mostly to reduce null-checking. Javascript code that's full of null-checks can be more nicely written with the help of some judiciously-placed Maybes.

Sorry, I think my remark was a little snarky. That wasn't my intention.

You're making perfect sense. My qualm is just that the other functions don't do any checking, but you want concat to do a bunch. I don't understand why concat needs to be special. map doesn't ensure that its argument is a function, ap doesn't ensure that the Just holds a function or that its argument is a Maybe a, chain doesn't ensure that the argument is a function. And none of them check to ensure that the return value is the proper type. Why should concat check that it holds a value that is a Semigroup a, and that its argument is a Maybe holding the same Semigroup a?

Copy link

Choose a reason for hiding this comment

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

Sorry, I think my remark was a little snarky. That wasn't my intention.

I didn't sense snark. I sensed us likely talking past each other. Most likely confusion, although perhaps some real fundamental disagreement too. It's hard because I have a mix of strong backgrounds in certain of the underlying topics and weak ones in others -- but strong opinions about everything, I'm afraid!

My qualm is just that the other functions don't do any checking, but you want concat to do a bunch. I don't understand why concat needs to be special.

It's more basic than that for me. I simply don't see a reason to include concat. I haven't seen any good argument that the sort of Maybe so far defined should be a Semigroup or a Monoid. And because of the types of issues discussed, I see real issues with trying to define it that way.

But I think even further down is the notion that I suspect you don't think about Maybe as a fully-fledged type. Am I right? Do you think in terms of Maybe a for some other type a? To me, the Maybe type as so far defined by Sanctuary, is a type on its own as far as I can tell. And that might explain a lot of our disagreement. Such a type as I see of course cannot comfortably be made part of a Semigroup. Did I get your position right? Does this help explain our differences? If so, do you want to try to convince me that Maybe, regardless of current implementation, should only be thought of as Maybe a?

@buzzdecafe
Copy link

It will already throw a runtime error if concat doesn't exist, what else could you do?

wrap it in a Maybe! oh wait ....

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