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') | |