This is me trying to explain concepts from typed-functional-programming to myself. The plan is to turn parts of this into something I can show to other people.
There are no deps.
Run the tests:
node tests
.envrc |
const assert = require('assert') | |
const test = require('./test') | |
const data = require('./data') | |
test('creates containers for values', () => { | |
const Id = (val) => data(Id, val) | |
assert.equal(Id('a').toString(), 'Id(a)') | |
}) | |
test('#case tries to match on constructor, call the matching function', () => { | |
const Id = (val) => data(Id, val) | |
const subject = Id('foo') | |
subject.case( | |
[Id, (val) => assert.equal(val, 'foo')] | |
) | |
}) | |
test('#case takes many patterns to match, and uses the first match', () => { | |
const Just = (val) => data(Just, val) | |
const Nothing = () => data(Nothing) | |
const subject = Just('foo') | |
subject.case( | |
[Nothing, () => assert(false)], | |
[Just, (val) => assert.equal(val, 'foo')] | |
) | |
}) | |
test('#case throws an error if no match can be found', () => { | |
const Just = (val) => data(Just, val) | |
const Nothing = () => data(Nothing) | |
const subject = Just(1) | |
try { | |
subject.case( | |
[Nothing, () => {}] | |
) | |
} catch (error) { | |
assert.equal(error.message, 'No match for Just(1)') | |
} | |
}) | |
test('cases have an optional second param for guards', () => { | |
const Just = (val) => data(Just, val) | |
const Nothing = () => data(Nothing) | |
const subject = Just(1) | |
subject.case( | |
[Nothing, () => ( | |
assert(false) | |
)], | |
[Just, (v) => v === 2, () => ( | |
assert(false) | |
)], | |
[Just, (v) => v === 1, (v) => ( | |
assert.equal(v, 1) | |
)] | |
) | |
}) | |
test('cases must have a function', () => { | |
const Just = (val) => data(Just, val) | |
const Nothing = () => data(Nothing) | |
const subject = Just(1) | |
try { | |
subject.case( | |
[Nothing, () => {}], | |
[Just, 'Foo'] | |
) | |
} catch (error) { | |
assert.equal(error.message, 'Match must be a function. Was passed String(Foo)') | |
} | |
}) |
const finder = (constructor, values) => (pattern) => { | |
if (pattern.length === 3) { | |
const [cons, guard] = pattern | |
return (cons === constructor) && guard(...values) | |
} else { | |
const [cons] = pattern | |
return (cons === constructor) | |
} | |
} | |
module.exports = (constructor, ...values) => ({ | |
case (...cases) { | |
const match = cases.find(finder(constructor, values)) | |
if (!match) { | |
throw new Error(`No match for ${this.toString()}`) | |
} | |
const fun = match[match.length - 1] | |
if (typeof fun !== 'function') { | |
throw new Error(`Match must be a function. Was passed ${fun.constructor.name}(${fun})`) | |
} | |
return match[match.length - 1](...values) | |
}, | |
toString () { | |
return `${constructor.name}(${values.join(' ')})` | |
} | |
}) |
const assert = require('assert') | |
const test = require('./test') | |
const { | |
Left, | |
Right, | |
map, | |
bind, | |
either | |
} = require('./either') | |
test('either return the result of the failure func if instance is a Left', () => { | |
assert.equal('Error: Boom', either( | |
e => `Error: ${e}`, | |
v => `Value: ${v}`, | |
Left('Boom') | |
)) | |
}) | |
test('either returns the value if instance is a Right', () => { | |
assert.equal('Value: 3', either( | |
e => `Error: ${e}`, | |
v => `Value: ${v.toString()}`, | |
Right(3) | |
)) | |
}) | |
test('map calls function if instance is a Right', () => { | |
const m = map((v) => v * 2, Right(3)) | |
assert.equal(6, either(e => e, v => v, m)) | |
}) | |
test('map doesn\'t call function if instance is a Left', () => { | |
const m = map((v) => v * 2, Left('Skipped')) | |
assert.equal('Skipped', either(e => e, v => v.toString(), m)) | |
}) | |
test('bind let\'s you return an Either', () => { | |
const div = (a) => (b) => { | |
if (b === 0) { | |
return Left('Divide by 0 error') | |
} else { | |
return Right(a / b) | |
} | |
} | |
const right = bind(Right(5), div(10)) | |
assert.equal('2', either(e => e, v => v.toString(), right)) | |
const left = bind(Right(0), div(10)) | |
assert.equal('Divide by 0 error', either(e => e, v => v.toString(), left)) | |
}) | |
test('bind will not run on Lefts', () => { | |
const left = bind(Left('Some error'), (n) => Right(n)) | |
assert.equal('Some error', either(e => e, v => v.toString(), left)) | |
}) |
const data = require('./data') | |
const Right = (value) => ( | |
data(Right, value) | |
) | |
const Left = (value) => ( | |
data(Left, value) | |
) | |
// either :: (a -> c) -> (b -> c) -> Either a b -> c | |
const either = (failure, success, instance) => ( | |
instance.case( | |
[Right, (value) => ( | |
success(value) | |
)], | |
[Left, (error) => ( | |
failure(error) | |
)] | |
) | |
) | |
// map :: (b -> c) -> Either a b -> c | |
const map = (fn, instance) => ( | |
instance.case( | |
[Right, (value) => ( | |
Right(fn(value)) | |
)], | |
[Left, (value) => ( | |
Left(value) | |
)] | |
) | |
) | |
const bind = (instance, fn) => ( | |
instance.case( | |
[Right, (value) => ( | |
fn(value) | |
)], | |
[Left, (value) => ( | |
Left(value) | |
)] | |
) | |
) | |
module.exports = { | |
Left, | |
Right, | |
either, | |
map, | |
bind | |
} |
const assert = require('assert') | |
const test = require('./test') | |
const { map, apply, pure, bind } = require('./function') | |
test('map takes an instance and function which will be called with the return value of the instance', () => { | |
const fun = map( | |
(b) => b * 2, | |
(a) => a + 1 // this is 'b' above | |
) | |
// Note that this is function composition | |
assert.equal(fun(1), 4) | |
// (1 + 1) * 2 | |
assert.equal(fun(2), 6) | |
// (2 + 1) * 2 | |
}) | |
test('apply takes an instance which returns a function, and applies that function to the function passed as a second argument', () => { | |
const fun = apply( | |
(a) => (b) => b + a, | |
(a) => a + 2 // this is 'b' above | |
) | |
assert.equal(fun(1), 4) | |
// 1 + (1 + 2) | |
assert.equal(fun(2), 6) | |
// 2 + (2 + 4) | |
}) | |
test('pure puts a function in a function by returning a function which ignores it\'s arg and returns the passed function', () => { | |
const fun = pure((b) => b * 2) | |
assert.equal(fun(42)(2), 4) | |
// throws 42 away | |
}) | |
test('it\'s perhaps more idomatic to use pure before applying, but means you loose the input value being passed to the instance', () => { | |
const fun = apply( | |
pure((b) => b * 2), | |
// ^ the instance never sees 'a' | |
(a) => a + 2 | |
) | |
assert.equal(fun(1), 6) | |
// (1 + 2) * 2 | |
assert.equal(fun(2), 8) | |
// (2 + 2) * 2 | |
}) | |
test('bind takes an instance and a function which will be called with the return value of the instance and return a new function which takes the original value', () => { | |
const fun = bind( | |
(a) => a + 2, // this is 'b' below | |
(b) => (a) => a * b | |
) | |
assert.equal(fun(1), 3) | |
// 1 * (1 + 2) | |
assert.equal(fun(2), 8) | |
// 2 * (2 + 2) | |
}) |
const map = (fn, instance) => | |
(a) => fn(instance(a)) | |
const apply = (instance, fn) => | |
(a) => instance(a)(fn(a)) | |
const pure = (f) => | |
(_a) => f | |
const bind = (instance, fn) => | |
(a) => fn(instance(a))(a) | |
const ret = pure | |
module.exports = { | |
map, | |
bind, | |
ret, | |
apply, | |
pure | |
} |
const assert = require('assert') | |
const test = require('./test') | |
const { | |
Cons, | |
Empty, | |
map, | |
bind, | |
join, | |
toArray | |
} = require('./list') | |
test('map applies a transformation to each elemnt in a list', () => { | |
const list = Cons(1, Cons(2, Cons(3, Empty()))) | |
const mapped = map(a => a * 2, list) | |
assert.deepEqual(toArray(mapped), [2, 4, 6]) | |
}) | |
test('map doesn\'t run the function on empty lists', () => { | |
const list = Empty() | |
const mapped = map(a => a * 2, list) | |
assert.deepEqual(toArray(mapped), []) | |
}) | |
test('join combines two lists', () => { | |
const as = Cons(1, Cons(2, Cons(3, Empty()))) | |
const bs = Cons(10, Cons(20, Cons(30, Empty()))) | |
const joined = join(as, bs) | |
assert.deepEqual(toArray(joined), [1, 2, 3, 10, 20, 30]) | |
}) | |
test('bind takes a function which returns a list, calls it for every element and joins the results', () => { | |
const list = Cons(1, Cons(2, Cons(3, Empty()))) | |
const bound = bind(list, (a) => ( | |
Cons(a, Cons(a * a, Empty())) | |
)) | |
assert.deepEqual(toArray(bound), [1, 1, 2, 4, 3, 9]) | |
}) |
const data = require('./data') | |
const Cons = (head, tail) => ( | |
data(Cons, head, tail) | |
) | |
const Empty = () => ( | |
data(Empty) | |
) | |
// map :: (a -> b) -> List a ->List b | |
const map = (fn, instance) => ( | |
instance.case( | |
[Empty, () => ( | |
instance | |
)], | |
[Cons, (head, tail) => ( | |
Cons(fn(head), map(fn, tail)) | |
)] | |
) | |
) | |
// join :: List a -> List a -> List a | |
const join = (a, b) => ( | |
a.case( | |
[Empty, () => b], | |
[Cons, (head, tail) => ( | |
Cons(head, join(tail, b)) | |
)] | |
) | |
) | |
// bind :: List a -> (a -> List a) -> List a | |
const bind = (instance, fn) => ( | |
instance.case( | |
[Empty, () => ( | |
instance | |
)], | |
[Cons, (head, tail) => ( | |
join(fn(head), bind(tail, fn)) | |
)] | |
) | |
) | |
const fold = (fn, acc, list) => ( | |
list.case( | |
[Empty, () => acc], | |
[Cons, (head, tail) => ( | |
fold(fn, fn(acc, head), tail) | |
)] | |
) | |
) | |
const toArray = (list) => ( | |
fold((acc, a) => acc.concat(a), [], list) | |
) | |
const fromArray = (arr) => ( | |
arr.reverse().reduce((acc, a) => ( | |
Cons(a, acc) | |
), Empty()) | |
) | |
module.exports = { | |
Cons, | |
Empty, | |
map, | |
bind, | |
join, | |
toArray, | |
fromArray | |
} |
const assert = require('assert') | |
const test = require('./test') | |
const id = a => a | |
const { | |
Just, | |
Nothing, | |
map, | |
bind, | |
maybe | |
} = require('./maybe') | |
test('maybe returns the fallback if instance is a Nothing', () => { | |
assert.equal('Woot!', maybe('Woot!', id, Nothing())) | |
}) | |
test('maybe returns the value if instance is a Just', () => { | |
assert.equal(3, maybe(1, id, Just(3))) | |
}) | |
test('map calls function if instance is a Just', () => { | |
const m = map((v) => v * 2, Just(3)) | |
assert.equal(6, maybe(0, id, m)) | |
}) | |
test('map doesn\'t call function if instance is a Nothing', () => { | |
const m = map((v) => v * 2, Nothing()) | |
assert.equal(7, maybe(7, id, m)) | |
}) | |
test('bind let\'s you return a Maybe', () => { | |
const div = (a) => (b) => { | |
if (b === 0) { | |
return Nothing() | |
} else { | |
return Just(a / b) | |
} | |
} | |
const just = bind(Just(5), div(10)) | |
assert.equal(2, maybe(0, id, just)) | |
const nothing = bind(Just(0), div(10)) | |
assert.equal(-1, maybe(-1, id, nothing)) | |
}) | |
test('bind will not run on Nothings', () => { | |
const nothing = bind(Nothing(), (n) => Just(n)) | |
assert.equal(0, maybe(0, id, nothing)) | |
}) |
const data = require('./data') | |
const Just = (value) => ( | |
data(Just, value) | |
) | |
const Nothing = () => ( | |
data(Nothing) | |
) | |
// maybe :: b -> (a -> b) -> Maybe a -> b | |
const maybe = (fallback, fn, instance) => ( | |
instance.case( | |
[Just, (value) => ( | |
fn(value) | |
)], | |
[Nothing, () => ( | |
fallback | |
)] | |
) | |
) | |
// map :: (a -> b) -> Maybe b | |
const map = (fn, instance) => ( | |
instance.case( | |
[Just, (value) => ( | |
Just(fn(value)) | |
)], | |
[Nothing, () => ( | |
Nothing() | |
)] | |
) | |
) | |
// ret :: a -> Maybe a | |
const ret = (value) => Just(value) | |
// bind :: Maybe a -> (a -> Maybe b) -> Maybe b | |
const bind = (instance, fn) => ( | |
instance.case( | |
[Just, (value) => ( | |
fn(value) | |
)], | |
[Nothing, () => ( | |
Nothing() | |
)] | |
) | |
) | |
module.exports = { | |
Just, | |
Nothing, | |
map, | |
ret, | |
bind, | |
maybe | |
} |
const assert = require('assert') | |
const test = require('./test') | |
const { | |
Reader, | |
run, | |
map, | |
bind, | |
ret, | |
ask | |
} = require('./reader') | |
test('run applies the contained function to some environment', () => { | |
const reader = Reader(e => e + 1) | |
assert.equal(run(1, reader), 2) | |
}) | |
test('map applies a function to the value returned by the containing function', () => { | |
const reader = Reader(e => e * 2) | |
const mapped = map(a => a + 2, reader) | |
assert.equal(run(1, mapped), 4) | |
}) | |
test('bind applies a function to the value returned by running the instance and returns a new instance', () => { | |
const reader = Reader(e => e * 2) | |
const bound = bind(reader, | |
value => Reader( | |
env => env + value | |
) | |
) | |
assert.equal(run(1, bound), 3) | |
}) | |
test('ret places a normal value into a Reader', () => { | |
const reader = ret(2) | |
assert.equal(run(0, reader), 2) | |
}) | |
test('ask is a prebuilt reader which moves the env to the value', () => { | |
const reader = Reader(env => env * 2) | |
const asked = bind(reader, | |
value => bind(ask, | |
env => ret(env + value) | |
) | |
) | |
assert.equal(run(2, asked), 6) // (env = 2) + (value = (env * 2)) | |
}) |
const data = require('./data') | |
// data Reader e a = Reader (e -> a) | |
const Reader = (ea) => ( | |
data(Reader, ea) | |
) | |
const run = (env, instance) => ( | |
instance.case( | |
[Reader, (ea) => ( | |
ea(env) | |
)] | |
) | |
) | |
const map = (fn, instance) => ( | |
instance.case( | |
[Reader, (ea) => ( | |
Reader(e => fn(ea(e))) | |
)] | |
) | |
) | |
const bind = (instance, fn) => ( | |
Reader((env) => { | |
const result = run(env, instance) | |
return run(env, fn(result)) | |
}) | |
) | |
const ret = (value) => ( | |
Reader(_ => value) | |
) | |
const asks = (fn) => ( | |
Reader(env => fn(env)) | |
) | |
const ask = asks(a => a) | |
module.exports = { | |
Reader, | |
run, | |
map, | |
bind, | |
ret, | |
ask | |
} |
const https = require('https') | |
const { Left, Right } = require('./either') | |
const { Task, map, run, all, sequence } = require('./task') | |
const KEY = process.env.NASA_KEY | |
const get = (url) => ( | |
Task((done) => { | |
https.get(url, (res) => { | |
if (res.statusCode >= 400) { | |
return done(Left(new Error( | |
`Request Failed. Status Code: ${res.statusCode}` | |
))) | |
} | |
let rawData = '' | |
res.on('data', (chunk) => { | |
rawData += chunk | |
}) | |
res.on('end', () => { | |
done(Right(JSON.parse(rawData))) | |
}) | |
}).on('error', (error) => { | |
done(Left(error)) | |
}) | |
}) | |
) | |
const asteroids = get( | |
`https://api.nasa.gov/neo/rest/v1/feed?start_date=2017-02-09&end_date=2017-02-10&api_key=${KEY}` | |
) | |
const first = map((res) => ( | |
res.near_earth_objects | |
), asteroids) | |
const moreAsteroids = get( | |
`https://api.nasa.gov/neo/rest/v1/feed?start_date=2017-02-11&end_date=2017-02-12&api_key=${KEY}` | |
) | |
const second = map((res) => ( | |
res.element_count | |
), moreAsteroids) | |
// Run in series | |
const sequenced = sequence([ | |
first, | |
second | |
]) | |
console.log('SEQ STARTED') | |
run(sequenced, (results) => { | |
console.log('SEQ FINISHED') | |
results.case( | |
[Left, (error) => ( | |
console.error('Series', error) | |
)], | |
[Right, (values) => ( | |
console.log('Series', values) | |
)] | |
) | |
}) | |
// Run in parallel | |
const parallel = all([ | |
first, | |
second | |
]) | |
console.log('PAR STARTED') | |
run(parallel, (results) => { | |
console.log('PAR FINISHED') | |
results.case( | |
[Left, (error) => ( | |
console.error('Par', error) | |
)], | |
[Right, (values) => ( | |
console.log('Par', values) | |
)] | |
) | |
}) |
const test = require('./test') | |
const assert = require('assert') | |
const { Right, Left, either } = require('./either') | |
const { Task, run, map, bind, apply, pure } = require('./task') | |
const wait = Task((done) => { | |
setTimeout(() => { | |
done(Right('Waited')) | |
}, 100) | |
}) | |
run(wait, (result) => { | |
test('run runs aysnc thingssss', () => { | |
const value = either(e => e, v => v, result) | |
assert.equal(value, 'Waited') | |
}) | |
}) | |
const upperWait = map( | |
(result) => (result.toUpperCase()), | |
wait | |
) | |
run(upperWait, (result) => { | |
test('map transforms the eventual result', () => { | |
const value = either(e => e, v => v, result) | |
assert.equal(value, 'WAITED') | |
}) | |
}) | |
const failedWait = Task((done) => { | |
setTimeout(() => { | |
done(Left('BOOMOOOMMOOOOOO!!!')) | |
}, 100) | |
}) | |
const upperFailed = map( | |
(result) => (result.toUpperCase()), | |
failedWait | |
) | |
run(upperFailed, (result) => { | |
test('map skips transformation if task failed', () => { | |
const value = either(e => e, v => v, result) | |
assert.equal(value, 'BOOMOOOMMOOOOOO!!!') | |
}) | |
}) | |
const waitThenWaitAgain = bind(wait, (value) => ( | |
Task((done) => ( | |
setTimeout(() => { | |
done(Right(`I've ${value}`)) | |
}, 100) | |
)) | |
)) | |
run(waitThenWaitAgain, (result) => { | |
test('bind passes the successful result of one task to another', () => { | |
const value = either(e => e, v => v, result) | |
assert.equal(value, 'I\'ve Waited') | |
}) | |
}) | |
const waitThenWaitAgainFailed = bind(failedWait, (value) => ( | |
Task((done) => ( | |
setTimeout(() => { | |
done(Right(`I've ${value}`)) | |
}, 100) | |
)) | |
)) | |
run(waitThenWaitAgainFailed, (result) => { | |
test('bind forwards a failed task', () => { | |
result.case( | |
[Left, (error) => { | |
assert.equal(error, 'BOOMOOOMMOOOOOO!!!') | |
}] | |
) | |
}) | |
}) | |
const waitForOne = Task((done) => { | |
setTimeout(() => { | |
done(Right(1)) | |
}, 100) | |
}) | |
run(apply(pure(a => a * 2), waitForOne), (result) => { | |
test('apply applies a function inside a task to the value in another task', () => { | |
result.case( | |
[Left, (error) => { | |
assert.ifError(error) | |
}], | |
[Right, (value) => { | |
assert.equal(2, value) | |
}] | |
) | |
}) | |
}) |
const data = require('./data') | |
const { Left, Right } = require('./either') | |
// data Task a b = Task (((Either a b) -> ()) -> ()) | |
const Task = (action) => ( | |
data(Task, action) | |
) | |
// run :: Task a b -> ((Either a b) -> ()) -> () | |
const run = (instance, done) => { | |
instance.case( | |
[Task, (action) => { | |
action(done) | |
}] | |
) | |
} | |
// map :: (b -> c) -> Task a b -> Task a c | |
const map = (fn, task) => ( | |
Task((done) => ( | |
run(task, (result) => ( | |
result.case( | |
[Right, (value) => ( | |
done(Right(fn(value))) | |
)], | |
[Left, (error) => ( | |
done(Left(error)) | |
)] | |
) | |
)) | |
)) | |
) | |
// apply :: Task a (b -> c) -> Task a b -> Task a c | |
const apply = (app, task) => ( | |
bind(app, (fn) => map(fn, task)) | |
) | |
// ret :: a -> Task b a | |
const ret = (value) => ( | |
Task((done) => ( | |
done(Right(value)) | |
)) | |
) | |
const pure = ret | |
// bind :: Task a b -> (b -> Task a c) -> Task a c | |
const bind = (task, fn) => ( | |
Task((done) => ( | |
run(task, (result) => ( | |
result.case( | |
[Right, (value) => ( | |
run(fn(value), done) | |
)], | |
[Left, (error) => ( | |
done(Left(error)) | |
)] | |
) | |
)) | |
)) | |
) | |
const all = (tasks) => ( | |
Task((done) => { | |
let complete = 0 | |
const results = [] | |
tasks.forEach((task, i) => { | |
run(task, (result) => ( | |
result.case( | |
[Left, (error) => ( | |
done(Left(error)) | |
)], | |
[Right, (value) => { | |
results[i] = value | |
complete += 1 | |
if (complete === tasks.length) { | |
done(Right(results)) | |
} | |
}] | |
) | |
)) | |
}) | |
}) | |
) | |
const sequence = (actions = []) => { | |
if (actions.length === 0) { | |
return ret(actions) | |
} | |
const [head, ...tail] = actions | |
return bind(head, (value) => ( | |
bind(sequence(tail), (values) => ( | |
ret([value, ...values]) | |
)) | |
)) | |
} | |
module.exports = { | |
Task, | |
run, | |
map, | |
ret, | |
bind, | |
apply, | |
all, | |
sequence, | |
pure | |
} |
module.exports = (message, fn) => { | |
console.log(message) | |
try { | |
fn() | |
console.log('Yep') | |
} catch (e) { | |
console.log('Nope', e.message, e.stack) | |
} | |
} |
require('./maybe-test') | |
require('./either-test') | |
require('./list-test') | |
require('./function-test') | |
require('./reader-test') | |
require('./task-test') | |