Swift package

swift-function-composition

Nominal Swift functions that compose while preserving effects and isolation

Give composition a type the compiler can reason about

Raw closures are excellent for one-off work, but overloads become fragile when a pipeline must account for async, throws, sendability, or actor isolation. Nominal wrappers make those effects part of the composed value’s type.

let isNotZero = SyncFunc<Int, Bool> { $0 != 0 }
let describe = SyncFunc<Bool, String> {
  $0 ? "true" : "false"
}

let description = describe <<< isNotZero
let result = description(10)

Choose the surface that reads naturally in the target: operators, .compose(...) methods, compose(...), or forward pipe(...). All wrappers support both run(with:) and callAsFunction.

Preserve the strongest effect

Composition upgrades monotonically: sync plus async becomes async; nonthrowing plus throwing becomes throwing; compatible sendable and main-actor functions preserve main-actor isolation. Different typed failures become Either<Left, Right> instead of being silently erased.

let load = SendableAsyncThrowingFunc<Int, Bool, LoadError> {
  try await client.loadFlag($0)
}

let render = SendableSyncFunc<Bool, String> {
  $0 ? "Enabled" : "Disabled"
}

let loadDescription = render <<< load

Swift 6.1 package traits let each target opt into only the surfaces it uses: NominalTypes, Operators, Methods, Functions, and Currying.

Effect shapeWrapper family
SynchronousSyncFunc / SendableSyncFunc / MainActorSyncFunc
ThrowingSyncThrowingFunc<Input, Output, Failure>
AsynchronousAsyncFunc and sendable or main-actor variants
Async throwingAsyncThrowingFunc<Input, Output, Failure>

The same pipeline can be spelled with <<<, .compose(...), compose(...), or forward pipe(...). All nominal wrappers support run(with:) and callAsFunction; enable Currying only when curry, uncurry, or flip makes the function shape clearer.