├── .gitignore ├── LICENSE.md ├── README.md ├── cont.js ├── either.js ├── example.js ├── free.js ├── index.js ├── interpret.js ├── io.js ├── maybe.js ├── monad.js ├── package.json └── state.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | **/node_modules 4 | backstop_data 5 | builds/**/*.png 6 | **/*-diff.png 7 | repo 8 | design-system 9 | .DS_Store 10 | generated 11 | .backstop 12 | *.log 13 | logs 14 | results 15 | pids 16 | current 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Brian Lonsdorf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Collection of Free monads 2 | 3 | ## Install 4 | 5 | `npm install freeky` 6 | 7 | ## Synopsis 8 | 9 | Video series on Free Monads here: https://www.youtube.com/watch?v=WH5BrkzGgQY&list=PLK_hdtAJ4KqUWp5LJdLOgkD_8qKW0iAHi&index=1 10 | 11 | Walking through our example.js 12 | 13 | Run with `node --harmony_destructuring example.js` 14 | 15 | ```js 16 | 17 | // either Example 18 | const gtZero = x => 19 | (x > 0) ? Right(x) : Left("it was less than zero") 20 | 21 | // cont example (cont just wraps task. prob a misnomer) 22 | const asyncGet = n => 23 | Cont((rej, res) => setTimeout(() => res(n), 100)) 24 | 25 | // do syntax works for any 1 monad. Since it's all in Free, we can use multiple 26 | const app = Monad.do(function *() { 27 | const ioNumber = yield IO(() => 1) 28 | const maybeNumber = yield Just(2) 29 | const contNumber = yield asyncGet(3) 30 | const eitherNumber = yield gtZero(4) 31 | return Monad.of(ioNumber + maybeNumber + eitherNumber + contNumber) 32 | }) 33 | 34 | // some nt's to Task 35 | const maybeToTask = m => m.fold(Task.of, Task.rejected) 36 | const contToTask = c => c.t 37 | const eitherToTask = m => m.fold(Task.rejected, Task.of) 38 | const ioToTask = i => new Task((rej, res) => res(i.f())) 39 | 40 | // this tells our Free.foldMap how to dispatch. We need all of them to turn into a target monad (in this case Task) 41 | const runApp = dispatch([ [IOType, ioToTask], 42 | [ContType, contToTask], 43 | [Either, eitherToTask], 44 | [Maybe, maybeToTask] 45 | ]) 46 | 47 | app.foldMap(runApp, Task.of).fork(console.error, console.log) 48 | ``` 49 | 50 | 51 | 52 | 53 | ## Custom Types 54 | 55 | You can define your own types in Free and use them as monads. 56 | This is useful if you want to define a composable dsl or multiple interpreters for an action: 57 | 58 | ```js 59 | const Http = daggy.taggedSum({ Get: ['url'], Post: ['url', 'data'] }) 60 | const {Get, Post} = Http 61 | 62 | const get = url => liftF(Get(url)) 63 | const post = (url, data) => liftF(Post(url, data)) 64 | 65 | var myFn = get('/home').chain(html => post('save', {markup: html}) 66 | 67 | const httpToTask = m => 68 | m.cata({ 69 | Get: url => new Task((rej, res) => $.getJSON(url).then(res).error(rej), 70 | Post: (url, data) => new Task((rej, res) => $.post(url, data).then(res).error(rej)) 71 | }) 72 | 73 | myFn.foldMap(httpToTask, Task.of) 74 | ``` 75 | 76 | or fake it for testing: 77 | 78 | ```js 79 | const httpToId = m => 80 | m.cata({ 81 | Get: url => Id(`getting ${url}`), 82 | Post: (url, data) => Id(`posting ${url} with ${data}`) 83 | }) 84 | 85 | myFn.foldMap(httpToId, Id.of) 86 | ``` 87 | 88 | use it with other monads 89 | 90 | ```js 91 | const app = Monad.do(function *() { 92 | const page = yield get('/myUrl') 93 | const maybeHeader = yield Just($('#header', page)) 94 | return maybeHeader.chain(h => IO(() => console.log(h))) 95 | }) 96 | 97 | const runApp = dispatch([ [IOType, ioToTask], 98 | [Http, httpToTask], 99 | [Maybe, maybeToTask] 100 | ]) 101 | 102 | runApp.foldMap(httpToTask, Task.of) 103 | ``` 104 | 105 | 106 | -------------------------------------------------------------------------------- /cont.js: -------------------------------------------------------------------------------- 1 | const daggy = require('daggy') 2 | const Task = require('data.task') 3 | const liftF = require('./free').liftF 4 | const ContType = daggy.tagged('t') 5 | const Cont = f => liftF(ContType((new Task(f)))) 6 | 7 | module.exports = {ContType, Cont} 8 | -------------------------------------------------------------------------------- /either.js: -------------------------------------------------------------------------------- 1 | const daggy = require('daggy') 2 | const liftF = require('./free').liftF 3 | 4 | const Either = daggy.taggedSum({ Left_: ['x'], Right_: ['x'] }) 5 | 6 | const Left = x => liftF(Either.Left_(x)) 7 | const Right = x => liftF(Either.Right_(x)) 8 | 9 | Either.of = Right 10 | 11 | Either.prototype.fold = function(f, g) { 12 | return this.cata({ Left_: f, Right_: g }) 13 | } 14 | 15 | module.exports = {Either, Left, Right} 16 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const Task = require('data.task') 2 | const {IOType, IO} = require('./io') 3 | const {Either, Left, Right} = require('./either') 4 | const {Maybe, Just, Nothing} = require('./maybe') 5 | const {ContType, Cont} = require('./cont') 6 | const {State} = require('./state') 7 | const {dispatch} = require('./interpret') 8 | const Monad = require('./monad') 9 | 10 | // some nt's, but where to stash these? 11 | const maybeToTask = m => m.fold(Task.of, Task.rejected) 12 | const contToTask = c => c.t 13 | const eitherToTask = m => m.fold(Task.rejected, Task.of) 14 | const ioToTask = i => new Task((rej, res) => res(i.f())) 15 | 16 | 17 | // either Example 18 | const gtZero = x => 19 | (x > 0) ? Right(x) : Left("it was less than zero") 20 | 21 | // cont example (cont just wraps task. prob a misnomer) 22 | const asyncGet = n => 23 | Cont((rej, res) => setTimeout(() => res(n), 100)) 24 | 25 | // do syntax works for any 1 monad. Since it's all in Free, we can use multiple 26 | const app = Monad.do(function *() { 27 | const ioNumber = yield IO(() => 1) 28 | const maybeNumber = yield Just(2) 29 | const contNumber = yield asyncGet(3) 30 | const eitherNumber = yield gtZero(4) 31 | return Monad.of(ioNumber + maybeNumber + eitherNumber + contNumber) 32 | }) 33 | 34 | // this tells our Free.foldMap how to dispatch. We need all of them to turn into a target monad (in this case Task) 35 | const runApp = dispatch([ [IOType, ioToTask], 36 | [ContType, contToTask], 37 | [Either, eitherToTask], 38 | [Maybe, maybeToTask] 39 | ]) 40 | 41 | app.foldMap(runApp, Task.of).fork(console.error, console.log) 42 | app.foldMap(runApp, Task.of).fork(console.error, console.log) 43 | app.foldMap(runApp, Task.of).fork(console.error, console.log) 44 | // 10 45 | // 10 46 | // 10 47 | 48 | // do syntax is much nicer 49 | gtZero(10) 50 | .chain(ten => 51 | asyncGet(4).map(four => 52 | ten + four)) 53 | .foldMap(runApp, Task.of).fork(console.error, console.log) 54 | // 14 55 | 56 | gtZero(0).chain(() => asyncGet(4)).foldMap(runApp, Task.of).fork(console.error, console.log) 57 | // error: it was less than zero 58 | -------------------------------------------------------------------------------- /free.js: -------------------------------------------------------------------------------- 1 | const daggy = require('daggy') 2 | 3 | const Free = daggy.taggedSum({Impure: ['x', 'f'], Pure: ['x']}) 4 | 5 | Free.of = Free.Pure 6 | 7 | const kleisli_comp = (f, g) => x => f(x).chain(g) 8 | 9 | Free.prototype.fold = function() { 10 | return this.x.fold.apply(this.x, arguments) 11 | } 12 | 13 | 14 | Free.prototype.map = function(f) { 15 | return this.cata({ 16 | Impure: (x, g) => Free.Impure(x, y => g(y).map(f)), 17 | Pure: x => Free.Pure(f(x)) 18 | }) 19 | } 20 | 21 | Free.prototype.ap = function(a) { 22 | return this.cata({ 23 | Impure: (x, g) => Free.Impure(x, y => g(y).ap(a)), 24 | Pure: f => a.map(f) 25 | }) 26 | } 27 | 28 | Free.prototype.chain = function(f) { 29 | return this.cata({ 30 | Impure: (x, g) => Free.Impure(x, kleisli_comp(g, f)), 31 | Pure: x => f(x) 32 | }) 33 | } 34 | 35 | const liftF = command => Free.Impure(command, Free.Pure) 36 | 37 | // Special object which should be ignored. 38 | const IGNORE_VALUE = {} 39 | 40 | Free.prototype.foldMap = function(interpreter, of) { 41 | return this.cata({ 42 | Pure: a => of(a), 43 | Impure: (intruction_of_arg, next) => { 44 | if (intruction_of_arg === IGNORE_VALUE) { 45 | return next().foldMap(interpreter, of) 46 | } else { 47 | return interpreter(intruction_of_arg).chain(result => 48 | next(result).foldMap(interpreter, of)) 49 | } 50 | } 51 | }) 52 | } 53 | 54 | module.exports = { liftF, Free, IGNORE_VALUE } 55 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | IO: require('./io'), 3 | Either: require('./either'), 4 | Maybe: require('./maybe'), 5 | Cont: require('./cont'), 6 | State: require('./state'), 7 | dispatch: require('./interpret'), 8 | Monad: require('./monad'), 9 | Free: require('./free'), 10 | liftF: require('./free').liftF 11 | } 12 | -------------------------------------------------------------------------------- /interpret.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | const id = x => x 3 | 4 | const find = (xs, f) => { 5 | var found; 6 | for(let x in xs) { 7 | if(f(xs[x])) { 8 | found = xs[x] 9 | break; 10 | } 11 | } 12 | return found; 13 | } 14 | 15 | const dispatch = pairs => 16 | instruction_of_arg => { 17 | const interpreter = find(pairs, xs => // [type, interpreter] 18 | instruction_of_arg.constructor === xs[0])[1] 19 | 20 | return interpreter(instruction_of_arg) 21 | } 22 | 23 | 24 | module.exports = { dispatch } 25 | -------------------------------------------------------------------------------- /io.js: -------------------------------------------------------------------------------- 1 | const daggy = require('daggy') 2 | const liftF = require('./free').liftF 3 | 4 | const IOType = daggy.tagged('f') 5 | const IO = f => liftF(IOType(f)) 6 | 7 | module.exports = {IOType, IO} 8 | -------------------------------------------------------------------------------- /maybe.js: -------------------------------------------------------------------------------- 1 | const daggy = require('daggy') 2 | const liftF = require('./free').liftF 3 | 4 | const Maybe = daggy.taggedSum({ Just_: ['x'], Nothing_: [] }) 5 | 6 | const Just = x => liftF(Maybe.Just_(x)) 7 | const Nothing = liftF(Maybe.Nothing_) 8 | 9 | Maybe.of = Just 10 | 11 | Maybe.fromNullable = x => x == undefined ? Nothing : Just(x) 12 | 13 | Maybe.prototype.fold = function(f, g) { 14 | return this.cata({ Just_: f, Nothing_: g }) 15 | } 16 | 17 | module.exports = {Maybe, Just, Nothing} 18 | -------------------------------------------------------------------------------- /monad.js: -------------------------------------------------------------------------------- 1 | const { Free, IGNORE_VALUE } = require('./free') 2 | 3 | const Monad = { 4 | do: gen => Free.Impure(IGNORE_VALUE, () => { 5 | const g = gen() 6 | const step = value => { 7 | const result = g.next(value) 8 | return result.done ? 9 | result.value : 10 | result.value.chain(step) 11 | } 12 | return step() 13 | }), 14 | of: Free.of 15 | } 16 | 17 | module.exports = Monad 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freeky", 3 | "version": "0.0.4", 4 | "description": "Free monad collection", 5 | "main": "index.js", 6 | "dependencies": { 7 | "daggy": "0.0.1", 8 | "data.task": "^3.1.1" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/DrBoolean/freeky.git" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/DrBoolean/freeky/issues" 22 | }, 23 | "homepage": "https://github.com/DrBoolean/freeky#readme" 24 | } 25 | -------------------------------------------------------------------------------- /state.js: -------------------------------------------------------------------------------- 1 | const daggy = require('daggy') 2 | const liftF = require('./free').liftF 3 | 4 | const State = daggy.tagged('run') 5 | 6 | State.get = liftF(State(s => [s,s])) 7 | State.put = s => liftF(State(_ => [null,s])) 8 | 9 | State.modify = f => State.get.chain(x => State.put(f(x))) 10 | 11 | State.of = a => State(b => [a, b]); 12 | State.prototype.chain = function(f) { 13 | return State(s => { 14 | const xs = this.run(s); 15 | return f(xs[0]).run(xs[1]); 16 | }); 17 | }; 18 | 19 | 20 | module.exports = State 21 | --------------------------------------------------------------------------------