-
-
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
,chain
etc. APIs (leverage ecosystem of Maybe tools for easier composition); can distinguish between "foundundefined
" 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 ofidx
) - (already mentioned): upgrade
find
to 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 seedb
and a producerb -> Maybe (a, b)
function, and begins constructing a data type from a single value up. It's the opposite ofreduce
! TheMaybe
part is used to indicate whether to keep producing (onJust
results) or stop (onNothing
). - In a tic-tac-toe game, each cell can be a
Maybe Mark
whereNothing
corresponds 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.then
returning a rejected promise.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 withnull
/undefined
, but that in a sequence of maybe-generating steps, thechain
function squishesJust (Just x) -> Just x
,Just Nothing -> Nothing
, andNothing
(as typeMaybe (Maybe x)
) toNothing
(as typeMaybe x
). The upshot of which means that returning aNothing
short-circuits the remaining steps, and returning aJust
continues the sequence of computations, and at the end you have a single layer ofJust
orNothing
. Your return value acts as a signal for whether to continue or not!Any special handling for
null
/undefined
, e.g. returning aNothing
when you encounter them, is on you as a human developer to opt into. But on the other hand, there are other kinds ofNothing
depending on context! So if you have a function which divides numbers… you could say thatsafeDivide(x, 0)
returnsNothing
. That isn't related tonull
orundefined
(in JS,5/0
returnsInfinity
) but it lets you use the sequencing and explicit case-handling APIs discussed already.