Skip to content

Instantly share code, notes, and snippets.

@branneman
Last active August 22, 2019 14:35
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save branneman/d0e98a1372bbaba6d93f to your computer and use it in GitHub Desktop.
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
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]() : ''}`
}
}
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
Copy link

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)):

//+ appUrl :: {host: String, port: Number} -> String
var appUrl = function(cfg) {
  if(!cfg.host || !cfg.port) return Left("missing host or port");
  return Right(cfg.host + ":" + cfg.port);
}

//+ startApp :: {host: String, port: Number} -> App
var startApp = compose(map(server.listen), appUrl)

startApp({host: "dotcom.com", port: 8080})
//=> Right(App)

startApp({})
//=> Left("missing host or port")

Here's another quick example using @folktale https://gist.github.com/DrBoolean/8357229

@branneman
Copy link
Author

@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')

@DrBoolean
Copy link

Wonderful!

@buzzdecafe
Copy link

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