monet / monet.js

monet.js - Monadic types library for JavaScript
https://monet.github.io/monet.js/
MIT License
1.6k stars 114 forks source link

Maybe monad does not completely respect the left identity monadic law #164

Closed darklight3it closed 6 years ago

darklight3it commented 6 years ago

The three monadic laws (also specified in fantasyland) are the following (chain aka bind):

  1. M.of(a).chain(f) is equivalent to f(a) (left identity)
  2. m.chain(M.of) is equivalent to m (right identity)
  3. m.chain(f).chain(g) is equivalent to m.chain(x => f(x).chain(g)) (associativity)

If you try the first test with a None() it should give you a error. In your code it gives:

None().chain(x => x+2)  // result None

A None Monad chained with a function that returns a non null/undefined value should return a Some.

You could correct this error in this way

bind: function (bindFn) {
    var appliedFn = bindFn(this.val);

    if(this.isValue){
          return bindFn(this.val);
     }

   if(appliedFn === null || appliedFn === undefined){
         return this;
   }

   return bindFn(this.val);
}
cwmyers commented 6 years ago

Hi @darklight3it

Thanks so much for taking the time to write up this issue.

I'm not sure I agree with your assertion. Remember None itself is not a monad, but Maybe is. The monad law for left identity start with M.of(a), which for Maybe will always be a Some because it is the only subclass that can contain a value.

For the second law, None.chain(M.of) should alway return a None, as m is a None.

Remember the f in chain is a function from A => Maybe[B], so really None.chain(x => x + 2) is non-sensical and wouldn't compile in a typed language. It would need to be None.chain(x => Some(x+2)) which should still return a None.

A None Monad chained with a function that returns a non null/undefined value should return a Some.

This is not true according to any of the 3 monad laws. A None can't turn into a Some because there is no value to extract out of the None to "chain" through.

Hopefully I've understood your issue correctly.

darklight3it commented 6 years ago

Thanks for your response, I need some insight on this issue. I'm making a typescript implementation of a Monad library just for fun and I'm facing the same issue.

Maybe i've found the incorrect example. Let's try with yours None.chain(x => Some(x+2)).

//left part as you imagine
None.chain(x => Some(x+2)) = None()

//right part
f(null) = Some(null+2) = Some(2)
f(undefined) = Some(undefined + 2) = Some(NaN)
f(NaN) = Some(NaN +2) = Some(NaN)

The two side of the equation are different and the rule does not seem to value. This is because not every operation implying a null/undefined/NaN value return a null/undefined/NaN. Other implementation of the maybe seem to take into account that case. I suppose also that to make a complete Maybe monad you should check also for NaN (at least in javascript).

The specification is not clear if the (x => Some(x+2)) argument could be a valid one for a None. But if this argument is permitted it certainly breaks the Monad completeness.

ulfryk commented 6 years ago

I have a simple example that may help:

if you have a function const fn = x => Some(x+2)

and left side of our equation:

None().chain(fn)

Then you can be 100% sure that fn will not be called at all, chain will just return this (the None created by None() constructor).

Lambda passed as an argument to the chain operator on Maybe monad in Monet (same with Maybe in Haskell or Option in Scala), will be just ignored - this is how fail-fast is implemented in this case.

darklight3it commented 6 years ago

Yes but this do not resolve my question. Is const fn = x => Some(x+2) a valid value for a chain?

If it is returning None() breaks the left identity law (at least in javascript where operation with null can return a valid value).

Probably in Haskell/Scala (i do not know them) this kind of operations are not permitted and return invalid value and the law still holds.

Sorry for the long discussion, but for me this is really interesting.

ulfryk commented 6 years ago

Yes const fn = x => Some(x+2) is valid value.

And it doesn't break the left identity law.

A None Monad chained with a function that returns a non null/undefined value should return a Some.

Above statement is not true - None monad chained with anything should always return None.

See M.of(a).chain(f) --> M.of(a) will always return Some<typeof a>.

The only case that is tricky here is the fact that M.of(null) (Some(null) / Maybe.of(null)) will throw. That is because monet version of Maybe was meant to overcome null / undefined issues in JavaScript.

See also this discussion: https://github.com/monet/monet.js/issues/53 especially this comment: https://github.com/monet/monet.js/issues/53#issuecomment-212276496

ulfryk commented 6 years ago

@darklight3it - did above explanation help? Maybe there's still something I'm missing…