Last active
August 22, 2019 14:35
-
-
Save branneman/d0e98a1372bbaba6d93f to your computer and use it in GitHub Desktop.
Simple Algebraic Data Types (ADTs) implementing fantasy-land: Identity, Const, Maybe, Either, IO
This file contains hidden or 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
const fl = require('fantasy-land') | |
const inspect = require('util').inspect.custom | |
// Fantasy Land | |
// of :: Applicative f => a -> f a | |
// map :: Functor f => f a ~> (a -> b) -> f b | |
// ap :: Apply f => f a ~> f (a -> b) -> f b | |
// chain :: Chain m => m a ~> (a -> m b) -> m b | |
/** | |
* Identity | |
*/ | |
class Identity { | |
constructor(x) { | |
this.x = x | |
} | |
static of(x) { | |
return new Identity(x) | |
} | |
[fl.map](f) { | |
return Identity.of(f(this.x)) | |
} | |
[fl.ap](m) { | |
return this[fl.map](m.x) | |
} | |
[fl.chain](f) { | |
return f(this.x) | |
} | |
[inspect]() { | |
return `Identity ${this.x[inspect] ? this.x[inspect]() : this.x}` | |
} | |
} | |
/** | |
* Const | |
*/ | |
class Const { | |
constructor(x) { | |
this.x = x | |
} | |
static of(x) { | |
return new Const(x) | |
} | |
[fl.map](f) { | |
return Const.of(this.x) | |
} | |
[fl.ap](m) { | |
return Const.of(this.x) | |
} | |
[fl.chain](f) { | |
return Const.of(this.x) | |
} | |
[inspect]() { | |
return `Const ${this.x[inspect] ? this.x[inspect]() : this.x}` | |
} | |
} | |
/** | |
* Maybe | |
*/ | |
class Maybe { | |
static of(x) { | |
return x == null ? Nothing.of() : Just.of(x) | |
} | |
} | |
class Nothing { | |
static of() { | |
return new Nothing() | |
} | |
[fl.map](f) { | |
return Nothing.of() | |
} | |
[fl.ap](m) { | |
return Nothing.of() | |
} | |
[fl.chain](f) { | |
return Nothing.of() | |
} | |
[inspect]() { | |
return 'Nothing' | |
} | |
} | |
class Just { | |
constructor(x) { | |
this.x = x | |
} | |
static of(x) { | |
return new Just(x) | |
} | |
[fl.map](f) { | |
return Just.of(f(this.x)) | |
} | |
[fl.ap](m) { | |
return this[fl.map](m.x) | |
} | |
[fl.chain](f) { | |
return f(this.x) | |
} | |
[inspect]() { | |
return `Just ${this.x[inspect] ? this.x[inspect]() : this.x}` | |
} | |
} | |
/** | |
* Either | |
*/ | |
class Either { | |
static of(l, r) { | |
if (arguments.length === 1) { | |
const _Either = r => Either.of(l, r) | |
_Either[inspect] = () => `x → Either ${l[inspect] ? l[inspect]() : l} x` | |
return _Either | |
} | |
return r == null ? Left.of(l) : Right.of(r) | |
} | |
} | |
class Left { | |
constructor(x) { | |
this.x = x | |
} | |
static of(x) { | |
return new Left(x) | |
} | |
[fl.map](f) { | |
return Left.of(this.x) | |
} | |
[fl.ap](m) { | |
return Left.of(this.x) | |
} | |
[fl.chain](f) { | |
return Left.of(this.x) | |
} | |
[inspect]() { | |
return `Left ${this.x[inspect] ? this.x[inspect]() : this.x}` | |
} | |
} | |
class Right { | |
constructor(x) { | |
this.x = x | |
} | |
static of(x) { | |
return new Right(x) | |
} | |
[fl.map](f) { | |
return Right.of(f(this.x)) | |
} | |
[fl.ap](m) { | |
return this[fl.map](m.x) | |
} | |
[fl.chain](f) { | |
return f(this.x) | |
} | |
[inspect]() { | |
return `Right ${this.x[inspect] ? this.x[inspect]() : this.x}` | |
} | |
} | |
/** | |
* IO | |
*/ | |
class IO { | |
constructor(x) { | |
this.x = x | |
} | |
static of(x) { | |
return new IO(x) | |
} | |
run(...args) { | |
return this.x(...args) | |
} | |
[fl.map](f) { | |
return IO.of(h => f(this.x(h))) | |
} | |
[fl.ap](m) { | |
return this[fl.map](m.run()) | |
} | |
[fl.chain](f) { | |
return IO.of(h => f(this.x(h)).run()) | |
} | |
[inspect]() { | |
return `IO${this.x[inspect] ? ' ' + this.x[inspect]() : ''}` | |
} | |
} |
This file contains hidden or 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
const { map, ap, chain, add } = require('ramda') | |
// Identity | |
const fn1 = x => Identity.of(add(1, x)) | |
Identity.of(2) //=> Identity 2 | |
map(add(1), Identity.of(2)) //=> Identity 3 | |
ap(Identity.of(add(4)), Identity.of(1)) //=> Identity 5 | |
chain(fn1, Identity.of(6)) //=> Identity 7 | |
// Const | |
const fn2 = x => Const.of(add(2, x)) | |
Const.of(11) //=> Const 11 | |
map(add(4), Const.of(13)) //=> Const 13 | |
ap(Const.of(add(5)), Const.of(17)) //=> Const 17 | |
chain(fn2, Const.of(19)) //=> Const 19 | |
// Maybe | |
const fn3 = x => Maybe.of(add(1, x)) | |
Maybe.of(23) //=> Just 23 | |
map(add(1), Maybe.of(28)) //=> Just 29 | |
map(add(1), Maybe.of(null)) //=> Nothing | |
ap(Maybe.of(add(5)), Maybe.of(26)) //=> Just 31 | |
ap(Maybe.of(add(5)), Maybe.of(null)) //=> Nothing | |
chain(fn3, Maybe.of(36)) //=> Just 37 | |
chain(fn3, Maybe.of(null)) //=> Nothing | |
// Maybe: Nothing | |
const fn4 = x => Maybe.of(add(1, x)) | |
Nothing.of() //=> Nothing | |
map(add(1), Nothing.of()) //=> Nothing | |
ap(Maybe.of(add(4)), Nothing.of()) //=> Nothing | |
chain(fn4, Nothing.of()) //=> Nothing | |
// Maybe: Just | |
const fn5 = x => Just.of(add(1, x)) | |
Just.of(41) //=> Just 41 | |
map(add(1), Just.of(42)) //=> Just 43 | |
ap(Just.of(add(40)), Just.of(7)) //=> Just 47 | |
chain(fn5, Just.of(48)) //=> Just 49 | |
// Either | |
Either.of('err', 59) //=> Right 59 | |
Either.of('err', null) //=> Left err | |
map(add(1), Either.of('err', 60)) //=> Right 61 | |
map(add(1), Either.of('err', null)) //=> Left err | |
// Either: Left | |
const fn6 = x => Right.of(add(2, x)) | |
Left.of(67) //=> Left 67 | |
map(add(4), Left.of(71)) //=> Left 71 | |
ap(Left.of(add(5)), Left.of(73)) //=> Left 73 | |
chain(fn6, Left.of(79)) //=> Left 79 | |
// Either: Right | |
const fn7 = x => Right.of(add(1, x)) | |
Right.of(83) //=> Right 83 | |
map(add(1), Right.of(88)) //=> Right 89 | |
ap(Right.of(add(90)), Right.of(7)) //=> Right 97 | |
chain(fn7, Right.of(100)) //=> Right 101 | |
// IO | |
const fn8 = IO.of(() => add(2)) | |
const fn9 = x => IO.of(() => add(100, x)) | |
IO.of(() => 83) //=> IO | |
IO.of(() => 103).run() //=> 103 | |
map(add(3), IO.of(() => 104)) //=> IO | |
map(add(3), IO.of(() => 104)).run() //=> 107 | |
ap(fn8, IO.of(() => 107)) //=> IO | |
ap(fn8, IO.of(() => 107)).run() //=> 109 | |
chain(fn9, IO.of(() => 13)) //=> IO | |
chain(fn9, IO.of(() => 13)).run() //=> 113 |
@DrBoolean Thanks for a great response!
I've updated my implementation, now supporting the actual use case:
R.map(R.add(1), Either('err', 3)); //=> Right(4)
R.map(R.add(1), Either('err', null)); //=> Left('err')
R.map(R.add(1), Right(5)); //=> Right(6)
R.map(R.add(1), Left('err')); //=> Left('err')
Wonderful!
i added a wrinkle to Either
, which is to make the "constructor" curried. See: https://github.com/ramda/ramda-fantasy/blob/master/src/Either.js
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is great!
I have to mention Either is a little off (I had this wrong in my implementations as some point and in the underscore video). The correct behavior acts like a "throw" in a pure functional app - it should always ignore the Left side (like maybe(null)):
Here's another quick example using @folktale https://gist.github.com/DrBoolean/8357229