Experimental The api can change between versions.
A javascript monadic library & functional fun. fantasy-land compliant, mostly.
The style of monad object is inspired by DrBoolean course on egghead.io and his book Mostly adequate guide to FP.
The choice for cata
, encase
, head
, tail
, last
is inspired by rametta's take on monads in pratica.
This is, in many ways, an evolution of oncha which I wrote with other people many years ago and is no longer maintained.
One of the main goals of the exalted.future is to make it possible to rely on natural transformation when composing Monads. So that you can write in one continuous flow your program, agnostic of the Monad you are working with. That is why flatMap
, mapReduce
, reduce
, fold
, and fork
use the same language, cata
. You can always call cata on an object, and it will compute your results. The following example attempts to illustrate that. Regardless that the fetch succeeds or fails the outcome will be the same, indifferent to calling cata
on Maybe, Either (Left|Right), or Future.
Future.promise(fetch('https://jsonplaceholder.typicode.com/todos/1'))
.chain(response => Either.encase(() => response.json()))
.chain(Future.promise)
.chain(Maybe)
.map(todo => todo.title)
.map(t => t.toUpperCase())
.cata({
Left: e => console.log('oops', e),
Right: json => console.log(json)
})
yarn add exalted.future
Name | Apply | Applicative | Setoid | Foldable | Functor | Monad | Chain |
---|---|---|---|---|---|---|---|
Either | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Future | ✔︎ | — | ✔︎ | ✔︎ | ✔︎ | — | ✔︎ |
Identity | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Maybe | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
- There is a divergence from fantasy-land where
reduce
is namedcata
and loosely based on daggy's union types. fold
always folds on identitya => a
, except when it does not like with theFuture
.Maybe.map
will return Nothing if the callback function returns null. In other wordsJust(null)
is impossible, unless you call the 'static' constructor like thisJust.of(null)
. See this pr for some explanation.- Left is not 100% applicative.
- Not all functions are documented, so you are encouraged to read the source code, you'll find
bimap
,swap
,fold
,foldr
,foldf
.
A Future monad for async computation. Left
is reject and Right
is resolve. Because Right
is always right and Left is not.
// Basic usage
Future((err, ok) => ok('Yay'))
.map(res => res.toUpperString())
.cata({
Left: err => log(`Err: ${err}`),
Right: res => log(`Res: ${res}`)
})
//=> 'YAY'
// Handle promises
Future.promise(fetch('https://api.awesome.com/catOfTheDay'))
.cata({
Left: err => log('There was an error fetching the cat of the day :('),
Right: cat => log('Cat of the day: ' + cat)
})
//=> 'Cat of the day: Garfield'
// Chain http calls
Future.promise(fetch('https://api.awesome.com/catOfTheDay'))
.chain(cat => Future.promise(fetch(`https://api.catfacts.com/${cat}`)))
.cata({
Left: err => log('There was an error fetching the cat of the day :('),
Right: facts => log('Facts for cat of the day: ' + facts)
})
//=> 'Facts for cat of the day: Garfield is awesome.'
Concats all the results form the list of futures.
all :: ([Futures]) -> b
Future.all(
Future.of('apple'),
Future((left, right) => setTimeout(() => right('orange'), 1000)),
Future.of('lemon')
).cata({
Left: () => (),
Right: ([ apple, orange, lemon ]) => console.log(apple, orange, lemon)
}) //=> apple, orange, lemon
Identity monad.
Id(5)
.map(num => num * 7)
.map(num => num - 1)
.cata({
Right: a => a
})
//=> 34
Maybe monad.
// Maybe of a string
Maybe('Hello exalted one')
.map(sentence => sentence.toUpperString())
.map(sentence => `${sentence}!`)
.cata({
Right: console.log
})
//=> 'HELLO EXALTED ONE!'
// Maybe of nothing
Maybe(null)
.map(sentence => sentence.toUpperString())
.alt(() => 'Maybe received a null')
.cata({
Right: console.log
})
//=> 'Maybe received a null'
An Either monad and nullable, Left, Right.
nullable('Hello') // this will return a Right('Hello')
.cata({
Left: () => 'Oops',
Right: val => `${val} world!`
})
//=> 'Hello world!'
nullable(null) // this will return a Left()
.cata({
Left: () => 'Oops',
Right: val => `${val} world!`
})
//=> 'Oops'
const extractEmail = obj => obj.email ? Right(obj.email) : Left()
extractEmail({ email: 'test@example.com' }
.map(extractDomain)
.cata({
Left: () => 'No email found!',
Right:x => x
})
//=> 'example.com'
extractEmail({ name: 'user' }
.map(extractDomain) // this will not get executed
.cata({
Left: () => 'No email found!',
Right: x => x
})
//=> 'No email found!'
The following functions are common to all monads types.
Sets the value to cata on.
alt :: Any -> Nothing of Any
Maybe(1).alt(5).cata({
Right: a => a
})
//=> 1
Maybe(null).alt(5).cata({
Right: a => a
})
//=> 5
chain :: (a -> b) -> b
Id(5).chain(a => Id(a))
//=> Id(5)
// You can use chain to join the monads.
Id(Id(5)).chain(a => a)
//=> Id(5)
cata :: ({ Left: () -> b, Right -> a -> a }) -> a | b
Id(5).cata({
Right: a => a
})
//=> 5
Id(5).cata({
Right: a => a + 1
})
//=> 6
Right(5).cata({
Left: a => 8 // ignored
Right: a => a + 1
})
//=> 6
Left(5).cata({
Left: a => a + 1
Right: a => 8 // ignored
})
//=> 6
Maybe(5).cata({
Right: a => a
})
//=> 5
Maybe(5).cata({
Left: () => { } // not called
Right: a => a + 1
})
//=> 6
Maybe(null).cata({
Left: () => 'there was a null'
Right: a => a + 1 // not called
})
//=> there was a null
Right(5).cata({
Left: () => 1,
Right: a => a + 2
})
//=> 7
Left(5).cata(a => a + 1)
//=> 6
chain :: (a -> b) -> b
Id(5).chain(a => Id(a))
//=> Id(5)
// You can use chain to join the monads.
Id(Id(5)).chain(a => a)
//=> Id(5)
Compose takes n functions as arguments and return a function.
const transform = compose(sentence => sentence.toUpperString(), sentence => `${sentence}!`)
const logTransform = compose(log, transform)
logTransform('Hello exalted one')
//=> 'HELLO EXALTED ONE!'
// supports miltiple arguments
compose(path.normalize, path.join)('./exalted', '/one')
//=> './exalted/one'
equals :: Id -> Boolean
Id(1).equals(Id(1))
//=> true
Id(2).equals(Id(1))
//=> false
Id(2).equals(Id(1)) === Id(1).equals(Id(1))
//=> false
inspect :: () -> String
Id(5).inspect()
//=> Id(5)
map :: (a -> b) -> Id of b
Id(7).map(a => a * 2)
//=> Id(14)
Map as partial application and first class with arity support.
map(a => a + 1, a => a * 3)([1, 2, 3])
//=> [4, 7, 10]
of :: a -> Id of a
Id(5).of(6)
//=> Id(6)
Id(5).of(Id(6))
//=> Id(Id(6))
Returns a Maybe.
head([1,2])
//=> Just(1)
head([])
//=> Nothing()
tail([1,2,3])
//=> Just([2,3])
tail([])
//=> Nothing()
last([1,2,3])
//=> Just(3)
last([])
//=> Nothing()
Returns Left | Right
.
Maybe.encase(() => JSON.parse('["foo","bar","baz"]'))
//=> Just(['foo','bar','baz'])
Maybe.encase(() => JSON.parse('['))
//=> Nothing()
Either.encase(() => JSON.parse('["foo","bar","baz"]'))
//=> Right(['foo','bar','baz'])
Either.encase(() => JSON.parse('['))
//=> Left(new SyntaxError ('Unexpected end of JSON input'))