API Documentation
Many of the examples contain functions with ramda. The reason is many ramda
function like R.map
for example detect the if the passed object has a map
method itself and then dispatches to that. This is cool because it let's you
write R.map(someFunc, somethingWithMap)
instead of somethingWithMap.map(someFunc)
.
and let's you take advantage of ramda pre-curried functions and let you apply
the function partially. LINK. This is in line with fantasyland LINK and you
can pretty mcuh expect this behaviour with anything that has a map
function
(aka Functors), ap
function (aka Applicatives), chain
function (Monads) and
and concat
function (Monoid).
Maybe
Represents a value that might not be there. In Elm syntax:
type Maybe a = Just a | Nothing
It is a Functor(aka has a map
function) and also Monad(aka has a chain
function).
.Just
Takes some value and creates a Just
// a -> Maybe a
Maybe.Just
Maybe.Just('Nicky') // Just('Nicky')
.Nothing
Creates a Nothing
. Takes no arguments.
// () -> Maybe a
Maybe.Nothing
Maybe.Nothing() // Nothing
#map
Let's you transform the value inside a Maybe. If it is a Just
the function
will be applied to it. If the Maybe is a Nothing
the map has no effect.
// (a -> b) -> Maybe a -> Maybe b
Maybe.prototype.map
const toUpper = str => str.toUpperCase();
Maybe.Just('Nicky').map(toUpper) // Just('NICKY')
Maybe.Nothing().map(toUpper) // Nothing
// with ramda
R.map(toUpper, Maybe.Just(4))
#chain
Let's you chain together multiple Maybes, while removing one layer so you don't
end up with nested Maybes. The callback will receive the value of the previous
Maybe, if is a Just
or will skip if it is a Nothing
.
// (a -> Maybe b) -> Maybe a -> Maybe b
Maybe.prototype.chain
const listHead = list => list.length === 0 ? Maybe.Nothing() : Maybe.Just(list[0])
Maybe.Just([1, 2, 3]).chain(listHead) // Just(1)
Maybe.Just([]).chain(listHead) // Nothing
Maybe.Nothing().chain(listHead) // Nothing
// with ramda
R.chain(listHead, Maybe.Just(4))
Result
Represents a result for an operation that might fail. In Elm syntax:
type Result a b = Err a | Ok b
It is a Functor(aka has a map
function) and also Monad(aka has a chain
function).
.Ok
Takes a value and creates an Ok
.
// a -> Result a
Result.Ok
Result.Ok(42) // Result(42)
.Err
Takes a value and creates an Err
.
// a -> Result a
Result.Err
Result.Err('Oh no') // Result('Oh no')
#map
Let's you transform the value inside a Result. If it is an Ok
the function
will be applied to it. If the Result is a Err
the map has no effect.
// (a -> value) -> Result x a -> Result x value
Result.prototype.map
const toUpper = str => str.toUpperCase();
Result.Ok('Nicky').map(toUpper) // Ok('NICKY')
Result.Err('Oh no!').map(toUpper) // Err('Oh no!')
// with ramda
R.map(toUpper, Result.Ok(4));
#mapError
Let's you transform the error inside a Result. If it is an Err
the function
will be applied to it. If the Result is an Ok
the function has no effect.
// (a -> value) -> Result a x -> Result value x
Result.prototype.mapError
const toUpper = str => str.toUpperCase();
Result.Ok('Nicky').mapError(toUpper) // Ok('Nicky')
Result.Err('Oh no!').mapError(toUpper) // Err('OH NO!')
#chain
Let's you chain together multiple Results, while removing one layer so you don't
end up with nested Results. The callback will receive the value of the previous
Result, if is an Ok
or will skip if it is an Err
.
// (a -> Result value) -> Result x a -> Result x value
Result.prototype.chain
const safeParseInt = str => {
const int = parseInt(str);
return isNaN(int) ? Result.Err('Nope') : Result.Ok(int);
};
Result.Ok('123').chain(safeParseInt) // Ok(123)
Result.Ok('This is NaN').chain(safeParseInt) // Err('Nope')
Result.Err('This is also NaN').chain(safeParseInt) // Err('This is also NaN')
// with ramda
R.chain(safeParseInt, Result.Ok(4));
union
Create your own union types. It takes an object, which Constructor names as keys and the number of arguments(arity) of that the constructors can take.
Name = union({
FullName: 2,
NickName: 1,
NoName: 0,
});
const name1 = Name.FullName('Peter', 'Lustig');
const name2 = Name.NickName('Pete');
const name3 = Name.NoName();
The constructors are always curried. So if you have an arity of more than 2, you can do partial function application.
const maxNeedsLastName = Name.FullName('Max'); // returns a function which takes the second argument.
const name = maxNeedsLastName('Pearson'); // FullName('Max', 'Pearson')
You can extend the type by accessing the prototype and adding methods, like this.
Name.prototype.toString = function() {
return matchWith(this, {
FullName: (first, last) => `${first} ${last}`,
NickName: name => name,
NoName: () => 'Unknown',
});
};
matchWith
Pattern match on union types. Takes a union and an object of cases. The keys need to be the constructors of the union and the values functions which accept the arguments stored within each constructor. It returns the return value of the function which is matched.
const age = Just(30);
matchWith(age, {
Just: val => val.toString(),
Nothing: () => 'I am not saying',
}); // returns '30'
const result = Result.Err('Bad Error');
matchWith(result, {
Ok: val => val.toString(),
Err: err => `Geez, not again: ${err}`,
}); // returns 'Geez, not again: Bad Error'
Errors are thrown when:
- The first argument is not a union.
- Not all cases are handled in the case object.
- Cases are handled that don't correspond to any constructor.
// throws an error because Nothing is not handled
matchWith(age, {
Just: val => val.toString(),
});
// throws an error because Nuttin is not a recognized pattern for `Maybe`
matchWith(age, {
Just: val => val.toString(),
Nuttin: () => 'I am not saying',
});
// throws an error because the first argument is not a union
matchWith({}, {
Just: val => val.toString(),
Nothing: () => 'I am not saying',
});
Task
Task are there for asynchronous tasks and to wrap up side-effects. They compose pretty well and can be mapped and chain.
In elm code:
Task error value
.of
Creating a Task from a function and a list of arguments it should be called with. The function can return a regular value or a Promise.
// get from the network in the browser
Task.of(fetch, ['https://example.com', { credentials: 'include' }]);
// get a random number
Task.of(Math.random);
// set a timeout
Task.of(() => new Promise((resolve) => setTimeout(resolve, 1000)));
.succeed
Create a task that immediately succeeds with the given value.
Task.succeed(42);
.fail
Create a task that immediately fails with the given error. The error can be any value but should ideally be a string or an Error object.
Task.fail('Something went wrong');
Task.fail(new Error('OMG'));
.all
Takes a list of tasks and returns a new task which, will (when run) execute all of these tasks concurrently. If successful it will succeed with an array of the tasks results. The first task that fails will make everything fail.
Task.all([Task.of(fetch, ['my.api.com']), Task.of(fetch, ['example.com'])])
.map(([result1, result2]) => ...) // will be mapped if fetch succeeds
.mapError(err => ...) // will be called if fetch fails
Task.all([Task.succeed(42), Task.fail('fail one'), Task.fail('fail 2')])
.mapError(err => ...) // 'fail one'
.sequence
Like Task.all
but executes the tasks sequentially.
Task.sequence([Task.of(fetch, ['my.api.com']), Task.of(fetch, ['example.com'])])
.map(([result1, result2]) => ...) // will be mapped if fetch succeeds
.mapError(err => ...) // will be called if fetch fails
Task.sequence([Task.succeed(42), Task.fail('fail one'), Task.fail('fail 2')])
.mapError(err => ...) // 'fail one'
#map
Let's you transform the value with which the Task will resolve.
If the task succeed the function will be applied to it its value.
If it fails the map has no effect. map
returns a new Task with the mapped value.
The mapping will be executed as soon as the task succeeds.
// (a -> value) -> Task x a -> Task x value
Task.prototype.map
const toUpper = str => str.toUpperCase();
Task.succeed('Hello').map(toUpper) // will map the value
Task.fail('Oh no').map(toUpper) // does nothing
// with ramda
R.map(toUpper, Task.succeed('Hello'))
#mapError
Let's you transform the value with which the Task will resolve.
If the task succeed the function will be applied to it its value.
If it fails the map has no effect. map
returns a new Task with the mapped value.
// (a -> b) -> Task a value -> Task b value
Task.prototype.mapError
const toUpper = str => str.toUpperCase();
Task.succeed('Hello').map(toUpper) // does nothing
Task.fail('Oh no').map(toUpper) // will map the error
#chain
Chain multiple tasks together. The first tasks will be completely done, when the second task starts. Takes a function which gets the resolved value of the previous task and returns the next task. The chain will end as soon as one task fails.
// (a -> Task err value) -> Task x a -> Task err value
Task.prototype.chain
const authTask = Task.of(fetch, ['auth.api.com']);
const loadDataTask = token => Task.of(fetch, ['api.data.com']);
Task.of(authTask).chain(loadDataTask) // chains the two tasks
// you can chain as many as you want
Task.of(authTask)
.chain(loadDataTask)
.chain(data => someOtherTask) // can go on and on
// with ramda
R.chain(token => Task.of(fetch, ['api.data.com']), Task.of(fetch, ['auth.api.com']))
#onError
Recover from failing tasks. Takes a function, which receives the error and returns a new task.
// (err -> Task x value) -> Task err a -> Task x value
Task.prototype.onError
Task.fail('Oh no').onError(err => Task.of(fetch, ['other.data.com'])) // will rescue use the fallback
Task.succeed('Hello').onError(err => Task.of(fetch, ['other.data.com']) // does nothing
#run
This is how tasks are actually executed. Since calling this function is impure
you should ideally only call it once in you entire program. If you chain
and
map
correctly then more than once shouldn't be necessary and keeps the rest of your
code pure. It returns a promise which fulfills if the Task succeeds or rejects
if the task fails. If you use run
right and only use it to kick of your application
then you can typically ignore this promise.
Task.prototype.run
Task.of(fetch, ['api.com']).run(); // actually runs the network request
// this is how create a task and only need one `run` with pure code
const main = Task.of(authTask, ['someid'])
.chain(token => dataTaskFromToken(token))
.map(data => ...)
.onError(err => ...)
// impure code
main.run();