-
-
Save getify/2dc45c9a82cfd93358fbffd21bdd601d to your computer and use it in GitHub Desktop.
| // 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 |
Thanks for this great explanation @glebec. Spot On!
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,chainetc. APIs (leverage ecosystem of Maybe tools for easier composition); can distinguish between "foundundefined" vs. "did not find it".minimum([]) === NothingparseBool('hello') === Nothing,parseBool('truedat') === Just([true, 'dat'])- (already mentioned): upgrade
findIndexto return maybe values (Nothinginstead of-1,Just idxinstead ofidx) - (already mentioned): upgrade
findto return maybe values
- certain arithmetic operations
- (already mentioned):
safeDivide(x, 0) === Nothing(instead ofInfinity),safeDivide(x, 2) === Just(x/2) squareRoot (-4) === Nothing(instead ofNaN),squareRoot(4) === Just(2)
- (already mentioned):
- 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 seedband a producerb -> Maybe (a, b)function, and begins constructing a data type from a single value up. It's the opposite ofreduce! TheMaybepart is used to indicate whether to keep producing (onJustresults) or stop (onNothing). - In a tic-tac-toe game, each cell can be a
Maybe MarkwhereNothingcorresponds to no mark andMark = 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.
- as opposed to a custom data type
- the
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
}
}
}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!
Happy to hear that @harmenjanssen.
PS the final example of the maybe monad, when I mentioned "bailing out" with
Nothing, is very analogous toPromise.thenreturning a rejected promise.I also wanted to say that I've been focused entirely on the "canonical"
Maybe adata 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
Maybemonadic is not that it does special things withnull/undefined, but that in a sequence of maybe-generating steps, thechainfunction squishesJust (Just x) -> Just x,Just Nothing -> Nothing, andNothing(as typeMaybe (Maybe x)) toNothing(as typeMaybe x). The upshot of which means that returning aNothingshort-circuits the remaining steps, and returning aJustcontinues the sequence of computations, and at the end you have a single layer ofJustorNothing. Your return value acts as a signal for whether to continue or not!Any special handling for
null/undefined, e.g. returning aNothingwhen you encounter them, is on you as a human developer to opt into. But on the other hand, there are other kinds ofNothingdepending on context! So if you have a function which divides numbers… you could say thatsafeDivide(x, 0)returnsNothing. That isn't related tonullorundefined(in JS,5/0returnsInfinity) but it lets you use the sequencing and explicit case-handling APIs discussed already.