babygau
babygau

Reputation: 1581

Why Monad's bind() method must return a func which then return a Monad?

Could anyone please tell me the difference between map/fmap (defined by Functor) and flatMap/bind/lift (defined by Monads) in https://gist.github.com/babygau/b80861ff5eabc582f7fa#file-monad-js? The former takes a function and return a Monad, the latter takes a function and return a function which then also returns a Monad. The question is why the latter need take extra step to return a Monad???

// Monads apply a function that returns a wrapped values to a wrapped value
// and then return a wrapped value
// Monads are chainable
class Wrapper {
  constructor(val) {
    console.log('Created a wrapped value ' + val);
    this.val = val;
  }
  // Functor's `fmap` in Haskell
  map(func) {
    return new Wrapper(func(this.val));
  }
  // Monad's `>>=` (pronounced bind) in Haskell
  flatMap(func) {
    return func(this.val);
  }
  // Monad's `return` in Haskell
  static of(val) {
    return new Wrapper(val);
  }
}

// Monad
console.log(Wrapper.of([1,2,3]).flatMap(function(val) {val.push(4);return Wrapper.of(val);})); //=> [1, 2, 3, 4]

// Functor
console.log(Wrapper.of([1,2,3]).map(function(val) {val.push(4); return val;})); //=> [1, 2, 3, 4]

Upvotes: 2

Views: 587

Answers (4)

Gjorgi Kjosev
Gjorgi Kjosev

Reputation: 1559

There are various reasons: sometimes its not even possible to return a value. Take JS promises for example, and lets say bind was then. If you do an async operation within thens callback you cannot return a value - but you can return a promise.

In most languages with static types its not possible for the callback to then to return both a sync value or a promise. So promises in those languages would have 2 methods: fmap only lets you apply sync functions to the promise wrapper, bind only async.

Upvotes: 0

Bartek Banachewicz
Bartek Banachewicz

Reputation: 39380

Your example:

// Monad
console.log(Wrapper.of([1,2,3]).flatMap(function(val) {val.push(4);return Wrapper.of(val);})); //=> [1, 2, 3, 4]

// Functor
console.log(Wrapper.of([1,2,3]).map(function(val) {val.push(4); return val;})); //=> [1, 2, 3, 4]

Is overly simplified. If you have a function of type a -> b, like you do (.push(4) :: Array -> Array), it's trivial to convert it to a -> m b (this is return), or relatively easy to lift it directly to m a -> m b like your map does.

However, we're looking at a case where your Wrapper is an Identity Monad. Consider real Maybe:

divBy2 :: Int -> Maybe Int
divBy2 a = if a `mod` 2 == 0 then Just a `div` 2 else Nothing

This function expresses a computation in Maybe very clearly. If you have >>=, you can chain it easily:

pure 8 >>= divBy2 >>= divBy2

However, if you only have fmap, there's no way of chaining such a function! (Without using something like join, which is defined in terms of >>= anyway).

Specifically, if you have a value 5 :: Int and a function Int -> Maybe Int, you can feed it in that function. But once you've done that, and have a value of Maybe Int, you'd need to extract the value out of the Maybe, and only then feed it to the next function. This is exactly what >>= is doing.

Functions returning actual monadic computations are very powerful, and that's where they have advantage over Functors.

Upvotes: 1

Carl Lei
Carl Lei

Reputation: 131

fmap/map, usually defined like (a -> b) -> f a -> f b in Haskell, and usually more like f a -> (a -> b) -> f b in js, expect the provided function to transform only the value in the monad, and the result monad is created by fmap.

bind/flatMap/chain, has a signature of f a -> (a -> f b) -> f b, has more freedom --- it can return monadic values beyond what Monad.of(singleValue) can express.

As an example, the List monad has a fmap method to return a List of the same length with values transformed by provided function, so that:

fmap (1 +) [1, 2, 3] == [2, 3, 4]

But what if you want a List of different length? It is not possible with fmap because fmap always return a List of the same length. bind solves that.

Prelude> [1..3] >>= \x -> [x..x+2]
[1,2,3,2,3,4,3,4,5]

bind, or flatMap, for Lists are usually defined as concatenating the resulting Lists together, so that you can return a List to say "I want this element to become zero, or 2, or more elements in the resulting List".

Upvotes: 3

ralh
ralh

Reputation: 2564

Because it can make for elegant and convenient code. For example, you have multiple operations that can return MyObject or null (in a monad, of course, so perhaps Option<MyObject> or so). You can easily chain them together, without actually checking if they returned null or not. If any operation returns null, all the other operations simply won't do anything.

//I assume Option is a kind of Wrapper
function canReturnNull(myVal) {
    //something that can return Option(myChangedVal) or Option(null)
}

var result = Option(myObj).flatMap(canReturnNull1).flatMap(canReturnNull2).flatMap(canReturnNull3);

If it returned a value instead of a monad, I could not chain the operations together like that.

That is, of course, for the Option monad. I recommend reading about the various kinds of monads in Haskell tutorials.

Upvotes: 1

Related Questions