Created
June 12, 2017 19:38
-
-
Save i-am-tom/71e351dd3c389a052e99324f152571d3 to your computer and use it in GitHub Desktop.
All the example code from the Extend article of Fantas, Eel, and Specification.
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
const daggy = require('daggy') | |
const { uncurryN } = require('wi-jit') | |
Array.prototype.empty = () => [] | |
Sum = daggy.tagged('value') | |
Sum.prototype.concat = function (that) { | |
return Sum(this.value + that.value) | |
} | |
Sum.empty = () => Sum(0) | |
// Our fully-constructed Pair type. | |
const Pair = T => { | |
const Pair_ = daggy.tagged('_1', '_2') | |
Pair_.prototype.map = function (f) { | |
return Pair_(this._1, f(this._2)) | |
} | |
Pair_.prototype.ap = function (fs) { | |
return Pair_(fs._1.concat(this._1), | |
fs._2(this._2)) | |
} | |
Pair_.of = x => Pair_(T.empty(), x) | |
Pair_.prototype.chain = function (f) { | |
const that = f(this._2) | |
return Pair_(this._1.concat(that._1), | |
that._2) | |
} | |
Pair_.prototype.sneakyPeekMap = function (f) { | |
return Pair_(this._1, f(this)) | |
} | |
Pair_.prototype.extend = Pair_.prototype.sneakyPeekMap | |
return Pair_ | |
} | |
//- Sum represents "hunger" | |
const Adventurer = Pair(Sum) | |
//+ type User = { name :: String, isHungry :: Bool } | |
const exampleUser = { | |
name: 'Tom', | |
isHungry: false | |
} | |
// You get the idea... WorkPair again! | |
//+ slayDragon :: User -> Adventurer User | |
const slayDragon = user => | |
Adventurer(Sum(100), user) | |
//+ slayDragon :: User -> Adventurer User | |
const runFromDragon = user => | |
Adventurer(Sum(50), user) | |
//- Eat IF we're hungry | |
//+ eat :: User -> Adventurer User | |
const eat = user => | |
user.isHungry | |
? Adventurer(Sum(-100), Object.assign( | |
{}, user, { isHungry: false } | |
)) | |
: Adventurer(Sum(0), user) | |
//- How could we know when we're hungry? | |
//- This function goes the other way... | |
//+ areWeHungry :: Adventurer User -> User | |
const areWeHungry = ({ _1: { value: hunger }, _2: user}) => | |
hunger > 200 | |
? Object.assign({}, user, { isHungry: true }) | |
: user | |
// Now, we do a thing, check our hunger, | |
// and eat if we need to! | |
// WE ARE SELF-AWARE. | |
// SKYNET | |
console.log( | |
slayDragon(exampleUser) | |
.sneakyPeekMap(areWeHungry).chain(eat) | |
// Adventurer(Sum(100), not hungry) | |
.chain(slayDragon) | |
.sneakyPeekMap(areWeHungry).chain(eat) | |
// Adventurer(Sum(200), not hungry) | |
.chain(runFromDragon) | |
.sneakyPeekMap(areWeHungry).chain(eat) | |
// Adventurer(Sum(150), not hungry)! | |
) | |
//- A root and a list of children! | |
//+ type RoseTree a = (a, [RoseTree a]) | |
const RoseTree = daggy.tagged( | |
'root', 'forest' | |
) | |
RoseTree.prototype.map = function (f) { | |
return RoseTree( | |
f(this.root), // Transform the root... | |
this.forest.map( // and other trees. | |
rt => rt.map(f))) | |
} | |
//+ extend :: RoseTree a | |
//+ ~> (RoseTree a -> b) | |
//+ -> RoseTree b | |
RoseTree.prototype.extend = | |
function (f) { | |
return RoseTree( | |
f(this), | |
this.forest.map(rt => | |
rt.extend(f)) | |
) | |
} | |
const MyTree = RoseTree(10, [ | |
RoseTree(1, [ | |
RoseTree(2, []) | |
]), | |
RoseTree(3, []), | |
RoseTree(4, [ | |
RoseTree(5, [ | |
RoseTree(6, []) | |
]) | |
]), | |
RoseTree(7, []), | |
RoseTree(8, [ | |
RoseTree(9, []), | |
RoseTree(10, []) | |
]) | |
]) | |
console.log( | |
JSON.stringify( | |
// Now, it's super easy to do this! | |
MyTree.extend(({ root, forest }) => | |
forest.length < 1 | |
? Object.assign({}, root, { colour: 'RED' }) | |
: forest.length < 5 | |
? Object.assign({}, root, { colour: 'ORANGE' }) | |
: Object.assign({}, root, { colour: 'GREEN' })) | |
, null, ' ') | |
) | |
const List = daggy.taggedSum({ | |
Cons: ['head', 'tail'], | |
Nil: [] | |
}) | |
const { Cons, Nil } = List | |
//- Usually, if you can write something for | |
//- Array, you can write it for List! | |
List.prototype.map = function (f) { | |
return this.cata({ | |
// Do nothing, we're done! | |
Nil: () => Nil, | |
// Transform head, recurse on tail | |
Cons: (head, tail) => | |
Cons(f(head), tail.map(f)) | |
}) | |
} | |
const arrayToList = xs => { | |
if (!xs.length) return Nil | |
const [ head, ... tail ] = xs | |
return Cons(head, arrayToList(tail)) | |
} | |
List.prototype.toArray = function () { | |
return this.cata({ | |
Cons: (head, tail) => ([ | |
head, ... tail.toArray() | |
]), | |
Nil: () => [] | |
}) | |
} | |
// Starting from today... (in celsius!) | |
const temperatures = arrayToList( | |
[23, 19, 19, 18, 18, 20, 24]) | |
//+ List a ~> (List a -> b) -> List b | |
List.prototype.extend = function (f) { | |
return this.cata({ | |
// Extend the list, repeat on tail. | |
Cons: (head, tail) => Cons( | |
f(this), tail.extend(f) | |
), | |
Nil: () => Nil // We're done! | |
}) | |
} | |
console.log( | |
// [YAY, YAY, YAY, YAY, BOO, BOO, ???] | |
temperatures | |
.extend(({ head, tail }) => | |
tail == Nil | |
? '???' | |
: head < tail.head | |
? 'BOO' : 'YAY') | |
.toArray() | |
) | |
Array.prototype.extend = function (f) { | |
if (this.length === 0) return [] | |
const [ _, ... tail ] = this | |
return [ f(this), ... tail.extend(f) ] | |
} | |
console.log( | |
// Now, we can use array-destructuring :) | |
[23, 19, 19, 18, 18, 20, 24].extend( | |
([head, ... tail]) => | |
tail.length == 0 | |
? '???' | |
: head < tail[0] | |
? 'BOO' : 'YAY') | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment