Skip to content

Instantly share code, notes, and snippets.

@ebiggs
Last active June 20, 2016 04:32
Show Gist options
  • Save ebiggs/4704876 to your computer and use it in GitHub Desktop.
Save ebiggs/4704876 to your computer and use it in GitHub Desktop.
CoffeeScript Maybe Monad - couldn't find one with a Google search so I had to roll my own, hopefully this one showed up for yours ;) Good code coverage in the jasmine spec, so feel confident using it with reckless abandon ;) Maybe.coffee was definitely built with Some(coffea arabica) in my veins ;)
###
Like java, javascript is optimized for throwing null pointer exceptions
coffeescript helps with its series of ? operators
but here is a scala option-eqsue "maybe monad" when such semantics
are preferred over the ? operators or when you want your data
to self-document its indefinite-ness (regular vars don't warn you
that you may get null or undefined!).
note that we're perfectly happy to give you a Some(null) or a Some(undefined)
just like scala. if you have an x that you want to fmap to None if it's
null or undefined then use Maybe(x) which will otherwise produce a Some(x)
Note: scala chose to name fmap 'map' and bind 'flatMap'
I mention that because fmap is not short for flatMap - a possible confusion.
###
maybeProto = {
alert: () -> alert @toString()
log: () -> console.log @toString()
__isMaybe: true
}
sp = someProto = Object.create(maybeProto)
sp.exists = true
sp.isEmpty = false
sp.toString = () -> "Some(#{@__v})"
sp.fmap = (f) -> Maybe(f(@__v))
sp.bind = (f) -> f(@__v)
sp.join = () -> if @__v.__isMaybe? then @__v else throw "already flat!"
sp.getOrElse = sp.get = () -> @__v
sp.toArray = () -> [@__v]
Some = (value) ->
some = Object.create(someProto)
some.__v = value
some
#There's only ever one instance of None!
None = Object.create(maybeProto)
None.exists = false
None.isEmpty = true
None.toString = () -> "None"
None.fmap = None.bind = None.join = () -> None #nice and toxic!
None.get = () -> throw "called get on none!"
None.getOrElse = (x) -> x
None.toArray = () -> []
Maybe = (value) ->
if value? then Some(value) else None
Maybe.empty = None
Maybe.fromArray = (arr) ->
if not (arr instanceof Array) then throw "array expected"
else switch arr.length
when 0 then None
when 1 then Some(arr[0])
else throw "array length must be 0 or 1"
#flattens Array of maybes into maybe an array
#if any of the elements are None then the result will be None
Maybe.flatten = (arrOfMaybes) ->
step = (args, arr) -> switch args.length
when 0 then None
#ugly shifts, but I can promise to only call once!
#I wont let shift hit the fan..
when 1 then args.shift().fmap (x) -> arr.concat [x]
else args.shift().bind (x) -> step(args, arr.concat [x])
step(arrOfMaybes, [])
# usage:
# Maybe.all(aOpt, bOpt, cOpt, dOpt, eOpt)((a, b, c, d, e) ->
# a+b+c+d+e #result
# )
# returns Some(result) if all args are Some(x) else None
Maybe.all = (args...) -> (f) ->
Maybe.flatten(args).fmap (arr) -> f(arr...)
#export
[@None, @Some, @Maybe] = [None, Some, Maybe]
(function(){var e,t,n,r,i,s,o,u=[].slice;r={alert:function(){return alert(this.toString())},log:function(){return console.log(this.toString())},__isMaybe:true};s=i=Object.create(r);s.exists=true;s.isEmpty=false;s.toString=function(){return"Some("+this.__v+")"};s.fmap=function(t){return e(t(this.__v))};s.bind=function(e){return e(this.__v)};s.join=function(){if(this.__v.__isMaybe!=null){return this.__v}else{throw"already flat!"}};s.getOrElse=s.get=function(){return this.__v};s.toArray=function(){return[this.__v]};n=function(e){var t;t=Object.create(i);t.__v=e;return t};t=Object.create(r);t.exists=false;t.isEmpty=true;t.toString=function(){return"None"};t.fmap=t.bind=t.join=function(){return t};t.get=function(){throw"called get on none!"};t.getOrElse=function(e){return e};t.toArray=function(){return[]};e=function(e){if(e!=null){return n(e)}else{return t}};e.empty=t;e.fromArray=function(e){if(!(e instanceof Array)){throw"array expected"}else{switch(e.length){case 0:return t;case 1:return n(e[0]);default:throw"array length must be 0 or 1"}}};e.flatten=function(e){var n;n=function(e,r){switch(e.length){case 0:return t;case 1:return e.shift().fmap(function(e){return r.concat([e])});default:return e.shift().bind(function(t){return n(e,r.concat([t]))})}};return n(e,[])};e.all=function(){var t;t=1<=arguments.length?u.call(arguments,0):[];return function(n){return e.flatten(t).fmap(function(e){return n.apply(null,e)})}};o=[t,n,e],this.None=o[0],this.Some=o[1],this.Maybe=o[2]}).call(this)
describe "Maybe monad", () ->
it "has a None object", () ->
expect(None).not.toBeNull
expect(None).not.toBeUndefined
it "has a Some function that should 'contain' anything, even null and undefined", () ->
expect(Some(1).get()).toBe(1)
expect(Some(null).get()).toBeNull()
expect(Some(undefined).get()).toBeUndefined()
expect(Some().get()).toBeUndefined()
it "has Maybe.fromArray and @.toArray funcs to support the one element array isomorphism", () ->
expect(Maybe.fromArray([])).toBe None
expect(Maybe.fromArray([1]).toString()).toBe("Some(1)")
expect(Maybe().toArray()).toEqual []
expect(Maybe(1).toArray()).toEqual([1])
expect(()->Maybe.fromArray([1, 2])).toThrow()
expect(()->Maybe.fromArray("not": "array")).toThrow()
it "has a Maybe function that should map null and undefined to the None object, and everything else to Some", () ->
expect(Maybe(null)).toBe None
expect(Maybe(undefined)).toBe None
expect(Maybe()).toBe None
expect(Maybe.empty).toBe None
expect(Maybe("hi").get()).toBe("hi")
it "has a monadic bind function to process its contained value", () ->
expect(Some('hello world').bind (x)->x).toBe('hello world')
expect(Some(1).bind (x)->x+4).toBe(5)
it "has a monadic bind function that will always produce a None from a None and not execute its func arg", () ->
expect(None.bind (x)->"supercalifragilistic").toBe None
expect(None.bind (x)->throw "I'm not ever called!").toBe None
expect(() -> None.bind (x)->throw "I'm not ever called!").not.toThrow()
it "has a toString method that surrounds its contained toString in 'Some(...)'", () ->
expect(Some(888).toString()).toBe("Some(888)")
expect(Some("hola mundo").toString()).toBe("Some(hola mundo)")
it "has a monadic fmap function to process new Somes from old Somes", () ->
expect(Some(999).fmap((x)->x+111).toString()).toBe("Some(1110)")
expect(Some(999).fmap((x)->x+111).fmap((x)->x+1).get()).toBe(1111)
it "has a monadic fmap function that will always produce a None from a None and not execute its func arg", () ->
expect(None.fmap (x)->"supercalifragilistic").toBe None
expect(None.fmap (x)->throw "I'm not ever called!").toBe None
expect(() -> None.fmap (x)->throw "y know body never call me?! fuuuuuuuu").not.toThrow()
it "has a monadic join function that will join a Some(Some(x)), throw on Some(x), and (None)->None", () ->
lilSumpinSumpin = Maybe(Maybe(1))
expect(lilSumpinSumpin.toString()).toBe("Some(Some(1))")
expect(lilSumpinSumpin.join().toString()).toBe("Some(1)")
expect(()->lilSumpinSumpin.join().join().toString()).toThrow()
expect(None.join()).toBe(None)
it "has a get method that gets from Some and throws an exception on None", () ->
expect(Maybe(4).get()).toBe(4)
expect(() -> Maybe().get()).toThrow()
it "has a getOrElse method that gets from Some and gets a supplied default from None", () ->
expect(Maybe(4).getOrElse(5)).toBe(4)
expect(Maybe( ).getOrElse(5)).toBe(5)
it "'ll do monadic chaining no problem ;)", () ->
obj = { o: "hi", c: 3, p: "o", f: (x) -> x + 2 }
find = (k) -> Maybe(obj[k])
chain = () -> find('c').bind (see) ->
find('p').bind (pea) -> \
find(pea).bind (ohh) -> \
find('f').bind (fun) ->
"#{pea} #{ohh} #{fun(see)}"
expect(chain()).toBe("o hi 5")
it "will have yield semantics with a trailing fmap", () ->
obj = { o: "hi", c: 3, p: "o", f: (x) -> x + 2 }
find = (k) -> Maybe(obj[k])
chain = () -> find('c').bind (see) ->
find('p').bind (pea) -> \
find(pea).bind (ohh) -> \
find('f').fmap (fun) ->
"#{pea} #{ohh} #{fun(see)}"
expect(chain().toString()).toBe("Some(o hi 5)")
it "will be None if there's at least one None in a chain of Somes", () ->
obj = { o: "hi", c: 3, p: "o", f: (x) -> x + 2 }
find = (k) -> Maybe(obj[k])
chain = () -> find('c').bind (see) ->
find('p').bind (pea) -> \
find('y').bind (why) -> \
find(pea).bind (ohh) -> \
find('f').bind (fun) ->
"#{pea} #{ohh} #{fun(see)}"
expect(chain()).toBe None
it "will flatten an array of Somes with Maybe.flatten([...]) by nesting a bunch of binds", () ->
arr = [Some(1), Some(2), Some(4), Some(8), Some(16), Some(32), Some(64)]
expect(Maybe.flatten(arr).get()).toEqual([1, 2, 4, 8, 16, 32, 64])
it "has a Maybe.flatten that will evaluate to None if any element in an Array of Mabyes is None or the Array is empty", () ->
arr = [Some(1), Some(2), Some(4), None, Some(16), Some(32), Some(64)]
expect(Maybe.flatten(arr)).toBe None
expect(Maybe.flatten([])).toBe None
it "will chain your Maybes together for you with Maybe.all and yield a maybe", () ->
obj = { o: "hi", c: 3, p: "o", f: (x) -> x + 2 }
find = (k) -> Maybe(obj[k])
maybeStr = Maybe.all(find('c'), find('p'), find('o'), find('f')) (see, pea, ohh, fun) ->
"#{pea} #{ohh} #{fun(see)}"
expect(maybeStr.toString()).toBe("Some(o hi 5)")
it "will have None be just as toxic in a Maybe.all", () ->
obj = { o: "hi", c: 3, p: "o", f: (x) -> x + 2 }
find = (k) -> Maybe(obj[k])
maybeStr = Maybe.all(find('c'), find('y'), find('p'), find('o'), find('f')) (see, why, pea, ohh, fun) ->
"#{pea} #{ohh} #{fun(see)}"
expect(maybeStr).toBe None
it "obeys the left identity monad law", () ->
f = (x) -> Maybe(x).fmap((y) -> y.toString() + " obey my laws!!")
g = (x) -> Some(x.toString() + " obey my laws!!")
expect(Maybe(9).bind f).toEqual f(9)
expect(Some(9).bind g).toEqual g(9)
#undefined
expect(Maybe().bind f).toEqual f()
expect(Maybe(null).bind f).toEqual f(null)
f = (x) -> None
expect(Maybe(9).bind f).toEqual f(9)
it "obeys the right identiity monad law", () ->
m = Some(9)
expect(m.bind Some).toEqual m
expect(None.bind Some).toEqual None
it "obeys the associativity monad law", () ->
#f = (x) -> Maybe(x).fmap (y) -> y * 9
#g = (x) -> Maybe(x).fmap((y) -> y.toString() + " obey my laws!!")
f = (x) -> Maybe(x * 9)
g = (x) -> x.toString() + " obey my laws!!"
m = Some(88)
expect(m.bind(f).bind(g)).toEqual m.bind((x)->f(x).bind(g))
expect(None.bind(f).bind(g)).toEqual None.bind((x)->f(x).bind(g))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment