convenient-fp-utils
Visual reference — animated diagrams for every function
Combinators
T
T :: a → (a → b) → b
Thrush combinator. Value first, function second. Flips application order so you can write
T(data)(pipeline) instead of pipeline(data).
3
→
T
→
x → x + 1
→
4
T(3)(x => x + 1) // => 4
T(input)(pipe([parse, validate, render])) // data-first pipeline
T(input)(pipe([parse, validate, render])) // data-first pipeline
substitution
substitution :: (a → b → c) → (a → b) → a → c
S combinator (Starling). Threads one value
x to two consumers: f(x)(g(x)). The value branches — one copy goes to g, the result feeds into f(x).
x = 5
f(x) = add(5)
g(x) = double(5) = 10
f(x)(g(x)) = add(5)(10)
15
// f = add, g = double
substitution(add)(double)(5) // => add(5)(double(5)) => add(5)(10) => 15
// real-world: thread err to both default builder and Maybe chain
substitution(compose(fromMaybe)(String))(pipe([toMaybe(isObject), chainErrMsg]))
substitution(add)(double)(5) // => add(5)(double(5)) => add(5)(10) => 15
// real-world: thread err to both default builder and Maybe chain
substitution(compose(fromMaybe)(String))(pipe([toMaybe(isObject), chainErrMsg]))
delayApply
delayApply :: (a → b) → a → (() → b)
Captures the argument in a thunk, delaying evaluation. Useful with
encase — exceptions land inside the Future instead of blowing up immediately.
JSON.parse
→
"bad json"
→
() => JSON.parse("bad json")
🛑
not called yet!
delayApply(JSON.parse)("bad") // => () => JSON.parse("bad")
// the thunk is safe to pass to encase — exception is caught
encase(delayApply(JSON.parse)("bad"))() // => Left(SyntaxError)
// the thunk is safe to pass to encase — exception is caught
encase(delayApply(JSON.parse)("bad"))() // => Left(SyntaxError)
constTrue / constFalse
constTrue :: a → Boolean | constFalse :: a → Boolean
K(true) and K(false). Ignore the argument, always return the same boolean. Useful as fallback predicates.
"anything"
→
constTrue
→
true
42
→
constTrue
→
true
null
→
constFalse
→
false
mergeSingleton
mergeSingleton :: String → a → StrMap a → StrMap a
Builds a singleton
{key: val} and concats it onto an existing StrMap. Point-free: compose(compose(concat))(singleton).
"age"
→
30
→
{age: 30}
++
{name: "Ana"}
=
{name: "Ana", age: 30}
mergeSingleton("age")(30)({ name: "Ana" }) // => { name: "Ana", age: 30 }
Maybe
toMaybe
toMaybe :: (a → Boolean) → a → Maybe a
Predicate gate:
Just(v) when the predicate passes, Nothing otherwise. The universal entry point into Maybe land.
isString
→
"hello"
→
✓ pred
→
Just("hello")
isString
→
42
→
✗ pred
→
Nothing
toMaybe(isString)("hello") // => Just("hello")
toMaybe(isString)(42) // => Nothing
toMaybe(isString)(42) // => Nothing
safeGet
safeGet :: (a → Boolean) → String → Any → Maybe a
Total property accessor. Checks the object is valid, extracts the key, validates with the predicate. Any failure →
Nothing. Never throws.
isString
"name"
{name: "Ana"}
→
Just("Ana")
isString
"name"
{name: 42}
→
Nothing
isString
"name"
null
→
Nothing
safeGet(isString)("name")({ name: "Ana" }) // => Just("Ana")
safeGet(isString)("name")({ name: 42 }) // => Nothing
safeGet(isString)("name")(null) // => Nothing (no throw!)
safeGet(isString)("name")({ name: 42 }) // => Nothing
safeGet(isString)("name")(null) // => Nothing (no throw!)
equalsNonNull
equalsNonNull :: a → Any → Boolean
S.equals throws on null/undefined (not Setoid). This gates non-nullable first, returning false instead of throwing.
"a"
"a"
→
true
"a"
null
→
false
(no throw)
firstOf / secondOf
firstOf :: Array a → Maybe a | secondOf :: Array a → Maybe a
Total array accessors.
firstOf = S.head. secondOf = compose tail then head (S.nth doesn't exist in v3.1.0).
["a", "b", "c"]
→
firstOf
→
Just("a")
["a", "b", "c"]
→
secondOf
→
Just("b")
[]
→
firstOf
→
Nothing
Guards
Type Guards
guard :: Any → Boolean
Total predicates — never throw. Each wraps
S.is(TypeRep) or a custom check with sanctuary-def type checking.isString"hi"→true
isString42→false
isBooleantrue→true
isBoolean0→false
isObject{}→true
isObject[]→false
isInt3→true
isInt3.5→false
isFiniteNumber3.5→true
isFiniteNumberNaN→false
isNonEmptyStr"x"→true
isNonEmptyStr""→false
isNonNullable"x"→true
isNonNullablenull→false
isDateStr"2026-08-17"→true
isDateStr"2026-02-30"→false
isIn["a","b"]("a")→true
isIn["a","b"]("c")→false
// isArrayOf: higher-order guard — takes a predicate
isArrayOf(isString)(["a", "b"]) // => true
isArrayOf(isString)(["a", 1]) // => false
isArrayOf(isString)(["a", "b"]) // => true
isArrayOf(isString)(["a", 1]) // => false
isInRange
isInRange :: FiniteNumber → FiniteNumber → Any → Boolean
Inclusive range check. Also validates the input is a finite number —
NaN, Infinity, strings all return false.
0
100
isInRange(1)(10)
5
→
true
isInRange(1)(10)
0
→
false
isInRange(1)(10)
NaN
→
false
record
record :: StrMap (a → Boolean) → Any → Boolean
Shape guard. Defines expected keys + their predicates. Returns
true when every predicate passes. Total — null/undefined inputs return false, never throw.
{ name: isString, age: isInt }
applied to:
{name: "Ana", age: 30}
→
name ✓
age ✓
=
true
{name: "Ana"}
→
name ✓
age ✗
=
false
null
→
false
(total, no throw)
Parse
parseFloatM
parseFloatM :: String → Maybe FiniteNumber
Total float parser. Empty strings,
"NaN", non-finite results all become Nothing.
"3.14"
→
parseFloatM
→
Just(3.14)
""
→
parseFloatM
→
Nothing
"abc"
→
parseFloatM
→
Nothing
parseIntM
parseIntM :: (String, Number, Number) → Maybe Integer
Range-bounded integer parser. Validates non-empty, parses base-10, checks integer + range. Uses prefix semantics (
"3.5" → 3).
"42"
[0, 100]
→
Just(42)
"200"
[0, 100]
→
Nothing
out of range
"3.5"
[0, 100]
→
Just(3)
parseInt prefix
Collections
allPass / anyPass
allPass :: [a → Boolean] → a → Boolean | anyPass :: [a → Boolean] → a → Boolean
allPass: AND of predicates — all must pass. anyPass: OR — at least one must pass. Truthy results are normalized to Boolean.allPass
[isPos, isEven]
4
→
✓ && ✓
=
true
[isPos, isEven]
3
→
✓
✗
=
false
anyPass
[isPos, isEven]
3
→
✓
✗
=
true
getEq
getEq :: (a → Boolean) → String → Any → Any → Boolean
Property equality: extract a field (type-checked by predicate), compare with
S.equals. Returns false on missing/wrong-typed keys.
isString
"type"
"strength"
{type: "strength"}
→
true
getEq(isString)("type")("strength")({type: "strength"}) // => true
getEq(isString)("type")("cardio")({type: "strength"}) // => false
getEq(isString)("type")("cardio")({type: "strength"}) // => false
findEq
findEq :: (a → Boolean) → String → Any → [Object] → Maybe Object
Find-by-property: returns
Just(firstMatch) or Nothing. Combines getEq with S.find.
{id: 1}
{id: 2}
{id: 3}
findEq(isInt)("id")(2)
→
Just({id: 2})
pluck
pluck :: (a → Boolean) → String → [Object] → [Maybe a]
Extracts a named property from each object in an array. Each result is a
Maybe — missing/wrong-typed keys become Nothing.
{name:"A"}
{name:"B"}
{x: 1}
pluck(isString)("name")
Just("A")
Just("B")
Nothing
zipObj
zipObj :: [String] → [a] → StrMap a
Zips a list of keys and a list of values into an object. Truncated to the shorter list.
"a"
"b"
"c"
+
1
2
3
↓
{a: 1, b: 2, c: 3}
map2
map2 :: Functor f ⇒ (a → b) → f (f a) → f (f b)
Maps over a nested functor — two levels deep.
map(map(f)).
[[1, 2], [3, 4]]
map2(x => x * 10)
[[10, 20], [30, 40]]
parallelAp
parallelAp :: (a → b) → (a → c) → a → [b, c]
Applies two functions to the same value, returns both results in an array. Like
substitution but collects into a pair instead of composing.
x = 5
double(5) = 10
square(5) = 25
[10, 25]
parallelAp(double)(square)(5) // => [10, 25]
Effects
tap
tap :: (a → Any) → a → a
Runs a side-effect function, then returns the original value unchanged. The classic "peek" combinator for debugging or logging inside a pipe.
42
→
tap(console.log)
→logs 42
42
(unchanged)
pipe([parse, tap(console.log), validate, render]) // inspect mid-pipe
encaseStorage
encaseStorage :: (() → a) → Either Error a
Wraps a throwing thunk into an
Either. Success → Right(value), exception → Left(error). Used for localStorage operations that may fail in private browsing.
() => 42
→
Right(42)
() => throw "boom"
→
Left("boom")
readStorage / writeStorage
readStorage :: String → () → String|null | writeStorage :: String → String → Either Error ()
readStorage returns a thunk that reads localStorage. writeStorage wraps setItem in S.encase — private browsing throws, this catches it as Left.
readStorage("key")
()
→
"stored_value"
writeStorage("key")
"data"
→
Right(undefined)
entriesOf
entriesOf :: StrMap Any → [[String, Any]]
The only iterator for heterogeneous StrMaps. Sanctuary folds throw on mixed-type values; this wraps
Object.entries safely via def.
{a: 1, b: "x"}
→
entriesOf
→
[["a", 1], ["b", "x"]]
rafThrottle
rafThrottle :: ((...args) → void) → ((...args) → void)
Throttles a function to one call per animation frame. Internal
requestAnimationFrame bookkeeping — the only mutable let lives inside the closure.
call 1
→
scheduled
call 2
→
dropped
call 3
→
dropped
🎬 next frame
→
call 1 executes
call 4
→
scheduled
replaceFirst / replaceNamed
replaceFirst :: RegExp → String → String → String
replaceNamed :: String → String → String → String
replaceNamed :: String → String → String → String
Curried string replacement.
replaceFirst takes a regex, replaceNamed takes a literal string. Both replace the first occurrence.
replaceFirst(/world/)
"Earth"
"hello world"
→
"hello Earth"
replaceNamed("{name}")
"Ana"
"Hi {name}!"
→
"Hi Ana!"