mostly-adequate-guide
mostly-adequate-guide copied to clipboard
`Left` given in Appendix B is broken and question if Left obeys laws
First thing, noticed that Left given in Appendix B is broken
Left.of(1).map( x => x + 1) // Right (2)
Furthermore, the chain
method returns the value in a container while Right
returns value without container, ie map(f(x)).join()
Another question in general is can Left
or Nothing
ever be constructed to obey the Laws?
for example such that
X.prototype.map = function map(f) {
return this.chain(a => this.constructor.of(f(a)));
};
// ap derived from chain/map
X.prototype.ap = function ap(other) {
return this.chain(f => other.map(f));
};
I ask because Left
does have an ap
method, therefore it has a map
method. (?)
Ramda-fantasy's Either seems to also behave in a similar way. Sanctuary uses independent functions, but their chain looks to behave like map
to my naive eye
https://github.com/sanctuary-js/sanctuary#maybefantasy-landchain--maybeaa-maybeb---maybeb
Not sure how one would construct an ap
or map
from Sanctuary's chain
Well, this is rather tricky question and since you're not the first one, I believe we need to re-work a bit the first introduction to of
. The implementation is indeed broken, but not because Left.of
returns Right
(that is simply a consequence of it), but because Left.of
shouldn't exist in the first place!
It doesn't make much sense in the end to define a function of
on Left
or Right
because they aren't types but values. The of
interface is meant to lift a single value inside a container a.k.a a particular type.
Please, have a look at this discussion here: https://github.com/MostlyAdequate/mostly-adequate-guide/issues/371
About the chain
method, I am not sure to understand your point. The whole point of chain
is to remove the outermost layer of a nested monad because of the hypothesis that m(m a)
will behave the same way as m a
as long as m
is a Monad. So indeed, by using chain, we "pop" one of those layer.
@KtorZ
Thanks for the description of Left
and Right
In the current implementation of Left
, chain
doesn't remove the item from the outermost layer.
I'll try distill my cloud of confusion down to some words...
I'm not too familiar with the Category Theory behind it but... is chain
about implementing some intent from CT or a mathematical transcription. For example, is chain
intended return a functor more than merely unwrap a value?
In other words, is chain
intended to unwrap a contained functor and not merely anything inside?
... or is chain
intended to return a functor of the same kind--endofunctor? Sorry if I'm just throwing slang around I don't fully appreciate just yet! Is this related to "a monad is monoid in the category of endofunctors"?
... or is chain just literally like (apply thing and unwrap), kind of like fold
?
Hey @babakness, an easy way to picture chain
is to think of it as a function to sequentially compose two actions, passing the result of the first action to the second one. In the end, that's what it is about: composition.
If you "encode" the previous sentence with type signatures, you have this:
chain :: Monad m => m a -> (a -> m b) -> m b
If we take the concrete case of the Either e
Monad, we get this:
chain :: Either e a -> (a -> Either e b) -> Either e b
Now remember that, Either e
is a special Monad that doesn't run actions when it represents a Left
side. This is true for map
, but also for chain
(since the latter can be defined in function of the former). Hence, chaining an action to an erroneous Either e a
value has no effect and simply returns an erroneous (Left side) Either e b
. Note that, in our implementation, we "cheat" a bit because of the very nature of JavaScript: it isn't a statically-typed language. To be more correct, we would have to return a new Left
instance of the target Either e b
, carrying the same left value than our current Either e a
. In short, it simply becomes: this
which is rather safe because our value is non-transient here, and no matter what, that is the latest stage of it, no further action can be applied to this value.
Side-note: Remark that I am never talking about the
Either
Monad because this doesn't make sense. In order to obtain a Monad (or even a Functor), we have to fix the type of one of the two sides ofEither
and, by construction, we've chosen to fix theLeft
side. That's why we always talk in terms ofEither e
.
@KtorZ
Thanks for the feedback, I think I understand a little bit more... the type signature was helpful. So let me see if I have it right:
chain :: Monad m => m a -> (a -> m b) -> m b
chain
takes a Monad m
of a
and a function that takes a
and returns the same Monad of b
...
const m = Identity.of( 1 )
const get_b = a => a + 1
m.chain( a => m.of( get_b( a ) ) )
If correct, is the the same part correct and a salient point of chain
? Here is a contrived example of chain
returning a different functor.
// Contrived example
Identity( 'example.com/api' )
.map( doStuff )
.map( doMoreStuff )
// key question, is this an abuse of the Monad?
// `chain` of Identity is returning a different Functor
.chain( x => validApi(x) ? Just(x) : Nothing )
// Now we're working with a Maybe not Identity
.map( ifValidApiStuff )
.map( etc )
It seems that chain
is returning a different functor from Identity
. Does this disqualify Identity as an Endofunctor and, therefore, disqualify Identity
as a Monad?
// key question, is this an abuse of the Monad? // `chain` of Identity is returning a different Functor .chain( x => validApi(x) ? Just(x) : Nothing )
Yes. Or more exactly, not an abuse of the Monad, but an abuse of our implementation. Let me explain.
The material from this book serves an educational purpose, especially the implementations that are kept simple on purpose, in order to focus on the ideas rather than the details. Having said that, your example turns out to be a valid JavaScript example, because:
- 1/ JavaScript is neither strong nor statically typed
- 2/ Our implementation of
chain
doesn't dare to check whether the function you give actually returns a Monad of the same type.
In a pure functional language where those concepts apply, you'll rely on a compiler to check those things. In Haskell for instance, this example won't compile for the reason that is currently bothering you: you're trying to introduce another Monad in your expression. There are ways to work-around that if needed via Monad Transformers or Free Monads , which both allow you to combine different kind of Monads together; Yet, this is probably a bit too advance for now. Provided we stick with simple Monads like in your example, the expression is actually invalid.
Underneath it all, your intuition is right, but JavaScript doesn't give us all the necessary tools to achieve such level of constraints with the usage of chain
(as well as every other functions that our algebraic structures offer). We simply rely on an implicit contract with the user who should know and not try to abuse these methods by using the right types.
We could have possibly added some assertions here and there to make the code loudly complain at runtime when provided with inputs from an invalid type. This has been done partly with the implementations used in the exercises, but we show an uncluttered and simpler version in the appendix.