getify / Functional-Light-JS

Pragmatic, balanced FP in JavaScript. @FLJSBook on twitter.
http://FLJSBook.com
Other
16.65k stars 1.96k forks source link

Incorrect description of flatMap (Appendix B) #210

Open PawelJ-PL opened 2 years ago

PawelJ-PL commented 2 years ago

Yes, I promise I've read the Contributions Guidelines (please feel free to remove this line -- if you leave this line here, I'm going to assume you didn't actually read it).

I think Appendix B incorrectly describes the flatMap (chain, bind) monad method. From the description, this method is used to leave the monad (as an example, the identity function was used to return a value without a "wrapper"). However, the monad does not actually expose any method for doing this.

The flatMap method is used to combine subsequent monads together in a sequential manner, which is the main strength of monads.

The flatMap function signature could look like this (A is a type of current value, F is the monad itself):

function flatMap<B>(fn: (value: A) => F<B>)

Therefore, the identity function cannot be used inside flatMap. In my opinion, the following is a valid example of using flatMap:


const a = Just.pure(5)
const fn = (value) => Just.pure((value + 3).toString())

const b = a.flatMap(fn) // result: Just("8")
getify commented 2 years ago

The intent is to show that chain(identity) can be used to reduce the monad to its underlying value (since chain does not re-wrap the way map does), not that this is the main purpose.

That usage is more for the purposes of either debugging or for connecting FP code to non-FP code. As such, it's not as relevant what the typical/implied type signatures of chain(..) from certain libraries or languages are, especially since this is explicitly un-typed JS.

This isn't a monadic law violation, even if it's atypical. FWIW, in my more recent monad uses and the library I wrote, I provide fold(..) on the monad instances, for this use-case, since the implied type signature isn't as strong as it is for chain(..).

PawelJ-PL commented 2 years ago

The chain (flatMap) from monad does not have the ability to unwrap underlaying value. The monad itself does not have this ability at all. In some languages we can use pattern matching for this purpose. Some monads have additional methods (often prefixed with unsafe) like unsafeToPromise or getOrElse. But value can not be unwrapped with flatMap. This method can only accept function returning value wrapped in another monad of the same outer type (for example flatMap on Either can accept only function returning another Either).

Difference between flatMap (chain) and map:

const a = Just.pure(5)

const fn1 = (value) => Just.pure(value + 3)
const fn2 = (value) => value + 3

// map
a.map(fn1) // result: Just(Just(8))
a.map(fn2) // result: Just(8)

// flatMap
a.flatMap(fn1) // result: Just(8)
a.flatMap(fn2) // incorrect - fn2 doesn't return Just monad. It can't be used here
PawelJ-PL commented 2 years ago

This is also shown in the fantasyland you mentioned in the book: https://github.com/fantasyland/fantasy-land#fantasy-landchain-method

fantasy-land/chain :: Chain m => m a ~> (a -> m b) -> m b

getify commented 2 years ago

The monad laws do not imply a type signature. The FL spec is an interpretation of those laws into JS, but they're not the authority that gets to tell everyone else how their methods should look. I don't care to be FL-compliant, so I don't have any need to fit into their type signature.

The monad laws exhibit/require a set of capabilities. The monad I've described in this book is able to do all the things that the monad laws require. The fact that it can also do something else non-monadic doesn't invalidate its monad-ness.

getify commented 2 years ago

Moreover, I remind you that you're focused on an appendix to a book written over 5 years ago. The views presented there are how I felt about monads 5 years ago, when I was very early on in my journey. I have a more well-rounded perspective on monads now, and my current explorations of them do not involve using flatMap(..) / chain(..) as a means to unwrap. As I said, I now provide fold(..) on my monads for the purposes of natural transformation.

PawelJ-PL commented 2 years ago

This comes from the monad laws. First example to explain the monad laws

monad

(note that in Haskell return Inject a value into the monadic type.). Other sources point to the same laws.

I feel that you perceive this comment as an attack, while I wanted to highlight the error (I still claim that there is an mistake in the book) so that this can be corrected in future releases. Currently it misses a very important feature of the monad which is the composition of functions for sequential data processing with the possibility to fail fast at the first failure.

getify commented 2 years ago

You are entitled to the opinion that what's presented there is an "error", but I just don't happen to agree. It's certainly an alternative (and additional, less mainstream) point-of-view about chain(..) / flatMap(..), and as I've already pointed out, it's clearly NOT the main purpose of that method.

For the sake of clarity (posterity reading this), here's the relevant section from the appendix:

That's the same kind of thing going on with a monad's chain(..) (often referred to as flatMap(..)). Instead of getting a monad holding the value as map(..) does, chain(..) additionally flattens the monad into the underlying value. Actually, instead of creating that intermediate monad only to immediately flatten it, chain(..) is generally implemented more performantly to just take a shortcut and not create the monad in the first place. Either way, the end result is the same.

One way to illustrate chain(..) in this manner is in combination with the identity(..) utility (see Chapter 3), to effectively extract a value from a monad:

...

A.chain(..) calls identity(..) with the value in A, and whatever value identity(..) returns (10 in this case) just comes right out without any intervening monad. In other words, from that earlier Just(..) code listing, we wouldn't actually need to include that optional inspect(..) helper, as chain(identity) accomplishes the same goal; it's purely for ease of debugging as we learn monads.

So what I said was:

  1. chain(..) (aka flatMap(..)) conceptually acts to flatten out the nesting of monad<monad> that would otherwise occur if you did map(..) and the function returned a monad. This is pointed out to be the same as doing arr.map(v => [ v * 2 ]), where the result would be an array of nested arrays.
  2. Furthermore, I point out that this concept of "wrapping and then unwrapping" is often inefficient in implementation, so usually the wrapping container is just skipped.
  3. And one way to illustrate the fact (that the wrapping is skipped) is to show that chain(identity) would in practice just extract the value.
  4. Finally, I mused that this usage (extracting the value) is basically the same purpose as the non-standard _inspect() method, which is for debugging.

I stand by that characterization.

The appendix is not in any way a full dissertation on monads or the fullness of everything that these capabilities provides. I readily admit that at the time of writing, I didn't really appreciate most of the utility/importance of monads, which is why this content was placed in an appendix as sort of "bonus" material rather than the main content of the book. So the fact that I don't cover, in more depth, what chain(..) is primarily for, is consistent with the narrow view I held at the time.

I have, more recently, written a much more complete explanation of my perspective on monads, including detailed coverage of the point of the chain(..) method (and its name alias flatMap(..)). Moreover, the use-case in question (exiting a monad, extracting a value, etc) is covered, not with chain(..) but with fold(..).

Were I to write another book on FP and to cover monads, this guides reflects how I would cover it, much more so than the 5 year old appendix we're discussing presently.

But what it comes down to is, I don't think what I said back then was an error. I think it was limited and naive because at the time I didn't understand or appreciate much about monads, and now I understand and appreciate them more.

However, neither then nor now, do I choose to constrain my perspective on the utility of monads with type signatures (in JS or any other language). Some people find comfort in the type signatures. I do not.