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

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]))

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)

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

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!)

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
isString42false
isBooleantruetrue
isBoolean0false
isObject{}true
isObject[]false
isInt3true
isInt3.5false
isFiniteNumber3.5true
isFiniteNumberNaNfalse
isNonEmptyStr"x"true
isNonEmptyStr""false
isNonNullable"x"true
isNonNullablenullfalse
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

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

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
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!"