├── .gitignore ├── README.md ├── examples ├── coyoneda-fusion.js ├── interpreter-coproduct.js ├── interpreter-forth.js ├── interpreter-http.js ├── interpreter-kvs.js ├── interpreter-terminal.js ├── interpreter-turtle.js ├── interpreter-tweet.js ├── parser.js └── trampoline.js ├── fantasy-frees.js ├── package.json ├── src ├── coyoneda.js ├── free.js ├── freeap.js ├── trampoline.js └── yoneda.js └── test ├── coyoneda.js ├── free.js ├── freeap.js ├── lib └── test.js └── yoneda.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fantasy Frees 2 | 3 | ![](https://raw.github.com/puffnfresh/fantasy-land/master/logo.png) 4 | 5 | ## General 6 | 7 | Free yourself in the land of fantasies. 8 | 9 | ### Interpreters 10 | 11 | See [examples](examples) for now. 12 | 13 | ## Testing 14 | 15 | ### Library 16 | 17 | Fantasy Options uses [nodeunit](https://github.com/caolan/nodeunit) for 18 | all the tests and because of this there is currently an existing 19 | [adapter](test/lib/test.js) in the library to help with integration 20 | between nodeunit and Fantasy Check. 21 | 22 | ### Coverage 23 | 24 | Currently Fantasy Check is using [Istanbul](https://github.com/gotwarlost/istanbul) 25 | for code coverage analysis; you can run the coverage via the following 26 | command: 27 | 28 | _This assumes that you have istanbul installed correctly._ 29 | 30 | ``` 31 | istanbul cover nodeunit -- test/*.js 32 | ``` 33 | -------------------------------------------------------------------------------- /examples/coyoneda-fusion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {Coyoneda} = require('./../fantasy-frees'); 4 | 5 | function add(x) { 6 | return x + 1; 7 | } 8 | 9 | function toString(x) { 10 | return '' + x; 11 | } 12 | 13 | const arr = [1, 2, 3, 4, 6, 7, 8, 9]; 14 | const coyo = Coyoneda.lift(arr); 15 | const res = coyo.map(add).map(toString); 16 | 17 | console.log(res.lower()); 18 | -------------------------------------------------------------------------------- /examples/interpreter-coproduct.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const daggy = require('daggy'); 4 | 5 | const {compose, identity} = require('fantasy-combinators'); 6 | const {Free} = require('./../fantasy-frees'); 7 | const Coproduct = require('fantasy-coproducts'); 8 | const IO = require('fantasy-io'); 9 | 10 | 11 | const FPrint = daggy.tagged('s', 'a'); 12 | const FRead = daggy.tagged('f'); 13 | const unit = daggy.tagged('x'); 14 | const Unit = () => unit(''); 15 | 16 | const Logger = daggy.taggedSum({ 17 | Error: ['x', 'a'], 18 | Debug: ['x', 'a'] 19 | }); 20 | 21 | FPrint.prototype.map = function(f) { 22 | return FPrint(this.s, f(this.a)); 23 | }; 24 | 25 | FRead.prototype.map = function(f) { 26 | return FRead(compose(f)(this.f)); 27 | }; 28 | 29 | Logger.prototype.map = function(f) { 30 | return this.cata({ 31 | Error: (a, b) => Logger.Error(a, f(b)), 32 | Debug: (a, b) => Logger.Debug(a, f(b)) 33 | }); 34 | }; 35 | 36 | function fprint(s) { 37 | return FPrint(s, Unit()); 38 | } 39 | 40 | function fread() { 41 | return FRead(identity); 42 | } 43 | 44 | function debug(x) { 45 | return Logger.Debug(x, Unit()); 46 | } 47 | 48 | function error(x) { 49 | return Logger.Error(x, Unit()); 50 | } 51 | 52 | function left(x) { 53 | return Coproduct.left(x); 54 | } 55 | 56 | function right(x) { 57 | return Coproduct.right(x); 58 | } 59 | 60 | function liftLeft(x) { 61 | return Free.liftF(left(x)); 62 | } 63 | 64 | function liftRight(x) { 65 | return Free.liftF(right(x)); 66 | } 67 | 68 | const readPrint = liftLeft(fprint("Hello, name?")).chain((_) => { 69 | return liftRight(left(fread())).chain((name) => { 70 | return liftRight(right(debug(name))).chain((_) => { 71 | return liftLeft(fprint("Hi " + name + "!")); 72 | }); 73 | }); 74 | }); 75 | 76 | function runIO(free) { 77 | return free.resume().fold( 78 | (x) => { 79 | return x.coproduct( 80 | (print) => { 81 | console.log(print.s); 82 | return runIO(print.a); 83 | }, 84 | (y) => { 85 | return y.coproduct( 86 | (read) => { 87 | return runIO(read.f("Timmy")) 88 | }, 89 | (log) => { 90 | log.cata({ 91 | Error: (x) => console.log('Error', x), 92 | Debug: (x) => console.log('Debug', x) 93 | }); 94 | return runIO(log.a); 95 | } 96 | ); 97 | } 98 | ); 99 | }, 100 | IO.of 101 | ); 102 | }; 103 | 104 | runIO(readPrint).unsafePerform(); 105 | -------------------------------------------------------------------------------- /examples/interpreter-forth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const daggy = require('daggy'); 4 | 5 | const {compose, constant, identity} = require('fantasy-combinators'); 6 | const {Free} = require('./../fantasy-frees'); 7 | const {Tuple2} = require('fantasy-tuples'); 8 | const {Lens} = require('fantasy-lenses'); 9 | const State = require('fantasy-states'); 10 | 11 | const Forth = daggy.taggedSum({ 12 | Push: ['a', 'next'], 13 | Add: ['next'], 14 | Mul: ['next'], 15 | Dup: ['next'], 16 | End: ['next'] 17 | }); 18 | const unit = daggy.tagged('x'); 19 | const Unit = () => unit(''); 20 | 21 | Forth.prototype.toString = function() { 22 | const named = (name) => (x) => 'Forth.' + name + '(' + x + ')'; 23 | return this.cata({ 24 | Push: (x, y) =>'Forth.Push(' + x + ', ' + y.toString() + ')', 25 | Add: named('Add'), 26 | Mul: named('Mul'), 27 | Dup: named('Dup'), 28 | End: named('End'), 29 | }); 30 | }; 31 | 32 | function push(val) { 33 | return Free.liftFC(Forth.Push(val, Unit())); 34 | } 35 | 36 | function add() { 37 | return Free.liftFC(Forth.Add(Unit())); 38 | } 39 | 40 | function mul() { 41 | return Free.liftFC(Forth.Mul(Unit())); 42 | } 43 | 44 | function dup() { 45 | return Free.liftFC(Forth.Dup(Unit())); 46 | } 47 | 48 | function end() { 49 | return Free.liftFC(Forth.End(Unit())); 50 | } 51 | 52 | const interpreters = { 53 | pure : (program) => { 54 | return program.cata({ 55 | Push: (value, next) => { 56 | return State((stack) => Tuple2(next, [value].concat(stack))); 57 | }, 58 | Add: (next) => { 59 | return State((stack) => { 60 | const x = stack[0]; 61 | const y = stack[1]; 62 | return Tuple2(next, [x + y].concat(stack.slice(2))); 63 | }); 64 | }, 65 | Mul: (next) => { 66 | return State((stack) => { 67 | const x = stack[0]; 68 | const y = stack[1]; 69 | return Tuple2(next, [x * y].concat(stack.slice(2))); 70 | }); 71 | }, 72 | Dup: (next) => { 73 | return State((stack) => { 74 | const x = stack[0]; 75 | return Tuple2(next, [x].concat(stack)); 76 | }); 77 | }, 78 | End: (next) => { 79 | return State((stack) => Tuple2(next, stack)); 80 | } 81 | }); 82 | } 83 | }; 84 | 85 | const script = push(3) 86 | .andThen(push(6)) 87 | .andThen(add()) 88 | .andThen(push(7)) 89 | .andThen(push(2)) 90 | .andThen(add()) 91 | .andThen(mul()) 92 | .andThen(dup()) 93 | .andThen(add()) 94 | .andThen(end()) 95 | 96 | console.log('--------------------------------------------'); 97 | console.log(Free.runFC(script, interpreters.pure, State).exec([])); 98 | console.log('--------------------------------------------'); 99 | -------------------------------------------------------------------------------- /examples/interpreter-http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const daggy = require('daggy'); 4 | 5 | const {compose, constant, identity} = require('fantasy-combinators'); 6 | const {Free} = require('./../fantasy-frees'); 7 | const {Lens} = require('fantasy-lenses'); 8 | 9 | const Response = daggy.tagged('headers', 'statusCode', 'body'); 10 | const Responses = daggy.taggedSum({ 11 | AddHeader : ['header', 'value', 'next'], 12 | RemoveHeader: ['header', 'next'], 13 | StatusCode : ['code', 'next'], 14 | Body : ['body', 'next'] 15 | }); 16 | 17 | const lenses = { 18 | headers : Lens.objectLens('headers'), 19 | statusCode: Lens.objectLens('statusCode'), 20 | body : Lens.objectLens('body'), 21 | }; 22 | const httpStatus = { 23 | ok : compose(statusCode)(constant(200)), 24 | created : compose(statusCode)(constant(201)), 25 | notFound : compose(statusCode)(constant(404)), 26 | internalError: compose(statusCode)(constant(500)) 27 | }; 28 | 29 | const unit = daggy.tagged('x'); 30 | const Unit = () => unit(''); 31 | 32 | Responses.prototype.map = function(f) { 33 | function go(p) { 34 | return (x, n) => p(x, f(n)); 35 | } 36 | return this.cata({ 37 | AddHeader: (k, v, n) => Responses.AddHeader(k, v, f(n)), 38 | RemoveHeader: go(Responses.RemoveHeader), 39 | StatusCode: go(Responses.StatusCode), 40 | Body: go(Responses.Body) 41 | }); 42 | }; 43 | 44 | function response() { 45 | return Response({}, 0, ''); 46 | } 47 | 48 | function addHeader(header, value) { 49 | return Free.liftF(Responses.AddHeader(header, value, Unit())); 50 | } 51 | 52 | function removeHeader(header) { 53 | return Free.liftF(Responses.RemoveHeader(header, Unit())); 54 | } 55 | 56 | function statusCode(code) { 57 | return Free.liftF(Responses.StatusCode(code, Unit())); 58 | } 59 | 60 | function body(body) { 61 | return Free.liftF(Responses.Body(body, Unit())); 62 | } 63 | 64 | function replaceHeader(header, value) { 65 | return removeHeader(header).chain((x) => addHeader(header, value)); 66 | } 67 | 68 | function json(value) { 69 | return replaceHeader('Content-Type', 'application/json').chain((x) => { 70 | return body(JSON.stringify(value)); 71 | }); 72 | } 73 | 74 | function dissoc(l, n, o) { 75 | // This is wrong atm. 76 | const x = l.run(o).map((x) => { 77 | delete x.headers[n]; 78 | return x; 79 | }); 80 | return x.extract(); 81 | } 82 | 83 | const interpreters = { 84 | pure: (free, res) => { 85 | return free.resume().fold( 86 | (x) => { 87 | return x.cata({ 88 | AddHeader: (header, value, n) => { 89 | var h = lenses.headers.andThen(Lens.objectLens(header)); 90 | return interpreters.pure(n, h.run(res).set(value)); 91 | }, 92 | RemoveHeader: (header, n) => { 93 | var h = lenses.headers.andThen(Lens.objectLens(header)); 94 | return interpreters.pure(n, dissoc(h, header, res)); 95 | }, 96 | StatusCode: (code, n) => { 97 | return interpreters.pure(n, lenses.statusCode.run(res).set(code)); 98 | }, 99 | Body: (body, n) => { 100 | return interpreters.pure(n, lenses.body.run(res).set(body)); 101 | } 102 | }); 103 | }, 104 | constant(res) 105 | ); 106 | } 107 | }; 108 | 109 | const res = response(); 110 | const script = addHeader('Content-Type', 'text/plain') 111 | .andThen(httpStatus.ok()) 112 | .andThen(json({text:'Hello World!'})); 113 | 114 | console.log('----------------------------'); 115 | console.log(interpreters.pure(script, res)); 116 | console.log('----------------------------'); -------------------------------------------------------------------------------- /examples/interpreter-kvs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const daggy = require('daggy'); 4 | 5 | const {compose, constant, identity} = require('fantasy-combinators'); 6 | const {Free} = require('./../fantasy-frees'); 7 | const {Lens} = require('fantasy-lenses'); 8 | 9 | const KVS = daggy.taggedSum({ 10 | Put: ['key', 'val', 'next'], 11 | Del: ['key', 'next'], 12 | Get: ['key', 'val'] 13 | }); 14 | 15 | const unit = daggy.tagged('x'); 16 | const Unit = () => unit(''); 17 | 18 | KVS.prototype.map = function(f) { 19 | return this.cata({ 20 | Put: (k, v, n) => KVS.Put(k, v, f(n)), 21 | Del: (k, n) => KVS.Del(k, f(n)), 22 | Get: (k, v) => KVS.Get(k, compose(f)(v)) 23 | }); 24 | }; 25 | 26 | function put(key, val) { 27 | return Free.liftF(KVS.Put(key, val, Unit())); 28 | } 29 | 30 | function del(key) { 31 | return Free.liftF(KVS.Del(key, Unit())); 32 | } 33 | 34 | function get(key) { 35 | return Free.liftF(KVS.Get(key, identity)); 36 | } 37 | 38 | function modify(key, f) { 39 | return get(key).chain((v) => put(key, f(v))); 40 | } 41 | 42 | function assoc(k, v, o) { 43 | return Lens.objectLens(k).run(o).set(v); 44 | } 45 | 46 | function dissoc(k, o) { 47 | var result = {}; 48 | for (var p in o) { 49 | if (p !== k) { 50 | result[p] = o[p]; 51 | } 52 | } 53 | return result; 54 | } 55 | 56 | function add(x) { 57 | return (y) => x + y; 58 | } 59 | 60 | const interpreters = { 61 | string: (free, store) => { 62 | function go(store, free, strs) { 63 | return free.resume().fold( 64 | (x) => { 65 | return x.cata({ 66 | Put: (k, v, n) => { 67 | return go(assoc(k, v, store), n, strs.concat('Putting ' + k + ' with ' + v)); 68 | }, 69 | Del: (k, n) => { 70 | return go(dissoc(k, store), n, strs.concat('Deleting ' + k)); 71 | }, 72 | Get: (k, f) => { 73 | return go(store, f(store[k]), strs.concat('Getting ' + k)); 74 | } 75 | }) 76 | }, 77 | constant(strs.join('\n')) 78 | ) 79 | } 80 | 81 | return go(store, free, []); 82 | }, 83 | pure: (free, store) => { 84 | return free.resume().fold( 85 | (x) => { 86 | return x.cata({ 87 | Put: (k, v, n) => interpreters.pure(n, assoc(k, v, store)), 88 | Del: (k, n) => interpreters.pure(n, dissoc(k, store)), 89 | Get: (k, f) => interpreters.pure(f(store[k]), store) 90 | }); 91 | }, 92 | constant(store) 93 | ); 94 | } 95 | }; 96 | 97 | 98 | const script = get('swiss bank account id').chain((id) => { 99 | return modify(id, add(1000)) 100 | .andThen(put('bermuda airport', 'getaway car')) 101 | .andThen(del('tax records')); 102 | }); 103 | 104 | const store = { 105 | 'swiss bank account id': 'xxxx1', 106 | 'xxxx1': 5032.12, 107 | 'tax records': [ 108 | {'date': '2/20/15', 'subject': 'aaaaa', 'amount': 2322.90 }, 109 | {'date': '2/20/15', 'subject': 'bbbbb', 'amount': 3412.90 }, 110 | {'date': '2/20/15', 'subject': 'ccccc', 'amount': 4502.90 }, 111 | {'date': '2/20/15', 'subject': 'ddddd', 'amount': 5692.90 } 112 | ] 113 | }; 114 | 115 | console.log('--------------------------------'); 116 | console.log(interpreters.string(script, store)); 117 | console.log('--------------------------------'); 118 | console.log(interpreters.pure(script, store)); 119 | console.log('--------------------------------'); 120 | -------------------------------------------------------------------------------- /examples/interpreter-terminal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const daggy = require('daggy'); 4 | const readin = require('readline-sync'); 5 | 6 | const {compose, constant, identity} = require('fantasy-combinators'); 7 | const {Free} = require('./../fantasy-frees'); 8 | const {Tuple2} = require('fantasy-tuples'); 9 | const {Lens} = require('fantasy-lenses'); 10 | const State = require('fantasy-states'); 11 | 12 | const Mock = daggy.tagged('in', 'out'); 13 | const Real = daggy.tagged('out'); 14 | const TerminalOp = daggy.taggedSum({ 15 | ReadLine: [], 16 | WriteLine: ['a'] 17 | }); 18 | 19 | Mock.prototype.read = function() { 20 | return Tuple2( 21 | this.in.length < 1 ? "" : this.in.concat().shift(), 22 | Mock(this.in.slice(1), this.out) 23 | ); 24 | }; 25 | 26 | Mock.prototype.write = function(x) { 27 | return Mock(this.in, x); 28 | }; 29 | 30 | Real.prototype.read = function() { 31 | const result = readin.question(''); 32 | return Tuple2( 33 | result, 34 | Real(this.out) 35 | ); 36 | }; 37 | 38 | Real.prototype.write = function(x) { 39 | return Real(x); 40 | }; 41 | 42 | TerminalOp.prototype.toString = function() { 43 | return this.cata({ 44 | ReadLine: () => 'ReadLine()', 45 | WriteLine: (x) => 'WriteLine(' + x + ')' 46 | }); 47 | }; 48 | 49 | function readLine() { 50 | return Free.liftFC(TerminalOp.ReadLine); 51 | } 52 | 53 | function writeLine(x) { 54 | return Free.liftFC(TerminalOp.WriteLine(x)); 55 | } 56 | 57 | const interpreters = { 58 | pure : (program) => { 59 | return program.cata({ 60 | ReadLine: () => { 61 | return State((env) => env.read()); 62 | }, 63 | WriteLine: (value) => { 64 | return State.modify((env) => env.write(value)); 65 | } 66 | }); 67 | } 68 | }; 69 | 70 | const script = readLine().chain((x) => { 71 | return readLine().chain((y) => { 72 | return writeLine(x + " " + y); 73 | }); 74 | }); 75 | const mock = Mock(["Hello", "World"], []); 76 | const real = Real([]); 77 | 78 | console.log('--------------------------------------------'); 79 | console.log(Free.runFC(script, interpreters.pure, State).exec(mock).out); 80 | console.log(Free.runFC(script, interpreters.pure, State).exec(real).out); 81 | console.log('--------------------------------------------'); 82 | -------------------------------------------------------------------------------- /examples/interpreter-turtle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const daggy = require('daggy'); 4 | 5 | const {compose, constant, identity} = require('fantasy-combinators'); 6 | const {Free} = require('./../fantasy-frees'); 7 | const {Lens} = require('fantasy-lenses'); 8 | const Writer = require('fantasy-writers'); 9 | 10 | const Turtle = daggy.taggedSum({ 11 | Forward : ['x', 'next'], 12 | Backward : ['x', 'next'], 13 | Left : ['x', 'next'], 14 | Right : ['x', 'next'], 15 | LineColor: ['x', 'next'], 16 | Clear : ['next'] 17 | }); 18 | 19 | const unit = daggy.tagged('x'); 20 | const Unit = () => unit(''); 21 | 22 | Turtle.prototype.map = function(f) { 23 | function go(c) { 24 | return (x, n) => c(x, f(n)); 25 | } 26 | return this.cata({ 27 | Forward : go(Turtle.Forward), 28 | Backward : go(Turtle.Backward), 29 | Left : go(Turtle.Left), 30 | Right : go(Turtle.Right), 31 | LineColor: go(Turtle.LineColor), 32 | Clear: (n) => Turtle.Clear(f(n)) 33 | }); 34 | }; 35 | 36 | function forward(x) { 37 | return Free.liftF(Turtle.Forward(x, Unit())); 38 | } 39 | 40 | function backward(x) { 41 | return Free.liftF(Turtle.Backward(x, Unit())); 42 | } 43 | 44 | function left(x) { 45 | return Free.liftF(Turtle.Left(x, Unit())); 46 | } 47 | 48 | function right(x) { 49 | return Free.liftF(Turtle.Right(x, Unit())); 50 | } 51 | 52 | function lineColor(x) { 53 | return Free.liftF(Turtle.LineColor(x, Unit())); 54 | } 55 | 56 | function clear() { 57 | return Free.liftF(Turtle.Clear(Unit())); 58 | } 59 | 60 | const interpreters = { 61 | string: (free) => { 62 | function go(free, writer) { 63 | return free.resume().fold( 64 | (x) => { 65 | return x.cata({ 66 | Forward: (x, n) => go(n, writer.tell(['Forward: ' + x])), 67 | Backward: (x, n) => go(n, writer.tell(['Backward: ' + x])), 68 | Left: (x, n) => go(n, writer.tell(['Left: ' + x])), 69 | Right: (x, n) => go(n, writer.tell(['Right: ' + x])), 70 | LineColor: (x, n) => go(n, writer.tell(['LineColor: ' + x])), 71 | Clear: (n) => go(n, writer.tell(['Clear'])) 72 | }) 73 | }, 74 | constant(writer) 75 | ) 76 | } 77 | 78 | return go(free, Writer.of([])); 79 | }, 80 | svg: function(free) { 81 | function copy(x) { 82 | const y = {}; 83 | for(var i in x) { 84 | y[i] = x[i]; 85 | } 86 | return y; 87 | } 88 | function extend(a, b) { 89 | const x = copy(a); 90 | for(var i in b) { 91 | x[i] = b[i]; 92 | } 93 | return x; 94 | } 95 | function open(name, attr, close) { 96 | const m = Object.keys(attr).map((x) => x + '="' + attr[x] + '"'); 97 | return '<' + name + ' ' + m.join(' ') + (close ? '/>' : '>'); 98 | } 99 | function close(name) { 100 | return ''; 101 | } 102 | function dir(s, a) { 103 | const r = s.dir * (Math.PI / 180); 104 | const x = s.x2 + a * Math.cos(r); 105 | const y = s.y2 + a * Math.sin(r); 106 | return extend(s, { x1: s.x2 107 | , y1: s.y2 108 | , x2: x 109 | , y2: y 110 | }); 111 | } 112 | function go(free, store, writer) { 113 | return free.resume().fold( 114 | (x) => { 115 | return x.cata({ 116 | Forward: (x, n) => { 117 | const s = extend(store, dir(store, x)); 118 | return go(n, s, writer.tell([open('line', s, true)])); 119 | }, 120 | Backward: (x, n) => { 121 | const s = extend(store, dir(store, -x)); 122 | return go(n, s, writer.tell([open('line', s, true)])); 123 | }, 124 | Left: (x, n) => { 125 | const s = extend(store, { dir: store.dir-x }); 126 | return go(n, s, writer); 127 | }, 128 | Right: (x, n) => { 129 | const s = extend(store, { dir: store.dir+x }); 130 | return go(n, s, writer); 131 | }, 132 | LineColor: (x, n) => { 133 | const s = extend(store, { 134 | style: 'stroke:' + x + ';stroke-width:2' 135 | }); 136 | return go(n, s, writer); 137 | }, 138 | Clear: (n) => { 139 | return go(n, store, writer); 140 | } 141 | }) 142 | }, 143 | constant(writer.tell([close('svg')])) 144 | ) 145 | } 146 | 147 | const attrs = {width:200, height:200, xmlns:"http://www.w3.org/2000/svg", version:"1.1"}; 148 | const out = Writer.of([]).tell([open('svg', attrs, false)]); 149 | return go(free, {x1:0,y1:0,x2:100,y2:100, dir:0}, out); 150 | } 151 | }; 152 | 153 | function repeat(x, y) { 154 | function go(a, b) { 155 | return b == 0 ? a : go(a.andThen(x), b - 1); 156 | } 157 | return go(x, y); 158 | } 159 | 160 | const script = clear(). 161 | andThen(lineColor("rgb(38,38,38)")). 162 | andThen(repeat(forward(100).andThen(right(144)), 4)); 163 | 164 | 165 | console.log('---------------------------------------------'); 166 | console.log(interpreters.string(script).run()._2.join('\n')); 167 | console.log('---------------------------------------------'); 168 | console.log(interpreters.svg(script).run()._2.join('\n')); 169 | console.log('---------------------------------------------'); 170 | -------------------------------------------------------------------------------- /examples/interpreter-tweet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const daggy = require('daggy'); 4 | 5 | const {identity} = require('fantasy-combinators'); 6 | const {Free} = require('./../fantasy-frees'); 7 | const {singleton} = require('fantasy-helpers'); 8 | 9 | const Cofree = require('fantasy-cofrees'); 10 | const Identity = require('fantasy-identities'); 11 | const Option = require('fantasy-options'); 12 | 13 | const Tweet = daggy.tagged('id', 'str'); 14 | const User = daggy.tagged('id', 'name', 'photo'); 15 | 16 | const Service = daggy.taggedSum({ 17 | GetTweets : ['id'], 18 | GetUserName : ['id'], 19 | GetUserPhoto : ['id'] 20 | }); 21 | 22 | const Request = daggy.taggedSum({ 23 | Fetch : ['x'], 24 | Pure : ['x'] 25 | }); 26 | 27 | function pure(x) { 28 | return Free.liftFC(Request.Pure(x)); 29 | } 30 | 31 | function fetch(s) { 32 | return Free.liftFC(Request.Fetch(s)); 33 | } 34 | 35 | function arrayNel(x) { 36 | function go(y, z) { 37 | return y.length < 1 ? z.x : go(y.slice(0, -1), Option.Some(Cofree(y.slice(-1)[0], z))); 38 | } 39 | return go(x, Option.None); 40 | } 41 | 42 | function nelArray(xs) { 43 | return [xs.a].concat(xs.f.cata({ 44 | Some: (x) => nelArray(x), 45 | None: () => [] 46 | })); 47 | } 48 | 49 | Cofree.prototype.sequence = function(p) { 50 | return this.traverse(identity, p); 51 | }; 52 | 53 | const interpreters = { 54 | pure : (req) => { 55 | const res = req.cata({ 56 | Pure: identity, 57 | Fetch: (s) => { 58 | return s.cata({ 59 | GetTweets: (id) => { 60 | return arrayNel([Tweet(1, 'Hello'), Tweet(2, 'World'), Tweet(3, '!')]); 61 | }, 62 | GetUserName: (id) => { 63 | return id === 1 ? 'Tim' 64 | : id === 2 ? 'Bob' 65 | : 'Anonymous'; 66 | }, 67 | GetUserPhoto: (id) => { 68 | return id === 1 ? ':-)' 69 | : id === 2 ? ':-D' 70 | : ':-|'; 71 | } 72 | }); 73 | } 74 | }); 75 | return Identity.of(res); 76 | } 77 | }; 78 | 79 | function getUser(id) { 80 | return fetch(Service.GetUserName(id)).chain((name) => { 81 | return fetch(Service.GetUserPhoto(id)).map((photo) => { 82 | return User(id, name, photo); 83 | }); 84 | }); 85 | } 86 | 87 | const id = 1; 88 | const script = fetch(Service.GetTweets(id)).chain((tweets) => { 89 | return tweets.map((tweet) => { 90 | return getUser(tweet.id).map((user) => { 91 | return singleton(tweet.str, user); 92 | }); 93 | }).sequence(Free); 94 | }); 95 | 96 | console.log('---------------------------------------------------------'); 97 | console.log(nelArray(Free.runFC(script, interpreters.pure, Identity).x)); 98 | console.log('---------------------------------------------------------'); 99 | -------------------------------------------------------------------------------- /examples/parser.js: -------------------------------------------------------------------------------- 1 | 'use struct'; 2 | 3 | const {tagged, taggedSum} = require('daggy'); 4 | const {FreeAp} = require('../fantasy-frees'); 5 | const {constant, identity} = require('fantasy-combinators'); 6 | 7 | const List = taggedSum({ 8 | Cons: ['head', 'tail'], 9 | Nil: [] 10 | }); 11 | List.of = (x) => List.Cons(x, List.Nil); 12 | List.empty = () => List.Nil; 13 | List.prototype.reverse = function() { 14 | const go = (x, acc) => { 15 | return x.cata({ 16 | Nil: constant(acc), 17 | Cons: (x, xs) => go(xs, List.Cons(x, acc)) 18 | }); 19 | }; 20 | return go(this, List.Nil); 21 | }; 22 | List.prototype.foldl = function(f, a) { 23 | const go = (x, acc) => { 24 | return x.cata({ 25 | Nil: constant(acc), 26 | Cons: (x, xs) => go(xs, f(acc, x)) 27 | }); 28 | }; 29 | return go(this, a); 30 | }; 31 | List.prototype.concat = function(a) { 32 | const go = (x, y) => { 33 | return x.cata({ 34 | Nil: constant(y), 35 | Cons: (a, b) => go(b, List.Cons(a, y)) 36 | }); 37 | }; 38 | return go(this.reverse(), a); 39 | }; 40 | 41 | const unit = tagged('x'); 42 | const Unit = () => unit(''); 43 | 44 | // Actual program. 45 | 46 | const AsmF = taggedSum({ 47 | Push: ['value', 'next'], 48 | Pop: ['next'], 49 | Sum: ['value'], 50 | Mul: ['value'] 51 | }); 52 | 53 | const dsl = { 54 | push: (value) => FreeAp.lift(AsmF.Push(value, Unit())), 55 | pop: (field) => FreeAp.lift(AsmF.Pop(identity)), 56 | sum: (field) => FreeAp.lift(AsmF.Sum(identity)), 57 | mul: (field) => FreeAp.lift(AsmF.Mul(identity)) 58 | }; 59 | 60 | const print = (p) => { 61 | return p.analyze((p) => { 62 | return p.cata({ 63 | Push: (v) => List.of('push ' + v), 64 | Pop: () => List.of('pop'), 65 | Sum: () => List.of('sum'), 66 | Mul: () => List.of('mul') 67 | }); 68 | }).reverse().foldl((a, b) => a + '\n' + b, 'Print >'); 69 | }; 70 | 71 | // Something is broken here! 72 | // As the ap composition laws don't hold true! 73 | const program = dsl.push(1).ap( 74 | dsl.push(2).ap( 75 | dsl.mul().ap( 76 | dsl.sum().ap( 77 | dsl.push(9) 78 | )))); 79 | console.log(print(program)); -------------------------------------------------------------------------------- /examples/trampoline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {Trampoline} = require('./../fantasy-frees'); 4 | 5 | function loop(n) { 6 | function inner(i) { 7 | return i == n ? 8 | Trampoline.done(n) : 9 | Trampoline.suspend(() => inner(i + 1)); 10 | } 11 | return Trampoline.run(inner(0)); 12 | } 13 | 14 | console.log(loop(1000000)); 15 | -------------------------------------------------------------------------------- /fantasy-frees.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Coyoneda = require('./src/coyoneda'); 4 | const Free = require('./src/free'); 5 | const FreeAp = require('./src/freeap'); 6 | const Trampoline = require('./src/trampoline'); 7 | const Yoneda = require('./src/yoneda'); 8 | 9 | module.exports = {Coyoneda, Free, FreeAp, Trampoline, Yoneda}; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fantasy-frees", 3 | "description": "Free yourself in the land of fantasies.", 4 | "author": "Simon Richardson (https://github.com/SimonRichardson)", 5 | "homepage": "https://github.com/fantasyland/fantasy-frees", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/fantasyland/fantasy-frees.git" 10 | }, 11 | "dependencies": { 12 | "fantasy-combinators": "^0.x.x", 13 | "fantasy-eithers": "^0.x.x", 14 | "fantasy-sorcery": "^0.x.x", 15 | "fantasy-land": "git+https://github.com/fantasyland/fantasy-land.git", 16 | "fantasy-consts": "git+https://github.com/fantasyland/fantasy-consts.git", 17 | "daggy": "^0.x.x" 18 | }, 19 | "devDependencies": { 20 | "nodeunit": "0.9.x", 21 | "fantasy-check": "^0.1.x", 22 | "fantasy-helpers": "^0.x.x", 23 | "fantasy-states": "^0.x.x", 24 | "fantasy-lenses": "^0.x.x", 25 | "fantasy-tuples": "^0.x.x", 26 | "fantasy-cofrees": "git+https://github.com/fantasyland/fantasy-cofrees.git", 27 | "fantasy-identities": "git+https://github.com/fantasyland/fantasy-identities.git", 28 | "fantasy-options": "git+https://github.com/fantasyland/fantasy-options.git", 29 | "fantasy-writers": "git+https://github.com/fantasyland/fantasy-writers.git", 30 | "fantasy-coproducts": "git+https://github.com/fantasyland/fantasy-coproducts.git", 31 | "fantasy-io": "git+https://github.com/fantasyland/fantasy-io.git", 32 | "readline-sync": "^1.2.19" 33 | }, 34 | "scripts": { 35 | "test": "node --harmony_destructuring node_modules/.bin/nodeunit test/*.js" 36 | }, 37 | "files": [ 38 | "fantasy-frees.js", 39 | "src/*.js" 40 | ], 41 | "main": "fantasy-frees.js", 42 | "version": "0.1.0", 43 | "bugs": { 44 | "url": "https://github.com/fantasyland/fantasy-frees/issues" 45 | }, 46 | "directories": { 47 | "example": "examples", 48 | "test": "test" 49 | }, 50 | "keywords": [ 51 | "Free", 52 | "Monads", 53 | "Coyoneda", 54 | "Yoneda", 55 | "Trampoline" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /src/coyoneda.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {compose, identity} = require('fantasy-combinators'); 4 | const {tagged} = require('daggy'); 5 | const {map} = require('fantasy-land'); 6 | 7 | const Coyoneda = tagged('f', 'x'); 8 | 9 | Coyoneda.lift = function(x) { 10 | return Coyoneda(identity, x); 11 | }; 12 | 13 | Coyoneda.prototype[map] = function(f) { 14 | return Coyoneda(compose(f)(this.f), this.x); 15 | }; 16 | 17 | Coyoneda.prototype.lower = function() { 18 | return this.x[map](this.f); 19 | }; 20 | 21 | module.exports = Coyoneda; -------------------------------------------------------------------------------- /src/free.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {compose, constant} = require('fantasy-combinators'); 4 | const {point} = require('fantasy-sorcery'); 5 | const {taggedSum} = require('daggy'); 6 | const {of, map, ap, chain} = require('fantasy-land'); 7 | 8 | const Either = require('fantasy-eithers'); 9 | const Coyoneda = require('./coyoneda'); 10 | 11 | const Free = taggedSum({ 12 | Return: ['x'], 13 | Suspend: ['x'], 14 | Chain: ['x', 'f'] 15 | }); 16 | 17 | Free[of] = Free.Return; 18 | 19 | Free.liftF = (c) => Free.Suspend(c[map](Free.Return)); 20 | 21 | Free.liftFC = (c) => Free.liftF(Coyoneda.lift(c)); 22 | 23 | Free.runFC = (m, f, p) => { 24 | return m.foldMap((coyo) => f(coyo.x)[map](coyo.f), p); 25 | }; 26 | 27 | Free.prototype[chain] = function(f) { 28 | const val = () => Free.Chain(this, f); 29 | return this.cata({ 30 | Return: val, 31 | Suspend: val, 32 | Chain: (y, g) => { 33 | return Free.Chain(y, (x) => Free.Chain(g(x), f)); 34 | }, 35 | }); 36 | }; 37 | 38 | Free.prototype[ap] = function(x) { 39 | return this[chain]((f) => x[map](f)); 40 | }; 41 | 42 | Free.prototype[map] = function(f) { 43 | return this[chain](compose(Free[of])(f)); 44 | }; 45 | 46 | Free.prototype.andThen = function(x) { 47 | return this[chain](constant(x)); 48 | }; 49 | 50 | Free.prototype.fold = function(f, g) { 51 | return this.resume().fold(f, g); 52 | }; 53 | 54 | Free.prototype.foldMap = function(f, p) { 55 | return this.resume().cata({ 56 | Left: (x) => { 57 | return f(x)[chain]((y) => y.foldMap(f, p)); 58 | }, 59 | Right: (x) => point(p, x) 60 | }); 61 | }; 62 | 63 | Free.prototype.resume = function() { 64 | return this.cata({ 65 | Return: Either.Right, 66 | Suspend: Either.Left, 67 | Chain: (x, f) => { 68 | return x.cata({ 69 | Return: (y) => f(y).resume(), 70 | Suspend: (y) => { 71 | return Either.Left(y[map]((z) => z[chain](f))); 72 | }, 73 | Chain: (y, g) => { 74 | return y[chain]((z) => g(z)[chain](f)).resume(); 75 | } 76 | }); 77 | } 78 | }); 79 | }; 80 | 81 | module.exports = Free; -------------------------------------------------------------------------------- /src/freeap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {taggedSum} = require('daggy'); 4 | const {compose, identity} = require('fantasy-combinators'); 5 | const Const = require('fantasy-consts'); 6 | const {of, ap, map} = require('fantasy-land'); 7 | 8 | const FreeAp = taggedSum({ 9 | Pure: ['a'], 10 | Ap: ['a', 'f'] 11 | }); 12 | 13 | FreeAp[of] = FreeAp.Pure; 14 | 15 | FreeAp.lift = (x) => { 16 | return FreeAp.Ap(x, FreeAp.Pure(identity)); 17 | }; 18 | 19 | FreeAp.prototype[ap] = function(f) { 20 | return this.cata({ 21 | Pure: (g) => f[map](g), 22 | Ap: (x, y) => FreeAp.Ap(x, f[ap](y[map]((g) => (a) => (b) => g(b, a)))) 23 | }); 24 | }; 25 | 26 | FreeAp.prototype[map] = function(f) { 27 | return this.cata({ 28 | Pure: (a) => FreeAp.Pure(f(a)), 29 | Ap: (x, y) => FreeAp.Ap(x, y[map](compose(f))) 30 | }); 31 | }; 32 | 33 | FreeAp.prototype.foldMap = function(f, p) { 34 | return this.cata({ 35 | Pure: FreeAp.Pure, 36 | Ap: (x, y) => y.foldMap(f)[ap](f(x)) 37 | }); 38 | }; 39 | 40 | FreeAp.prototype.analyze = function(f) { 41 | return this.foldMap((a) => Const(f(a))).x; 42 | }; 43 | 44 | module.exports = FreeAp; -------------------------------------------------------------------------------- /src/trampoline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {identity} = require('fantasy-combinators'); 4 | const Free = require('./free'); 5 | 6 | const done = (x) => Free.of(x); 7 | const suspend = (x) => Free.Suspend(x); 8 | const delay = (x) => suspend(done(x)); 9 | 10 | function run(x) { 11 | var done = false; 12 | 13 | const left = (f) => f(); 14 | const right = (x) => { 15 | done = true; 16 | return x; 17 | }; 18 | while(!done) { 19 | x = x.resume().cata({ 20 | Left: left, 21 | Right: right 22 | }); 23 | } 24 | return x; 25 | } 26 | 27 | module.exports = {delay, done, run, suspend}; -------------------------------------------------------------------------------- /src/yoneda.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {compose, identity} = require('fantasy-combinators'); 4 | const {tagged} = require('daggy'); 5 | const {map} = require('fantasy-land'); 6 | 7 | const Yoneda = tagged('f'); 8 | 9 | Yoneda.lift = (x) => Yoneda((y) => x[map](y)); 10 | 11 | Yoneda.prototype[map] = function(f) { 12 | return Yoneda((x) => this.run(compose(x)(f))); 13 | }; 14 | 15 | Yoneda.prototype.lower = function() { 16 | return this.f(identity); 17 | }; 18 | 19 | Yoneda.prototype.run = function(k) { 20 | return this.f(k); 21 | }; 22 | 23 | module.exports = Yoneda; -------------------------------------------------------------------------------- /test/coyoneda.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const λ = require('./lib/test'); 4 | const {functor, Identity} = λ; 5 | 6 | const {Coyoneda} = λ.Frees; 7 | 8 | function of(x) { 9 | return Coyoneda.lift(Identity.of(x)); 10 | } 11 | 12 | function run(x) { 13 | return x.lower(); 14 | } 15 | 16 | exports.coyoneda = { 17 | 18 | // Functor tests 19 | 'All (Functor)': functor.laws(λ)(of, run), 20 | 'Identity (Functor)': functor.identity(λ)(of, run), 21 | 'Composition (Functor)': functor.composition(λ)(of, run) 22 | }; 23 | -------------------------------------------------------------------------------- /test/free.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const λ = require('./lib/test'); 4 | const {applicative, functor, monad} = λ; 5 | 6 | const {Free} = λ.Frees; 7 | 8 | function run(x) { 9 | return x.resume(); 10 | } 11 | 12 | exports.free = { 13 | 14 | // Applicative Functor tests 15 | 'All (Applicative)': applicative.laws(λ)(Free, run), 16 | 'Identity (Applicative)': applicative.identity(λ)(Free, run), 17 | 'Composition (Applicative)': applicative.composition(λ)(Free, run), 18 | 'Homomorphism (Applicative)': applicative.homomorphism(λ)(Free, run), 19 | 'Interchange (Applicative)': applicative.interchange(λ)(Free, run), 20 | 21 | // Functor tests 22 | 'All (Functor)': functor.laws(λ)(Free.of, run), 23 | 'Identity (Functor)': functor.identity(λ)(Free.of, run), 24 | 'Composition (Functor)': functor.composition(λ)(Free.of, run), 25 | 26 | // Monad tests 27 | 'All (Monad)': monad.laws(λ)(Free, run), 28 | 'Left Identity (Monad)': monad.leftIdentity(λ)(Free, run), 29 | 'Right Identity (Monad)': monad.rightIdentity(λ)(Free, run), 30 | 'Associativity (Monad)': monad.associativity(λ)(Free, run) 31 | }; 32 | -------------------------------------------------------------------------------- /test/freeap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const λ = require('./lib/test'); 4 | const {applicative, functor, curry, identity} = λ; 5 | 6 | const {FreeAp} = λ.Frees; 7 | const {Tuple2} = λ.Tuples; 8 | 9 | exports.free = { 10 | 11 | // Applicative Functor tests 12 | 'All (Applicative)': applicative.laws(λ)(FreeAp, identity), 13 | 'Identity (Applicative)': applicative.identity(λ)(FreeAp, identity), 14 | 'Composition (Applicative)': applicative.composition(λ)(FreeAp, identity), 15 | 'Homomorphism (Applicative)': applicative.homomorphism(λ)(FreeAp, identity), 16 | 'Interchange (Applicative)': applicative.interchange(λ)(FreeAp, identity), 17 | 18 | // Functor tests 19 | 'All (Functor)': functor.laws(λ)(FreeAp.of, identity), 20 | 'Identity (Functor)': functor.identity(λ)(FreeAp.of, identity), 21 | 'Composition (Functor)': functor.composition(λ)(FreeAp.of, identity), 22 | 23 | // Manual Tests 24 | 'Test that curried version of Tuple is constructed.': λ.check( 25 | (a, b) => { 26 | const x = FreeAp.of(curry(Tuple2)).ap(FreeAp.of(a)).ap(FreeAp.of(b)); 27 | return λ.equals(x)(FreeAp.of(Tuple2(a, b))); 28 | }, 29 | [λ.AnyVal, λ.AnyVal] 30 | ) 31 | }; 32 | -------------------------------------------------------------------------------- /test/lib/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const λ = require('fantasy-check/src/adapters/nodeunit'); 4 | const applicative = require('fantasy-check/src/laws/applicative'); 5 | const functor = require('fantasy-check/src/laws/functor'); 6 | const monad = require('fantasy-check/src/laws/monad'); 7 | 8 | const daggy = require('daggy'); 9 | 10 | const {curry, isInstanceOf} = require('fantasy-helpers'); 11 | const {constant, identity} = require('fantasy-combinators'); 12 | 13 | const Identity = require('fantasy-identities'); 14 | const Tuples = require('fantasy-tuples'); 15 | 16 | const Frees = require('../../fantasy-frees'); 17 | 18 | const isIdentity = isInstanceOf(Identity); 19 | const isIdentityOf = isInstanceOf(identityOf); 20 | 21 | function equals(a) { 22 | return (b) => { 23 | // Replace with fantasy-equality 24 | const x = a.a; 25 | const y = b.a; 26 | return x._1 === y._1 && x._2 === y._2; 27 | }; 28 | } 29 | 30 | Identity.prototype.traverse = function(f, p) { 31 | return p.of(f(this.x)); 32 | }; 33 | 34 | function identityOf(type) { 35 | const self = this.getInstance(this, identityOf); 36 | self.type = type; 37 | return self; 38 | } 39 | 40 | const λʹ = λ 41 | .property('applicative', applicative) 42 | .property('functor', functor) 43 | .property('monad', monad) 44 | .property('equals', equals) 45 | .property('Frees', Frees) 46 | .property('Tuples', Tuples) 47 | .property('Identity', Identity) 48 | .property('isIdentity', isIdentity) 49 | .property('identityOf', identityOf) 50 | .property('curry', curry) 51 | .property('constant', constant) 52 | .property('identity', identity) 53 | .method('arb', isIdentityOf, function(a, b) { 54 | return Identity.of(this.arb(a.type, b - 1)); 55 | }); 56 | 57 | 58 | // Export 59 | if(typeof module != 'undefined') 60 | module.exports = λʹ; 61 | -------------------------------------------------------------------------------- /test/yoneda.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const λ = require('./lib/test'); 4 | const {functor, Identity} = λ; 5 | 6 | const {Yoneda} = λ.Frees; 7 | 8 | function of(x) { 9 | return Yoneda.lift(Identity.of(x)); 10 | } 11 | 12 | function run(x) { 13 | return x.lower(); 14 | } 15 | 16 | exports.coyoneda = { 17 | 18 | // Functor tests 19 | 'All (Functor)': functor.laws(λ)(of, run), 20 | 'Identity (Functor)': functor.identity(λ)(of, run), 21 | 'Composition (Functor)': functor.composition(λ)(of, run) 22 | }; 23 | --------------------------------------------------------------------------------