Skip to content

Instantly share code, notes, and snippets.

@getify
Last active March 3, 2023 09:23
Show Gist options
  • Save getify/2dc45c9a82cfd93358fbffd21bdd601d to your computer and use it in GitHub Desktop.
Save getify/2dc45c9a82cfd93358fbffd21bdd601d to your computer and use it in GitHub Desktop.
is Maybe a "monad?
// is Just(..) a monad? Well, it's a monad constructor.
// Its instances are certainly monads.
function Just(v) {
return { map, chain, ap };
function map(fn) {
return Just(fn(v));
}
function chain(fn) {
return fn(v);
}
function ap(monad) {
monad.map(v);
}
}
// is Nothing() a monad? Well, it's a monad constructor.
// Its instances are certainly monads.
function Nothing() {
return { map: Nothing, chain: Nothing, ap: Nothing };
}
// This is how Maybe(..) is usually implemented.
// But Maybe(..) here doesn't construct pure/valid monad instances,
// since its map() does a value-type check, which is a no-no.
function Maybe(v) {
return { map, chain, ap };
function map(fn) {
if (v == null) return Nothing();
return Just(fn(v));
}
function chain(fn) {
return fn(v);
}
function ap(monad) {
return monad.map(v);
}
}
var identity = v => v;
var prop = k => o => o[k];
var myObj = { something: { other: { and: 42 } } };
Maybe( myObj )
.map( prop("something") )
.map( prop("other") )
.map( prop("and") )
.chain( identity ); // 42
// This is a more "pure" / accurate implementation of Maybe:
// But, is Maybe here a monad? It's not even a constructor of a monad,
// it's a namespace that holds methods that can make different kinds
// of monads.
var Maybe = { Just, Nothing, of: Just };
var identity = v => v;
// we moved the empty check from Maybe into prop()
var isEmpty = v => v == null;
var prop = k => o => isEmpty(o[k]) ? Nothing() : Maybe.of(o[k]);
var myObj = { something: { other: { and: 42 } } };
Maybe.of( myObj )
.chain( prop("something") )
.chain( prop("other") )
.chain( prop("and") )
.chain( identity ); // 42
@glebec
Copy link

glebec commented May 5, 2019

PS the final example of the maybe monad, when I mentioned "bailing out" with Nothing, is very analogous to Promise.then returning a rejected promise.

promiseA
.then(returnsGoodPromise1) // runs
.then(returnsRejectedPromise) // runs
.then(returnsGoodPromise2) // does not run
.then(returnsGoodPromise3) // does not run

maybeA
.chain(returnsJustVal1) // runs
.chain(returnsNothing) // runs
.chain(returnsJustVal2) // does not run
.chain(returnsJustVal3) // does not run

I also wanted to say that I've been focused entirely on the "canonical" Maybe a data type here, but that isn't to say that the "magic" null/undefined chaining thingy isn't possibly useful in a JS context. It just isn't a monad, in the sense that it obeys all the laws we want monads to obey. Those laws are what let us treat our programs like symbolic equations, giving us more power to write and refactor without thinking through implementations.

The flip of that coin is that the thing which makes Maybe monadic is not that it does special things with null/undefined, but that in a sequence of maybe-generating steps, the chain function squishes Just (Just x) -> Just x, Just Nothing -> Nothing, and Nothing (as type Maybe (Maybe x)) to Nothing (as type Maybe x). The upshot of which means that returning a Nothing short-circuits the remaining steps, and returning a Just continues the sequence of computations, and at the end you have a single layer of Just or Nothing. Your return value acts as a signal for whether to continue or not!

Any special handling for null/undefined, e.g. returning a Nothing when you encounter them, is on you as a human developer to opt into. But on the other hand, there are other kinds of Nothing depending on context! So if you have a function which divides numbers… you could say that safeDivide(x, 0) returns Nothing. That isn't related to null or undefined (in JS, 5/0 returns Infinity) but it lets you use the sequencing and explicit case-handling APIs discussed already.

@abiodun0
Copy link

abiodun0 commented May 6, 2019

Thanks for this great explanation @glebec. Spot On!

@glebec
Copy link

glebec commented May 6, 2019

Thanks @abiodun0.

@getify I realized that my very last post – about reasons for Nothing besides null/undefined values - is another fruitful branch of this topic, so here's a smorgasbord of assorted Maybe tricks.

  • various "finders" and list/string-processors which can fail. Advantages: can use the map, chain etc. APIs (leverage ecosystem of Maybe tools for easier composition); can distinguish between "found undefined" vs. "did not find it".
    • minimum([]) === Nothing
    • parseBool('hello') === Nothing, parseBool('truedat') === Just([true, 'dat'])
    • (already mentioned): upgrade findIndex to return maybe values (Nothing instead of -1, Just idx instead of idx)
    • (already mentioned): upgrade find to return maybe values
  • certain arithmetic operations
    • (already mentioned): safeDivide(x, 0) === Nothing (instead of Infinity), safeDivide(x, 2) === Just(x/2)
    • squareRoot (-4) === Nothing (instead of NaN), squareRoot(4) === Just(2)
  • constructive tools, interestingly! Maybe can be used as a signal for whether to construct or not.
    • the mapMaybe :: (a -> Maybe b) -> [a] -> [b] function combines map & filter into a single function, using Maybe as a selection interface:
      • mapMaybe((el) => typeof el === 'string' ? Just(el + '!') : Nothing, [4, 'hi', false, 'yo']) returns ['hi!', 'yo!']
    • the function unfoldr :: (b -> Maybe (a, b)) -> b -> [a] takes an initial seed b and a producer b -> Maybe (a, b) function, and begins constructing a data type from a single value up. It's the opposite of reduce! The Maybe part is used to indicate whether to keep producing (on Just results) or stop (on Nothing).
    • In a tic-tac-toe game, each cell can be a Maybe Mark where Nothing corresponds to no mark and Mark = X | O.
      • as opposed to a custom data type GameSlot = Empty | X | O, the Maybe-wrapped version lets us leverage the existing Maybe API/toolset… this is a common theme.

Those are just some that come to mind, only some of which have any bearing on or relation to null/undefined. In general the big advantage over null/undefined per se is that we are putting both failure and success cases into a wrapper with a specific API, and those wrappers / that API lets you easily compose results and express sequenced operations without dealing directly with the plumbing too much.

For example, in the tic-tac-toe model above, you could write a function flipPlayers easily (assuming your boards are also functors):

function flipPlayers (board) {
  return board.map(cell => { // assuming boards are functors
    return cell.map(mark => { // cells are Maybe Mark values
      return mark === 'X' ? 'O' : 'X' // no need to explicitly think about blank spots – Maybe `map` handles that
    }
  }
}

@harmenjanssen
Copy link

I find this extremely useful and I'm very happy I stumbled upon this.
I've read a lot of functional programming resources, mostly in the context of Javascript, and am still working though http://haskellbook.com/, but this is so well put it filled me up with inspiration and the sense that I get it.

Thanks a lot, @glebec!

@glebec
Copy link

glebec commented May 9, 2019

Happy to hear that @harmenjanssen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment