Implements
Functor
, Apply
, Chain
, Applicative
, Monad
Reader e a
Reader
is a lazy Product Type that enables the composition of computations that depend on a shared environment (e -> a)
. The left portion, the e
must be fixed to a type for all related computations. The right portion a
can vary in its type.
As Reader
is lazy, wrapping a function of the form (e -> a)
, nothing is executed until it is run with an environment. Reader
provides a method on it's instance that will take an environment called runWith
that will run the instance with a given environment.
Not only is Reader
's environment fixed to a type, but it should be immutable for the "life" computation. If a referential type is used as the environment great care should be taken to not modify the value of the environment.
import Reader from 'crocks/Reader'
import concat from 'crocks/pointfree/concat'
const { ask } = Reader
// greet :: String -> Reader String String
const greet = greeting =>
Reader(name => `${greeting}, ${name}`)
// addFarewell :: String -> Reader String String
const addFarewell = farewell => str =>
ask(env => `${str}${farewell} ${env}`)
// flow :: Reader String String
const flow =
greet('Hola')
.map(concat('...'))
.chain(addFarewell('See Ya'))
flow
.runWith('Thomas')
//=> Hola, Thomas...See Ya Thomas
flow
.runWith('Jenny')
//=> Hola, Jenny...See Ya Jenny
Functor
, Apply
, Chain
, Applicative
, Monad
Reader :: (e -> a) -> Reader e a
The constructor for a Reader
type is a unary function that requires another unary function as its input. After passing the constructor a function, it will return a new Reader
instance. The left type is parameterized by the input, or domain, of the wrapped function. While the right type represents the return value, or co-domain.
The left type e
represents a family of Reader
s that can be combined and must be fixed to that type for all valid combination of instances.
import Reader from 'crocks/Reader'
import setProp from 'crocks/helpers/setProp'
// Reader Object Object
Reader(setProp('animal', 'cat'))
//=> Reader (Object -> Object)
// Reader a Number
Reader(x => x.length || 0)
//=> Reader (a -> Number)
Reader.ask :: () -> Reader e e
Reader.ask :: (e -> b) -> Reader e b
A construction helper that returns a Reader
with the environment on the right portion of the Reader
. ask
can take a function, that can be used to map the environment to a different type or value.
import Reader from 'crocks/Reader'
const { ask } = Reader
// add :: Number -> Number -> Number
const add =
x => y => x + y
// Typical constructor
Reader(add(10))
.runWith(56)
//=> 66
// Using `ask` with no function
// (identity on environment)
ask()
.runWith(56)
//=> 56
// Using `ask` with a function
// (map environment before deposit)
ask(add(10))
.runWith(56)
//=> 66
Reader.of :: a -> Reader e a
of
is used to construct a Reader
with the right portion populated with it's argument. of
essentially will lift a value of type a
into a Reader
, giving back a Reader
that is "pointed" to the specific value provided. of
makes for a wonderful starting point for some of the more complicated flows.
import Reader from 'crocks/Reader'
import objOf from 'crocks/helpers/objOf'
import thrush from 'crocks/combinators/applyTo'
const { ask } = Reader
// add :: Number -> Number -> Number
const add =
x => y => x + y
Reader.of(34)
.map(add(6))
.runWith()
//=> 40
Reader.of('Bobby')
.map(objOf('name'))
.runWith()
//=> { name: 'Bobby' }
Reader.of(57)
.chain(x => ask(add).map(thrush(x)))
.runWith(43)
//=> 100
Reader e a ~> (a -> b) -> Reader e b
While the left side, or the environment, of the Reader
must always be fixed to the same type, the right side, or value, of the Reader
may vary. Using map
allows a function to be lifted into the Reader
, mapping the result into the result of the lifted function.
import Reader from 'crocks/Reader'
import assign from 'crocks/helpers/assign'
import B from 'crocks/combinators/composeB'
import getProp from 'crocks/Maybe/getProp'
import objOf from 'crocks/helpers/objOf'
import option from 'crocks/pointfree/option'
const { ask } = Reader
// length :: Array -> Number
const length =
x => x.length
ask()
.map(length)
.runWith([ 1, 2, 3 ])
//=> 3
// propOr :: (String, a) -> b -> a
const propOr = (key, def) =>
B(option(def), getProp(key))
// lengthObj :: Array -> Object
const lengthObj =
B(objOf('length'), length)
// addLength :: Object -> Reader Array Object
const addLength = x =>
ask(propOr('list', []))
.map(B(assign(x), lengthObj))
Reader.of({ num: 27 })
.chain(addLength)
.runWith({ list: [ 1, 2, 3 ] })
//=> { length: 3, num: 27 }
Reader e (a -> b) ~> Reader e a -> Reader e b
ap
allows for values wrapped in a Reader
to be applied to functions also wrapped in a Reader
. In order to use ap
, the Reader
must contain a function as its value. Under the hood, ap
unwraps both the function and the value to be applied and applies the value to the function. Finally it will wrap the result of that application back into a Reader
. It is required that the inner function is curried.
import Reader from 'crocks/Reader'
import B from 'crocks/combinators/composeB'
import assign from 'crocks/helpers/assign'
import liftA2 from 'crocks/helpers/liftA2'
import objOf from 'crocks/helpers/objOf'
const { ask } = Reader
// namePart :: Number -> String -> String
const namePart = indx => x =>
x.split(' ')[indx] || ''
// combine :: Object -> Reader Object
const combine =
x => ask(assign(x))
// full :: Reader Object
const full =
ask(({ full }) => full)
// first :: Reader Object
const first =
full
.map(B(objOf('first'), namePart(0)))
// last :: Reader Object
const last =
full
.map(B(objOf('last'), namePart(1)))
// fluent style
Reader.of(assign)
.ap(first)
.ap(last)
.chain(combine)
.runWith({ full: 'Mary Jones' })
//=> { full: 'Mary Jones', first: 'Mary', last: 'Jones' }
// liftAssign :: Reader Object -> Reader Object -> Reader Object
const liftAssign =
liftA2(assign)
// using a lift function
liftAssign(first, last)
.chain(combine)
.runWith({ full: 'Tom Jennings' })
//=> { full: 'Tom Jennings', first: 'Tom', last: 'Jennings' }
Reader e a ~> (a -> Reader e b) -> Reader e b
One of the ways Monad
s like Reader
are able to be combined and have their effects applied, is by using the chain
method. In the case of Reader
, the effect is to read in and make available the shared environment. chain
expects a function that will take any a
and return a new Reader
with a value of b
.
import Reader from 'crocks/Reader'
import B from 'crocks/combinators/composeB'
import getProp from 'crocks/Maybe/getProp'
import option from 'crocks/pointfree/option'
const { ask } = Reader
// multiply :: Number -> Number -> Number
const multiply =
x => y => x * y
// add :: Number -> Number -> Number
const add =
x => y => x + y
// propOr :: (String, a) -> b -> a
const propOr = (key, def) =>
B(option(def), getProp(key))
// applyScale :: Number -> Reader Object Number
const applyScale = x =>
ask(propOr('scale', 1))
.map(multiply(x))
// applyScale :: Number -> Reader Object Number
const applyOffset = x =>
ask(propOr('offset', 0))
.map(add(x))
// applyTransforms :: Number -> Reader Object Number
const applyTransform = x =>
Reader.of(x)
.chain(applyOffset)
.chain(applyScale)
applyTransform(45)
.runWith({})
//=> 45
applyTransform(45)
.runWith({ offset: 20 })
//=> 65
applyTransform(45)
.runWith({ scale: 2 })
//=> 90
applyTransform(45)
.runWith({ scale: 2, offset: 20 })
//=> 130
Reader e a ~> e -> a
As Reader
is a lazy datatype that requires a shared environment to run, it's instance provides a runWith
method that takes in an environment and returns the result of the computation.
import Reader from 'crocks/Reader'
import Pair from 'crocks/Pair'
import fst from 'crocks/Pair/fst'
import liftA2 from 'crocks/helpers/liftA2'
import snd from 'crocks/Pair/snd'
const { ask } = Reader
// data :: Pair Number Number
const data =
Pair(20, 45)
// getCorrect :: Reader (Pair Number Number) Number
const getCorrect =
ask(fst)
// getTotal :: Reader (Pair Number Number) Number
const getTotal =
ask(snd)
// divide :: Number -> Number -> Number
const divide =
x => y => x / y
// formatPercent :: Number -> String
const formatPercent =
x => `${Math.floor(x * 1000) / 10}%`
// calcPercent :: Reader (Pair Number Number) String
const calcPercent =
liftA2(divide, getCorrect, getTotal)
.map(formatPercent)
calcPercent
.runWith(data)
//=. '44.4%'
Contribute on Github! Edit this section.