Last active
June 20, 2016 04:32
-
-
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 ;)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### | |
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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