Combinators

applyTo#

crocks/combinators/applyTo

applyTo :: a -> (a -> b) -> b

Ever run into a situation where you have a value but do not have a function to apply it to? Well this little bird, named Thrush, is there to help out. Just give it a value and it will give you back a function ready to take a function. Once that function is provided, it will return the result of applying your value to that function.

import applyTo from 'crocks/combinators/applyTo'
import First from 'crocks/First'
import Pair from 'crocks/Pair'
import compose from 'crocks/helpers/compose'
import flip from 'crocks/combinators/flip'
import isArray from 'crocks/predicates/isArray'
import isNumber from 'crocks/predicates/isNumber'
import isString from 'crocks/predicates/isString'
import map from 'crocks/pointfree/map'
import merge from 'crocks/pointfree/merge'
import mreduceMap from 'crocks/helpers/mreduceMap'
import safeLift from 'crocks/Maybe/safeLift'
// prices :: [ Number ]
const prices = [ 4.99, 29.99, 15.99 ]
// getPrices :: (a -> b) -> [ Number ]
const getPrices = compose(
applyTo(prices),
map
)
// discount :: Number -> Number -> Number
const discount = percent => price =>
Number((price - percent / 100 * price).toFixed(2))
getPrices(discount(10))
//=> [ 4.49, 26.99, 14.39 ]
getPrices(discount(80))
//=> [ 1, 6, 3.2 ]
// add :: Number -> Number -> Number
const add = x => y =>
x + y
// runAll :: [ (a -> b) ] -> a -> [ b ]
const runAll =
flip(compose(map, applyTo))
runAll([ add(10), add(20) ], 3)
//=> [ 13, 23 ]
// length :: [ a ] -> Number
const length = x =>
x.length
// yell :: String -> String
const yell = x =>
x.toUpperCase()
// Strategy :: Pair (a -> Boolean) (* -> *)
// strategies :: [ Strategy ]
const strategies = [
Pair(isNumber, add(10)),
Pair(isArray, length),
Pair(isString, yell)
]
// options :: [ Strategy ] -> a -> b
const options = flip(
x => mreduceMap(
First,
compose(applyTo(x), merge(safeLift))
)
)
options(strategies, 'hello')
//=> Just "HELLO"
options(strategies, [ 1, 9, 39 ])
//=> Just 3
options(strategies, 13)
//=> Just 23
options(strategies, null)
//=> Nothing

compose2#

crocks/combinators/compose2

compose2 :: (c -> d -> e) -> (a -> c) -> (b -> d) -> a -> b -> e

compose2 allows for composition between a binary function and two unary functions. compose2 takes a binary function followed by two unary functions and returns a binary function that maps the first argument with the first unary and the second with the second, passing the results to the given binary and returning the result.

import compose2 from 'crocks/combinators/compose2'
import and from 'crocks/logic/and'
import applyTo from 'crocks/combinators/applyTo'
import flip from 'crocks/combinators/flip'
import hasProp from 'crocks/predicates/hasProp'
import isNumber from 'crocks/predicates/isNumber'
import liftA2 from 'crocks/helpers/liftA2'
import map from 'crocks/pointfree/map'
import prop from 'crocks/Maybe/prop'
import safe from 'crocks/Maybe/safe'
import safeLift from 'crocks/Maybe/safeLift'
// isNonZero :: Number -> Boolean
const isNonZero = x =>
x !== 0
// isValidDivisor :: Number -> Boolean
const isValidDivisor =
and(isNumber, isNonZero)
// divideBy :: Number -> Number -> Number
const divideBy = x => y =>
y / x
// safeDivide :: Number -> Number -> Maybe Number
const safeDivide = compose2(
liftA2(divideBy),
safe(isValidDivisor),
safe(isNumber)
)
safeDivide(0.5, 21)
//=> Just 42
safeDivide('0.5', 21)
//=> Nothing
safeDivide(0.5, '21')
//=> Nothing
safeDivide(29, 0)
//=> Just 0
safeDivide(0, 29)
//=> Nothing
// Item :: { id: Integer }
// Items :: Array Item
const items =
[ { id: 2 }, { id: 1 } ]
// pluck :: String -> Array Object -> Maybe a
const pluck =
compose2(applyTo, prop, flip(map))
pluck('id', items)
//=> [ Just 2, Just 1 ]
// summarize :: String -> String -> String
const summarize = name => count =>
`${name} purchased ${count} items`
// getLength :: a -> Maybe Number
const getLength = safeLift(
hasProp('length'),
x => x.length
)
// createSummary :: Person -> Array Item -> String
const createSummary = compose2(
liftA2(summarize),
prop('name'),
getLength
)
createSummary({
name: 'Sam Smith'
}, items)
//=> Just "Sam Smith purchased 2 items"
// capitalize :: String -> String
const capitalize = str =>
`${str.charAt(0).toUpperCase()}${str.slice(1)}`
// join :: String -> String -> String -> String
const join = delim => right => left =>
`${left}${delim}${right}`
// toUpper :: String -> String
const toUpper = x =>
x.toUpperCase()
// createName :: String -> String -> String
const createName =
compose2(join(', '), capitalize, toUpper)
createName('Jon', 'doe')
//=> DOE, Jon
createName('sara', 'smith')
//=> SMITH, Sara

composeB#

crocks/combinators/composeB

composeB :: (b -> c) -> (a -> b) -> a -> c

Provides a means to describe a composition between two functions. it takes two functions and a value. Given composeB(f, g), which is read f after g, it will return a function that will take value a and apply it to g, passing the result as an argument to f, and will finally return the result of f. This allows only two functions, if you want to avoid things like: composeB(composeB(f, g), composeB(h, i)) then check out compose.

import composeB from 'crocks/combinators/composeB'
import Either from 'crocks/Either'
import ifElse from 'crocks/logic/ifElse'
import isString from 'crocks/predicates/isString'
const { Left, Right } = Either
// yell :: String -> String
const yell = x =>
`${x.toUpperCase()}!`
// safeYell :: a -> Either a String
const safeYell = ifElse(
isString,
composeB(Right, yell),
Left
)
safeYell('quite')
//=> Right "QUITE!"
safeYell(42)
//=> Left 42

constant#

crocks/combinators/constant

constant :: a -> () -> a

This is a very handy dandy function, used a lot. Pass it any value and it will give you back a function that will return that same value no matter what you pass it. constant is perfect for those moments where you need to pass a function but do not care about the input. constant will swallow any value given to it and always return the initial value it was given. It is important to note that any function that is passed into constant will get the added benefit of having curry applied to it.

import constant from 'crocks/combinators/constant'
import Result from 'crocks/Result'
import bimap from 'crocks/pointfree/bimap'
import composeB from 'crocks/combinators/composeB'
import ifElse from 'crocks/logic/ifElse'
import isString from 'crocks/predicates/isString'
import getPropOr from 'crocks/helpers/getPropOr'
const { Ok, Err } = Result
// whatsTheAnswer :: () -> Number
const whatsTheAnswer =
constant(42)
whatsTheAnswer('to life?')
//=> 42
whatsTheAnswer('to the universe?')
//=> 42
whatsTheAnswer('to everything?')
//=> 42
// ensure :: (a -> Boolean) -> a -> Result a
const ensure = pred =>
ifElse(pred, Ok, Err)
// getLength :: Result a String -> Result Number
const getLength = bimap(
constant(0), getPropOr(0, 'length')
)
// getLengthOfString :: a -> Result a String
const getLengthOfString = composeB(
getLength,
ensure(isString)
)
getLengthOfString('testing')
//=> Ok 7
getLengthOfString(42)
//=> Err 0
getLengthOfString([ 1, 2, 3, 4 ])
//=> Err 0

converge#

crocks/combinators/converge

converge :: (b -> c -> d) -> (a -> b) -> (a -> c) -> a -> d

Provides a means of passing an acculumating function and two branching functions. A value can be applied to the resulting function which will then be applied to each branching function, the results of which will be applied to the accumulating function.

import converge from 'crocks/combinators/converge'
import alt from 'crocks/pointfree/alt'
import getProp from 'crocks/Maybe/getProp'
import liftA2 from 'crocks/helpers/liftA2'
import getPropOr from 'crocks/helpers/getPropOr'
// data :: [ Number ]
const data = [ 1, 2, 3, 4, 5 ]
// divide :: Number -> Number -> Number
const divide = x => y =>
y / x
// add :: (Number, Number) -> Number
const add = (a, b) =>
b + a
// sum :: [ Number ] -> Number
const sum = xs =>
xs.reduce(add, 0)
// length :: [ a ] -> Number
const length =
getPropOr(0, 'length')
// average :: [ Number ] -> Number
const average =
converge(divide, length, sum)
average(data)
//=> 3
// maybeGetDisplay :: a -> Maybe b
const maybeGetDisplay =
getProp('display')
// maybeGetFirst :: a -> Maybe b
const maybeGetFirst =
getProp('first')
// maybeGetLast :: a -> Maybe b
const maybeGetLast =
getProp('last')
// buildFullName :: String -> String -> String
const buildFullName = surname => firstname =>
`${firstname} ${surname}`
// maybeConcatStrings :: Maybe String -> Maybe String -> Maybe String
const maybeBuildFullName = a => b =>
liftA2(buildFullName, a, b)
.alt(a)
.alt(b)
// maybeMakeDisplay :: a -> Maybe String
const maybeMakeDisplay = converge(
maybeBuildFullName,
maybeGetLast,
maybeGetFirst
)
// maybeGetName :: a -> Maybe b
const maybeGetName =
converge(alt, maybeMakeDisplay, maybeGetDisplay)
maybeGetName({ display: 'Jack Sparrow' })
//=> Just "Jack Sparrow"
maybeGetName({ first: 'J', last: 'S' })
//=> Just "J S"
maybeGetName({ display: 'Jack Sparrow', first: 'J', last: 'S' })
//=> Just "Jack Sparrow"
maybeGetName({ first: 'J' })
//=> Just "J"
maybeGetName({ first: 'S' })
//=> Just "S"

flip#

crocks/combinators/flip

flip :: (a -> b -> c) -> b -> a -> c

This little function just takes a function and returns a function that takes the first two parameters in reverse. flip is perfectly suited for those moments where you have the context of your function but not the data. Applying flip to the function will allow you to pass in your context and will return a function waiting for the data. This will happen often when you're using composition.

When required, one can compose flip calls down the line to flip all, or some of the other parameters if there are more than two. Mix and match to your heart's desire.

import flip from 'crocks/combinators/flip'
import Pred from 'crocks/Pred'
import composeB from 'crocks/combinators/composeB'
import concat from 'crocks/pointfree/concat'
import isNumber from 'crocks/predicates/isNumber'
import mconcat from 'crocks/helpers/mconcat'
import runWith from 'crocks/pointfree/runWith'
concat('first param. ', 'second param. ')
//=> "second param. first param. ""
flip(concat, 'first param. ', 'second param. ')
//=> "first param. second param. ""
// checkAll :: [ a -> Boolean ] -> a -> Boolean
const checkAll =
composeB(flip(runWith), mconcat(Pred))
// lte :: Number -> Number -> Boolean
const lte = a => b =>
b <= a
// gte :: Number -> Number -> Boolean
const gte = a => b =>
b >= a
// between2and10 :: a -> Boolean
const between2and10 = checkAll([
isNumber,
gte(2),
lte(10)
])
between2and10(8)
//=> true
between2and10(11)
//=> false
between2and10(1)
//=> false
between2and10('not a number')
//=> false

identity#

crocks/combinators/identity

identity :: a -> a

This function and constant are the workhorses of writing code with this library. It quite simply is just a function that when you pass it something, it returns that thing right back to you. So simple, I will leave it as an exercise to reason about why this is so powerful and important.

psi#

crocks/combinators/psi

psi :: (b -> b -> c) -> (a -> b) -> a -> a -> c

psi is a function that can be considered the sister of converge. Where converge takes one argument and maps it through two unary functions, merging the resulting values with a binary function, psi takes two arguments and runs them each through the same unary function before merging them with the given binary function.

psi is often used to compose equality checking functions or when needing to validate two arguments of the same type.

import psi from 'crocks/combinators/psi'
import and from 'crocks/logic/and'
import equals from 'crocks/pointfree/equals'
import isNumber from 'crocks/predicates/isNumber'
import liftA2 from 'crocks/helpers/liftA2'
import safe from 'crocks/Maybe/safe'
// isNonZero :: Number -> Boolean
const isNonZero = x =>
x !== 0
// isValidDivisor :: Number -> Boolean
const isValidDivisor =
and(isNumber, isNonZero)
// divideBy :: Number -> Number -> Number
const divideBy = x => y =>
y / x
// safeDivide :: Number -> Number -> Maybe Number
const safeDivide =
psi(liftA2(divideBy), safe(isValidDivisor))
safeDivide(0.5, 21)
//=> Just 42
safeDivide('0.5', 21)
//=> Nothing
safeDivide(0.5, '21')
//=> Nothing
safeDivide(29, 0)
//=> Nothing
// capitalize :: String -> String
const capitalize = str =>
`${str.charAt(0).toUpperCase()}${str.slice(1)}`
// join :: String -> String -> String -> String
const join = delim => right => left =>
`${left}${delim}${right}`
// createName :: String -> String -> String
const createName =
psi(join(', '), capitalize)
createName('Jon', 'doe')
//=> Doe, Jon
createName('sara', 'smith')
//=> Smith, Sara
// toLowerCase :: String -> String
const toLowerCase = str =>
str.toLowerCase()
// equalsIgnoreCase :: String -> String -> Boolean
const equalsIgnoreCase =
psi(equals, toLowerCase)
equalsIgnoreCase('test', 'TEst')
//=> true
equalsIgnoreCase('test', 'not-test')
//=> false

substitution#

crocks/combinators/substitution

substitution :: (a -> b -> c) -> (a -> b) -> a -> c

While it may seem like a complicated little bugger, substitution can come in very handy from time to time. substitution is used when you have a binary function and you can supply the first argument and can use that value to create the second argument. It first takes a binary function followed by a unary function for it's first two arguments. This will return a function that is ready to take some context, a. Once supplied the fun starts, it will pass the given a to the binary and unary functions, and will then apply the result of the unary function as the second parameter of the binary function. Finally after all that juggling, it will return the result of that binary function.

When used with partial application on that first parameter, a whole new world of combinatory madness is presented!

import substitution from 'crocks/combinators/substitution'
import composeB from 'crocks/combinators/composeB'
import curry from 'crocks/core/curry'
// getDetails :: String -> Number -> String
const getDetails = curry((text, length) =>
`The given text "${text}" has a length of ${length}`
)
// getLength :: a -> Number
const getLength = s =>
s.length
substitution(getDetails, getLength, 'testing')
//=> "The given text \"testing\" has a length of 7"
// getLastIndex :: a -> Number
const getLastIndex = composeB(
x => x - 1,
getLength
)
// slice :: Array -> Number -> Array
const slice = curry((arr, index) =>
arr.slice(index))
substitution(slice, getLastIndex, [ 1, 2, 3, 4, 5 ])
//=> [ 5 ]