Skip to content

Instantly share code, notes, and snippets.

@bradparker
Last active February 27, 2017 07:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bradparker/5c634bb4dfc60b2d143e2cc36adb7f1c to your computer and use it in GitHub Desktop.
Save bradparker/5c634bb4dfc60b2d143e2cc36adb7f1c to your computer and use it in GitHub Desktop.

Wanna learn about wheels? Maybe build a wheel? I dunno

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.

Get started

There are no deps.

Run the tests:

node tests
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')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment