The once-upon-a-time
package aims to bring the approaches to generic programming and software correctness that's been in use for quite a while in major functional languages (monads, functors, etc.).
In this case, we want to have a better alternative to using Null
, for when we may or may not have a value. Haskell and ML models this explicitly with a type they, respectively, call Maybe and Option — the same thing, really, just different names.
var Maybe = require('once-upon-a-time').monads.Maybe
Take the find
function, for example. Find must take in a predicate (a function that takes an element of a collection, and returns whether the element passes the test or not), a collection of things, and return the first thing that passes the test. But we do need to define what the function will return if no element passes the test. Usually, people would use null
, or undefined
, but we can't use either in this case because the collection may contain null values, and we would never be sure whether the returned element passed the test or not!
So, instead, we will wrap the return value in a Maybe monad, so if we found something we can explicitly say: "We found something, and this is the thing". And if we didn't, we can say: "Sorry, nothing passed the test."
//+ find :: [a], (a -> bool) -> Maybe<a>
function find(collection, predicate) {
for (var i = 0; i < collection.length; ++i) {
var item = collection[i]
if (predicate(item)) return Maybe.Just(item)
}
return Maybe.Nothing()
}
The Maybe<a>
type, in this case, is very similar to what ES6 did with generators, except there they've wrapped the return value in a { value: a }
object, and modelled the Nothing case with null
. Thus a generator would still be able to return the value null
, by wrapping it like { value: null }
.
There is some advantages to using monads, however, in that you have a more generic framework, and can reuse all the functions that work on monads in every monad. This is fairly interesting from the abstraction point of view because you can work with less concepts, have a smaller code-base, and all that.
Another advantage of the Maybe<a>
type is that you have to explicitly handle the cases where a value may not have been found, and this completely eliminates the possibility of NullPointerException
or TypeError: undefined is not a function.
:
function asPositive(a){ return +a }
function asNegative(a){ return -a }
// Finds the first binary function.
var binary = find([asPositive, asNegative], function(f) { return f.length === 2 })
Since we're working with Maybe<a>
we can't call the function right away, we need to "unwrap" the value first:
var result = binary.map(function(f){ return f("foo", "bar") })
map
is a method of the functor specification that applies
an operation to the values of a functor, and returns a new functor of the same type.
And yes, Array is a functor! The map
operation there works similarly.
In the Maybe monad, map
will return Nothing without applying the function, if there's
no value, or return Just(f(a))
, if there's an a
value. So, in all the cases, result
is still a Maybe<a>
type. And it still contains the value Nothing
.
If we want a default value, we can use the orElse
method:
var result2 = result.orElse(function(){ return "Bleh" })
Since all monads follow a generic interface, though, you can just plug in a library of generic monad operations, and they will work with every monad. It doesn't matter if you have a Maybe, a Promise, a Validation, a List, or whatever. They'll just work.
For example, if we want to pass the result2
value and a promise of the contents of
a webpage we're asynchronously scrapping from the internet, without all the trouble
of handling things manually, we can just use the lift2
function, which applies a
regular function to the values of two monads:
The lift2
function is provided by fantasy-sorcery:
https://github.com/fantasyland/fantasy-sorcery/blob/master/index.js#L47-L54
pageContents = request('http://www.somewebpage.com')
lift2(result2, pageContents, function(a, b) {
console.log(a + b)
})
Compare this with the usual callback approach:
request('http://www.somewebpage.com', function(err, data) {
if (err) throw err
var binaryOp = find([asNegative, asPositive], function(a){ return a.length === 2 })
if (binaryOp === null) {
var thing = "Bleh"
} else {
var thing = binaryOp("foo", "bar")
}
console.log(thing + data)
})