@kimera/io
v0.1.1
Published
Lightweight monadic abstraction to 'purely' handle side effects in javascript.
Downloads
2
Maintainers
Readme
An IO provides
- Lazy evaluation of side effects.
- Easy cancellation at any point during its computation and resource cleanup after that.
- Clean API for easy resource management while doing side effects.
- Ultra lightweight, Gzipped ~ 1kb.
- Friendly Error Messages.
- Follows Haskell laws for Functors, Applicatives and Monads (See the tests for these laws).
Getting Started
yarn add @kimera/io
or
npm install --save @kimera/io
Basic Examples
import IO from '@kimera/io'
IO.of(1)
.map(x => x + 10)
.chain(x => IO.of(x + '!')) // JavaScript is awesome
.fork(
err => console.log(err),
x => console.log(x) // 11!
)
// Fetching data from API
const pureFetch = IO.encaseP(fetch);
pureFetch('https://jsonplaceholder.typicode.com/todos/1')
.chain(IO.encaseP(response => response.json()))
.fork(
console.error,
console.log
});
API
IO implements Fantasy Land and Static Land -compatible Functor, Bifunctor, Applicative and Monad (of, ap, map, bimap, chain). All versions of Fantasy Land are supported.
Creating IOs
IO
Creates an IO with the given computation. A computation is a function which takes two callbacks. Both are continuations for the computation. The first is reject, commonly abbreviated to rej; The second is resolve, or res. When the computation is finished (possibly asynchronously) it may call the appropriate continuation with a failure or success value.
Additionally, the computation may return a function containing resource management logic.
IO((reject, resolve) => {
setTimeout(resolve, 3000, 'Hello world');
});
of
Creates an IO which immediately resolves with the given value.
IO.of('Hello')
.map(x => `${x} World!`)
.fork(console.err, console.log)
rejected
Creates an IO which immediately rejects with the given value.
IO.rejected('Hello')
.map(x => `${x} World!`)
.fork(console.err, console.log) // Hello
encaseP
Allows Promise-returning functions to be turned into IO-returning functions.
Takes a function which returns a Promise, and a value, and returns an IO. When forked, the IO calls the function with the value to produce the Promise, and resolves with its resolution value, or rejects with its rejection reason.
const pureFetch = IO.encaseP(fetch);
pureFetch('https://jsonplaceholder.typicode.com/todos/1')
.chain(IO.encaseP(response => response.json()))
.fork(
console.error,
console.log
});
Transforming IO
map
Transforms the resolution value inside the OP, and returns an IO with the new value. The transformation is only applied to the resolution branch: if the IO is rejected, the transformation is ignored.
IO.of(1)
.map(x => x + 1)
.fork(console.error, console.log);
bimap
Maps the left function over the rejection value, or the right function over the resolution value, depending on which is present.
IO.of(1)
.bimap(x => x + '!', x => x + 1)
.fork(console.error, console.log);
//> 2
IO.reject('error')
.bimap(x => x + '!', x => x + 1)
.fork(console.error, console.log);
//! "error!"
chain
Sequence a new IO using the resolution value from another. Similarly to map, chain expects a function to transform the resolution value of an IO. But instead of returning the new value, chain expects an IO to be returned.
The transformation is only applied to the resolution branch: if the IO is rejected, the transformation is ignored.
IO.of(1)
.chain(x => IO.of(x + 1))
.fork(console.error, console.log);
//> 2
ap
Applies the function contained in the left-hand IO or Apply to the value contained in the right-hand IO or Apply. If one of the IO rejects the resulting IO will also be rejected.
IO.of(x => y => x + y)
.ap(IO.of(1))
.ap(IO.of(2))
.fork(console.error, console.log);
//> 3
fold
Applies the left function to the rejection value, or the right function to the resolution value, depending on which is present, and resolves with the result. Can be used with other type constructors like Left | Right from Either.
IO.of('hello')
.fold(Left, Right)
.fork(() => {}, console.log);
//> Right('hello')
IO.reject('it broke')
.fold(Left, Right)
.fork(() => {}, console.log);
//> Left('it broke')
Running IOs in parallel using Applicatives (Parallelism)
If an IO containing a function with order > 1, then the IOs applied to it will run parallely.
const f = x => y => x + y;
let firstLoaded = false;
let secondLoaded = false;
const M1 = IO((_, resolve) => {
setTimeout(() => {
firstLoaded = true;
resolve(10);
}, 1000);
});
const M2 = IO((_, resolve) => {
setTimeout(() => {
secondLoaded = true;
resolve(10);
}, 1000);
});
IO.of(f)
.ap(M1)
.ap(M2)
.fork(() => {}, () => {});
setTimeout(() => {
console.log(firstLoaded, secondLoaded) // true, true
}, 1100);
Running IOs
fork
Execute the computation represented by an IO, passing reject and resolve callbacks to continue once there is a result.
This function is called fork because it literally represents a fork in our program: a point where a single code-path splits in two. It is recommended to keep the number of calls to fork at a minimum for this reason. The more forks, the higher the code complexity.
Generally, one only needs to call fork in a single place in the entire program.
IO.of('world').fork(
err => console.log(`Oh no! ${err.message}`),
thing => console.log(`Hello ${thing}!`)
);
Cancelling IOs
Once forked, an IO can be cancelled at any point during its computation. Note that if cancelled, handler functions passed into fork will not run, instead the clean up functions returned from the side effecty functions will run.
const run = () => IO((_, resolve) => {
let timeout setTimeout(() => {
resolve('Finished');
}, 1000);
return () => clearTimeout(timeout);
});
const cancelAndCleanup = run()
.map(x => {
cancelAndCleanup();
return x;
})
.chain(() =>
IO((_, resolve) => {
setTimeout(() => {
resolve('Failed');
}, 1000);
}),
)
.fork(console.log, console.log);
Cleaning up after running sideEffects
Additionally functions inside IO can return a cleanup or resource management. If your computation chain is composed together with multiple IOs returning these cleanup functions, then if cancellation/cleanup function is called, all the cleanup functions will run according to their respective order,
const computation = IO((reject, resolve) => {
resolve('Running');
return () => console.log('First Cleanup');
})
.chain(x => IO((reject, resolve) => {
resolve(x + ' Second Running');
return () => console.log('Second Cleanup')
}))
const cleanup = computaion.fork(console.err, console.log)
cleanup()
// First Cleanup
// Second Cleanup