FP is a Swift library that brings functional programming patterns to your codebase in a composable, type-safe way. It extends Swift's built-in types (Optional, Result, Array, Publisher, async/await Task, AsyncSequence) and introduces new data structures that make common patterns — error handling, dependency injection, state threading, validation — explicit, predictable, and easy to test.
The library draws from Haskell and Scala Cats conventions and is designed to be used incrementally: start with just the core extensions and adopt more as your comfort grows.
- Learning Resources
- Installation
- Library Overview
- Joining things together (Semigroup)
- Neutral element when joining (Monoid)
- Map (Functor)
- Zip / Apply (Applicative)
- FlatMap (Monad)
- Fold (Foldable)
- Traverse (Traversable)
- Alternative (Choice)
- Comonad (Extend)
- Functional Getter / Setter for Structs (Lens)
- Functional Getter / Setter for Enums (Prism)
- Assembling Optics (AffineTraversal)
- Identity Optic (.id)
- Safe Collection Access ([safe:], [id:], and ix)
- Bidirectional Conversions (Iso)
- Deriving Optics with Macros (@Lenses and @Prisms)
- Composing Transformations (Endo)
- Cost-Free Mutations (EndoMut)
- SumType2 — Shared Interface for Two-Case Types
- Concurrency: Sendable-First
- Utilities
- Operator Reference
- Types
- Contributing
- Testing
- Platform Support
- License
New to functional programming? These are some of the best starting points:
- Functors, Applicatives, and Monads in Pictures — a visual, intuition-first introduction to the core concepts
- Learn You a Haskell for Great Good! — a beginner-friendly free book that explains the ideas behind this library
The repository ships with an Xcode Playground that lets you experiment with every concept covered in this README — Functor, Applicative, Monad, Monoid, Optics, Transformers, and more — one page at a time.
To open it:
- Open
Examples/Playground/Sandbox.xcworkspacein Xcode (the workspace, not the playground file directly — the workspace resolves the FP package dependency). - Select the Sandbox scheme and set the destination to My Mac.
- Build the scheme (⌘B) so the playground can resolve the FP module.
- In the Project Navigator, expand Sandbox.playground to see the numbered pages (01 – Functor, 02 – Applicative, …).
- Navigate to the page you want, uncomment the
learn(…)call at the bottom of the section you want to run, and execute the playground.
Each page contains ready-to-run functions with inline result comments. Uncomment one learn(…) call at a time to see its output in the console or inline results sidebar.
FP is a Swift Package Manager library and is designed to be modular: import only what you need. Each module builds on the previous one, so you can start small and expand.
// Package.swift
dependencies: [
.package(url: "https://github.com/luizmb/FP.git", from: "1.0.0")
]Adds @Lenses and @Prisms macros that generate Lens and Prism optics directly from type declarations. Requires Swift 6.2+.
The minimum you need. Adds several functional operations to Swift's built-in types — Optional, Result, Array, Combine's Publisher, and Swift Concurrency's AsyncSequence and more.
Adds custom symbolic operators for all CoreFP types. Using operators is entirely optional — every operator has a named function equivalent in CoreFP — but they allow a more concise, expression-oriented style.
Before adding this module, check your codebase for existing definitions of these symbols. Some (like
<>,>>>,|>, or^) are used in other libraries and could cause conflicts or ambiguity errors at the call site.
Adds new types that are common in functional languages but absent from Swift's standard library, such as Either<A, B>, Validation<E, A>, Reader<Environment, Output>, Stateful<S, A>, Writer<Log, A>.
Provides the same operator sugar as CoreFPOperators, but for the types in DataStructure. This module depends on both DataStructure and CoreFPOperators, and is only useful when both are present.
| You want | Import |
|---|---|
| Functional operations on built-in types only | CoreFP |
| The above plus symbolic operators | CoreFP + CoreFPOperators |
| Built-in types + additional data structures | CoreFP + DataStructure |
| Everything, with operator syntax | FP |
Macro-derived Lens and Prism optics |
FPMacros |
The FP umbrella product re-exports all four modules, so a single line covers everything:
import FPOr import selectively:
import CoreFP
import CoreFPOperators
import DataStructure
import DataStructureOperatorsAdd the chosen products to your target in Package.swift:
.target(
name: "MyTarget",
dependencies: [
.product(name: "FP", package: "FP") // or any individual module
]
)A Semigroup is any type where two values can be combined into one value of the same type. You already know several semigroups from everyday Swift:
String.combine("Hello, ", "World!") // "Hello, World!"
Array.combine([1, 2], [3, 4]) // [1, 2, 3, 4]The only rule is that combining must be associative — it shouldn't matter how you group the operations, only the order:
// These two must always be equivalent:
String.combine(String.combine("a", "b"), "c") // "abc"
String.combine("a", String.combine("b", "c")) // "abc"This library defines a Semigroup protocol, implemented by String, Array, Optional, Dictionary, Set, Result, and numeric types like Int, Double, and CGFloat, as well as Bool — but more on those in a moment. You can also make your own types conform to it by implementing combine.
Curiosity: lasagna is a semigroup, because putting one lasagna on top of another gives you lasagna.
sconcat reduces a non-empty sequence using combine:
sconcat("Hello", [", ", "World", "!"]) // "Hello, World!"
sconcat([1, 2], [[3, 4], [5, 6]]) // [1, 2, 3, 4, 5, 6]<> is the infix operator for combine:
"Hello, " <> "World!" // "Hello, World!"
[1, 2] <> [3, 4] // [1, 2, 3, 4]A Monoid is a semigroup with one extra requirement: there must be a neutral element (called identity) that leaves any value unchanged when combined with it — regardless of which side it appears on.
"" <> "hello" // "hello" — empty string is the identity for String
"hello" <> "" // "hello"
[] <> [1, 2] // [1, 2] — empty array is the identity for Array
[1, 2] <> [] // [1, 2]You can access the identity through the static property:
String.identity // ""
[Int].identity // []mconcat collapses an entire array using combine, starting from identity:
mconcat(["Hello", ", ", "World", "!"]) // "Hello, World!"
mconcat([[1, 2], [3], [4, 5]]) // [1, 2, 3, 4, 5]
mconcat([String]()) // "" — empty input returns identityNumbers and Booleans
Most numeric types can be combined in more than one way — you can add them or multiply them — so there isn't a single obvious Monoid instance for Int. Swift also doesn't allow the same type to conform to a protocol twice.
This library solves that with lightweight wrapper types:
// Addition — identity is 0
Int.Monoids.Sum.combine(3, 4) // Sum(7)
Int.Monoids.Sum.identity // Sum(0)
mconcat([1, 2, 3] as [Int.Monoids.Sum]).rawValue // 6
// Multiplication — identity is 1
Int.Monoids.Product.combine(3, 4) // Product(12)
Int.Monoids.Product.identity // Product(1)
mconcat([2, 3, 4] as [Int.Monoids.Product]).rawValue // 24
// Minimum — identity is Int.max (any value beats it)
Int.Monoids.Min.combine(7, 3) // Min(3)
Int.Monoids.Min.identity.rawValue // Int.max
mconcat([5, 1, 9] as [Int.Monoids.Min]).rawValue // 1
// Maximum — identity is Int.min (any value beats it)
Int.Monoids.Max.combine(7, 3) // Max(7)
Int.Monoids.Max.identity.rawValue // Int.min
mconcat([5, 1, 9] as [Int.Monoids.Max]).rawValue // 9The same pattern applies to UInt, Float, Double, CGFloat, and all other numeric types. Float literals work too:
Double.Monoids.Sum.combine(1.5, 2.5) // Sum(4.0)
mconcat([1.0, 2.5, 0.5] as [Double.Monoids.Sum]).rawValue // 4.0
Double.Monoids.Min.combine(2.5, 1.1) // Min(1.1)
Double.Monoids.Max.combine(2.5, 1.1) // Max(2.5)SIMD vectors get the same four wrappers via SIMDMonoid, operating element-wise on each lane. Integer scalars use wrapping arithmetic (&+, &*) while floating-point scalars use standard arithmetic:
// Element-wise sum — identity is the zero vector
let a = SIMD4<Int>.Monoids.Sum(SIMD4(1, 2, 3, 4))
let b = SIMD4<Int>.Monoids.Sum(SIMD4(10, 20, 30, 40))
SIMD4<Int>.Monoids.Sum.combine(a, b).rawValue // SIMD4(11, 22, 33, 44)
// Element-wise product — identity is the ones vector
SIMD2<Float>.Monoids.Product.combine(
.init(SIMD2(2.0, 3.0)),
.init(SIMD2(4.0, 5.0))
).rawValue // SIMD2(8.0, 15.0)
// Element-wise min / max
let v = [SIMD2(5, 9), SIMD2(1, 3), SIMD2(8, 2)].map { SIMD2<Int>.Monoids.Min($0) }
mconcat(v).rawValue // SIMD2(1, 2)All SIMD sizes from SIMD2 through SIMD64 are supported for every SIMDMonoidScalar type (Int, Int8–Int64, UInt–UInt64, Float, Double).
Bool works the same way:
// Conjunction (&&) — identity is true
Bool.Monoids.And.combine(.init(true), .init(false)) // And(false)
Bool.Monoids.And.identity // And(true)
mconcat([Bool.Monoids.And(true), .init(true), .init(false)]).rawValue // false
// Disjunction (||) — identity is false
Bool.Monoids.Or.combine(.init(false), .init(true)) // Or(true)
Bool.Monoids.Or.identity // Or(false)
mconcat([Bool.Monoids.Or(false), .init(false), .init(true)]).rawValue // true
// Exclusive disjunction (!=) — identity is false
Bool.Monoids.Xor.combine(.init(true), .init(false)) // Xor(true)
Bool.Monoids.Xor.combine(.init(true), .init(true)) // Xor(false)
Bool.Monoids.Xor.identity // Xor(false)
mconcat([Bool.Monoids.Xor(true), .init(false), .init(true)]).rawValue // falseOptional
Optional<A> is a Semigroup when A is a Semigroup, and a Monoid when A is a Monoid. This matches Haskell's Maybe instance exactly: both present → combine the wrapped values; one nil → return the non-nil side; both nil → nil. The identity is .none.
let a: String? = "hello"
let b: String? = " world"
Optional<String>.combine(a, b) // Optional("hello world") — both present: combine
Optional<String>.combine(a, .none) // Optional("hello") — only left present
Optional<String>.combine(.none, b) // Optional(" world") — only right present
Optional<String>.combine(.none, .none) // nil
Optional<String>.identity // nil
a <> b // Optional("hello world") — operator
mconcat([a, .none, b]) // Optional("hello world")
mconcat([.none, .none] as [String?]) // nil — identityResult
Result<Success, Failure> has no canonical Monoid in Haskell's base — Either faces the same problem there. This library provides four explicit newtype wrappers inside Result.Monoids so you name your intent rather than relying on an arbitrary default:
| Wrapper | Bias | When both sides match |
|---|---|---|
Optimistic |
success wins | only successes combine; two failures keep the left; Failure need not be Semigroup |
OptimisticCombining |
success wins | both sides combine; identity is .failure(Failure.identity) |
Pessimistic |
failure wins | only failures combine; two successes keep the left; Success need not be Semigroup |
PessimisticCombining |
failure wins | both sides combine; identity is .success(Success.identity) |
typealias R = Result<String, String>
// Optimistic — success wins; failures fall through; two failures keep the left
R.Monoids.Optimistic(.success("hello")) <> R.Monoids.Optimistic(.success(" world")) // .success("hello world")
R.Monoids.Optimistic(.success("hello")) <> R.Monoids.Optimistic(.failure("oops")) // .success("hello")
R.Monoids.Optimistic(.failure("e1")) <> R.Monoids.Optimistic(.failure("e2")) // .failure("e1")
// OptimisticCombining — success wins; both sides combine when matching; Monoid
R.Monoids.OptimisticCombining(.success("hello")) <> R.Monoids.OptimisticCombining(.success(" world")) // .success("hello world")
R.Monoids.OptimisticCombining(.failure("bad")) <> R.Monoids.OptimisticCombining(.failure(" stuff")) // .failure("bad stuff")
R.Monoids.OptimisticCombining.identity // .failure("") — Failure.identity
// Pessimistic — failure wins; successes fall through; two successes keep the left
R.Monoids.Pessimistic(.failure("bad")) <> R.Monoids.Pessimistic(.failure(" stuff")) // .failure("bad stuff")
R.Monoids.Pessimistic(.failure("bad")) <> R.Monoids.Pessimistic(.success("ok")) // .failure("bad")
R.Monoids.Pessimistic(.success("ok")) <> R.Monoids.Pessimistic(.success("ok2")) // .success("ok")
// PessimisticCombining — failure wins; both sides combine when matching; Monoid
R.Monoids.PessimisticCombining(.success("hello")) <> R.Monoids.PessimisticCombining(.success(" world")) // .success("hello world")
R.Monoids.PessimisticCombining(.failure("bad")) <> R.Monoids.PessimisticCombining(.success("ok")) // .failure("bad")
R.Monoids.PessimisticCombining.identity // .success("") — Success.identity
// mconcat is available for the two Monoid variants (OptimisticCombining / PessimisticCombining)
mconcat([
R.Monoids.OptimisticCombining(.success("hello")),
R.Monoids.OptimisticCombining(.failure("oops")),
R.Monoids.OptimisticCombining(.success(" world")),
]) // .success("hello world") — successes win and combineAn empty tray of lasagna would be the identity element — making lasagna a monoid too.
A Functor is any container-like structure whose contents you can transform without unwrapping it. The transformation is applied to the value inside, and the container comes back with the new value in it — shape preserved, contents changed.
Swift already ships with this for the most common types:
[1, 2, 3].map { $0 * 2 } // [2, 4, 6]
Optional(5).map { $0 * 2 } // Optional(10)
Result<Int, any Error>.success(5).map { $0 * 2 } // .success(10)This library also provides fmap as a free function, which is useful for point-free composition:
fmap({ $0 * 2 }, Optional(5)) // Optional(10)Some containers have two type parameters, not one. Result<Success, Failure> is the obvious example: .map transforms the success side, and .mapError transforms the failure side.
In FP languages, we normally have a function .bimap to allow both sides to be transformed in one go. This library introduces this option as well.
let result: Result<Int, String> = .failure("not found")
result.bimap(
{ $0 * 2 }, // success path (not reached here)
{ "Error: \($0)" } // failure path
)
// .failure("Error: not found")
Result<Int, String>.success(21).bimap(
{ $0 * 2 },
{ "Error: \($0)" }
)
// .success(42)When you map over a container, the output type changes "in the same direction" as the function you provide — give it (Int) -> String and you get Container<String> back. This is covariance: the type parameter varies with the transform.
But some type parameters vary in the opposite direction, and those are called contravariant. The clearest example is a function's input. If you have a function (String) -> Bool (say, a validator), you can adapt it to also accept Int by first converting Int → String. You're not mapping the output — you're pre-processing the input. That "pre-processing the input" operation is called contramap.
This library makes the concept concrete via the Reader type (a wrapper around (Environment) -> Output). In a dependency injection context, contramapEnvironment lets a component that needs a specific sub-dependency be adapted to accept the whole root environment:
struct Dependencies {
var urlRequester: URLRequester
var jsonParser: JSONParser
var dateNow: DateNow
var dispatchQueueMain: DispatchQueueMain
}
// A reader scoped to just the URLRequester sub-dependency
let fetchItems: Reader<URLRequester, [Item]> = Reader { $0.get("/items") }
// contramapEnvironment zooms out: adapt it to accept the full Dependencies root
let fetchItemsFromRoot: Reader<Dependencies, [Item]> = fetchItems.contramapEnvironment(\.urlRequester)A Profunctor is a type that is covariant in one parameter and contravariant in another. Plain functions are the textbook example: (A) -> B can be mapped on the output (covariant in B) and contramapped on the input (contravariant in A). That makes functions profunctors.
dimap does both in one call — narrowing the environment and transforming the output in a single expression:
// A reader scoped to just the URLRequester
let checkReachability: Reader<URLRequester, Bool> = Reader { $0.isReachable }
// Narrow the input from Dependencies to URLRequester, and describe the Bool result as a String
let serviceStatus: Reader<Dependencies, String> = checkReachability.dimap(
\.urlRequester, // Dependencies → URLRequester (narrow the environment)
{ $0 ? "service online" : "service offline" } // Bool → String (describe the result)
)Symbolic operators are syntactic sugar — every one of them delegates to a named function in CoreFP. Use them when they make the code more readable, ignore them when they don't.
<£> maps a function over a container (function on the left):
{ $0 * 2 } <£> Optional(5) // Optional(10)
{ $0 * 2 } <£> [1, 2, 3] // [2, 4, 6]
{ $0 * 2 } <£> Result<Int, String>.success(5) // .success(10)
{ $0 * 3 } <£> Just(2).eraseToAnyPublisher() // publisher of 6<&> is the flipped version — container on the left:
Optional(5) <&> { $0 * 2 } // Optional(10)
[1, 2, 3] <&> { $0 * 2 } // [2, 4, 6]
Result<Int, String>.success(5) <&> { $0 * 2 } // .success(10)
Just(2).eraseToAnyPublisher() <&> { $0 * 3 } // publisher of 6£> replaces the contents with a constant (container on the left, value on the right):
Optional(5) £> "hello" // Optional("hello")
[1, 2, 3] £> "x" // ["x", "x", "x"]
Result<Int, String>.success(42) £> "done" // .success("done")
Just(2).eraseToAnyPublisher() £> "done" // publisher of "done"<£ is the flipped version — value on the left:
"hello" <£ Optional(5) // Optional("hello")
"x" <£ [1, 2, 3] // ["x", "x", "x"]
"done" <£ Result<Int, String>.success(42) // .success("done")
"done" <£ Just(2).eraseToAnyPublisher() // publisher of "done"Zip combines two containers into one container of pairs. The key insight is that the containers remain independent until you combine them:
zip([1, 2, 3], ["a", "b", "c"]) // [(1, "a"), (2, "b"), (3, "c")]
zip(Optional(1), Optional(2)) // Optional((1, 2))
zip(Optional(1), Optional<Int>.none) // nil — one nil means the pair is nilFor Array, zip pairs elements by index (the shorter array wins). For Optional, both values must be present for anything to come out. For Publisher, it waits until both have emitted and pairs them as they arrive.
Result doesn't have a stdlib zip, but this library adds it:
zip(Result<Int, String>.success(1), Result<Int, String>.success(2)) // .success((1, 2))
zip(Result<Int, String>.success(1), Result<Int, String>.failure("!")) // .failure("!")Apply
Apply is a related operation: what if the function itself is inside a container? apply unwraps both the function and the value, applies the function, and wraps the result back up:
Optional({ $0 * 2 }).apply(Optional(3)) // Optional(6)
Optional<(Int) -> Int>.none.apply(Optional(3)) // nilFor Array, apply gives every combination — each function applied to every value:
[{ $0 + 1 }, { $0 * 10 }].apply([1, 2]) // [2, 3, 10, 20]The relationship between zip and apply: apply is essentially zip followed by map. First zip the function-container with the value-container to get pairs, then map { (fn, value) in fn(value) } over the pairs. This library implements both, and internally they delegate to the same logic.
Types that support apply are called Applicatives. Every Monad is an Applicative, but not vice versa — Applicatives can't express sequential dependencies between steps (you'll need flatMap for that).
Parallel execution
Because zip and apply combine independent effects, they can run concurrently. When you zip two DeferredTask values, both start immediately and the result becomes available when the slower one finishes. flatMap, by contrast, can only start the second task after the first provides a value — it is inherently sequential.
Use zip when two effects don't depend on each other. Use flatMap when they do.
<*> applies a wrapped function to a wrapped value (function container on the left):
Optional({ $0 + 41 }) <*> Optional(1) // Optional(42)
[{ $0 * 2 }, { $0 * 3 }] <*> [1, 2] // [2, 4, 3, 6]
Result.success({ $0 * 2 }) <*> Result.success(21) // .success(42)
Just({ $0 + 1 }).eraseToAnyPublisher() <*> Just(41).eraseToAnyPublisher()*> sequences two effects and keeps the right result — the left effect still runs, but its value is discarded:
Optional(42) *> Optional("hello") // Optional("hello") — 42 ran, but only "hello" survives
[1, 2] *> ["a", "b"] // ["a", "b", "a", "b"]
Result<Int, String>.success(42) *> Result.success("hello") // .success("hello")<* keeps the left result instead:
Optional("hello") <* Optional(42) // Optional("hello")
["a", "b"] <* [1, 2] // ["a", "a", "b", "b"]
Result.success("hello") <* Result.success(42) // .success("hello")map transforms the value inside a container. But sometimes the transform itself produces a container, and you'd end up with a nested one:
Optional("42").map { Int($0) } // Optional<Optional<Int>> — nested, awkwardflatMap does the same thing, then flattens the result:
Optional("42").flatMap { Int($0) } // Optional<Int> — flat
Optional("xx").flatMap { Int($0) } // nilThis "apply a container-returning function, then flatten" is exactly what a Monad is. The name sounds academic but the idea is familiar: optional chaining (foo?.bar?.baz) is monadic thinking in disguise. Each ?. is a flatMap step that stops the chain if anything is nil.
The shape of flatMap is always the same:
func flatMap<B>(_ transform: (A) -> Container<B>) -> Container<B>But what it does depends entirely on the container — the same structure solves a different problem for each type.
Optional — failure propagation
Each step can fail, and the chain stops at the first nil:
func findUser(id: Int) -> User? { ... }
func findAddress(user: User) -> Address? { ... }
func city(from address: Address) -> String? { ... }
let result = findUser(id: 42)
.flatMap(findAddress)
.flatMap(city)
// String? — nil if any step failedArray — nondeterminism
Each step can return multiple results. flatMap collects all combinations:
[1, 2, 3].flatMap { [$0, $0 * 10] } // [1, 10, 2, 20, 3, 30]Think of it as "for each input element, generate zero or more output elements, then collect everything flat."
Publisher / DeferredTask — async sequencing
The second effect can't start until the first finishes and provides its value:
fetchUser(id: 42)
.flatMap { user in fetchPermissions(for: user) }
.flatMap { perms in loadDashboard(permissions: perms) }Unlike zip (which runs effects in parallel), flatMap is always serial. Step two depends on the result of step one — that's exactly when you reach for flatMap.
>>- is bind with the container on the left — same argument order as flatMap:
Optional("42") >>- { Int($0) } // Optional(42)
[1, 2] >>- { [$0, $0 * 10] } // [1, 10, 2, 20]
Result.success("2") >>- { Result.success(Int($0) ?? 0) } // .success(2)
Just(42).eraseToAnyPublisher() >>- { Just($0 * 2).eraseToAnyPublisher() }-<< is the flipped version — function on the left, container on the right:
{ Int($0) } -<< Optional("42") // Optional(42)>=> composes two "Kleisli arrows" — functions that return containers — into one:
let parseInt: (String) -> Int? = { Int($0) }
let doubleIt: (Int) -> Int? = { .some($0 * 2) }
let parseAndDouble = parseInt >=> doubleIt
parseAndDouble("21") // Optional(42)
parseAndDouble("xx") // nilThis is function composition for container-returning functions. <<< and >>> compose regular functions; >=> and <=< compose Kleisli arrows.
<=< is the right-to-left version:
let parseAndDouble = doubleIt <=< parseIntA Foldable is any structure you can collapse into a single value by visiting each element. Arrays are the obvious example, but Optional is foldable too — it has either zero or one element.
// withDefault — provide a fallback when a value is absent (curried, for composition)
withDefault(0)(Optional(42)) // 42
withDefault(0)(nil) // 0
// fold — collapse an Optional to a single type
Optional(5).fold(onNone: 0, onSome: { $0 * 2 }) // 10
(nil as Int?).fold(onNone: 0, onSome: { $0 * 2 }) // 0
// Static variant for point-free composition
let safeParse: (String) -> Int = { Int($0) }.map >>> Optional.fold(onNone: -1, onSome: id)
// foldMap — map each element to a Monoid, then combine them
Optional(3).foldMap { Int.Monoids.Sum($0) } // Sum(3)
(nil as Int?).foldMap { Int.Monoids.Sum($0) } // Sum(0) — identity
[1, 2, 3].foldMap { Int.Monoids.Sum($0) } // Sum(6)
// foldLeft / foldRight on Array (curried)
Array.foldLeft(0, +)([1, 2, 3, 4]) // 10 — (((0+1)+2)+3)+4
Array.foldRight(-, 0)([1, 2, 3]) // 2 — 1-(2-(3-0))
// toList — Optional as a zero-or-one list
Optional(42).toList // [42]
(nil as Int?).toList // []A Traversable is a structure you can map over with a function that produces a container, collecting all the containers into one. Think of it as "map, then flip the nesting."
The two key operations are:
traverse— map and flip at oncesequence— flip without mapping (the common case)
// Array<Optional> → Optional<Array>
// All must be present; one nil collapses the whole result
[Optional(1), Optional(2), Optional(3)].sequence() // Optional([1, 2, 3])
[Optional(1), nil, Optional(3)].sequence() // nil
["1", "2", "3"].traverse { Int($0) } // Optional([1, 2, 3])
["1", "x", "3"].traverse { Int($0) } // nil
// Array<Result> → Result<Array>
[Result<Int, MyError>.success(1), .success(2)].sequence() // .success([1, 2])
[Result<Int, MyError>.success(1), .failure(.err)].sequence() // .failure(.err)
// Optional<Array> → Array<Optional>
Optional([1, 2, 3]).sequence() // [Optional(1), Optional(2), Optional(3)]
(nil as [Int]?).sequence() // [nil]
// Optional<Result> → Result<Optional>
Optional(Result<Int, MyError>.success(42)).sequence() // .success(Optional(42))
(nil as Result<Int, MyError>?).sequence() // .success(nil)Alternative models a choice between two effects: try the first; if it's "empty" (nil, [], failure), fall back to the second.
// Optional — first non-nil wins
(nil as Int?) <|> Optional(3) // Optional(3)
Optional(1) <|> Optional(3) // Optional(1) — first wins if present
// Array — concatenation
[1, 2] <|> [3, 4] // [1, 2, 3, 4]
[] <|> [3, 4] // [3, 4]
// Result — first success wins
Result<Int, Error>.failure(err) <|> .success(3) // .success(3)
Result<Int, Error>.success(1) <|> .success(3) // .success(1)
// DeferredStream — second stream starts when first finishes
let a = DeferredStream<Int>.wrap(AsyncStream.just(1, 2))
let b = DeferredStream<Int>.wrap(AsyncStream.just(3, 4))
for await v in (a <|> b) { print(v) } // 1, 2, 3, 4
// DeferredTask<A?> — race: both start concurrently, first non-nil wins, other cancelled
let primary: DeferredTask<User?> = DeferredTask { await primaryAPI.find(id: 42) }
let fallback: DeferredTask<User?> = DeferredTask { await fallbackAPI.find(id: 42) }
let user = await (primary <|> fallback).run()
// DeferredTask<Result<A,E>> — race: both start concurrently, first .success wins
let fast: DeferredTask<Result<Data, Error>> = DeferredTask { await cdn.fetch(url) }
let slow: DeferredTask<Result<Data, Error>> = DeferredTask { await origin.fetch(url) }
let data = await (fast <|> slow).run()DeferredTask has no empty (a never-resolving task would deadlock), so <|> is only available on the transformer variants DeferredTask<A?> and DeferredTask<Result<A,E>>. For racing two tasks with no fallback semantics, use race:
// race — first-to-complete wins, other is cancelled (base DeferredTask<A>)
let fastest = await race(taskA, taskB).run()A Comonad is the dual of a Monad. While a Monad lets you inject values (pure) and extract context-dependent results (flatMap), a Comonad lets you extract the current value (extract) and extend a function over the whole context (extend / coflatMap).
The Writer type in this library is a Comonad:
// extract — pull out the value (dual of pure)
Writer(42, ["log"]).extract // 42
// coflatMap / extend — map a function over the entire writer context
Writer(21, ["step"]).coflatMap { w in w.value * 2 + w.log.count }
// Writer(43, ["step"]) — value: 21*2 + 1, log preserved
// duplicate — wrap the writer in another writer (dual of join)
Writer(42, ["log"]).duplicate // Writer(Writer(42, ["log"]), ["log"])->> extends a comonad (container on the left):
Writer(21, ["x"]) ->> { $0.value * 2 } // Writer(42, ["x"])<<- is the flipped version — function on the left:
{ $0.value * 2 } <<- Writer(21, ["x"]) // Writer(42, ["x"])Swift value types are immutable-by-default, which is great until you need to update a field several levels deep. The naïve approach requires unpacking the whole hierarchy:
var config = appState.config
var theme = config.theme
theme.colors.primary = .red
config.theme = theme
appState = AppState(config: config, ...) // tedious and error-proneA Lens solves this by pairing a getter and setter into a single composable value. It focuses on a specific field and lets you get, set, or transform it cleanly.
struct User { var name: String; var age: Int }
let nameLens = lens(\.name) // Lens<User, String>
let user = User(name: "Alice", age: 30)
nameLens.get(user) // "Alice"
nameLens.set(user, "Bob") // User(name: "Bob", age: 30)
nameLens.over { $0.uppercased() }(user) // User(name: "ALICE", age: 30)For let properties (where WritableKeyPath isn't available), supply the setter manually:
struct Person { let name: String; let age: Int }
let nameLens = lens(\.name) { Person(name: $1, age: $0.age) }Composing Lenses
The real power is composition. >>> chains two lenses into one that dives deeper into the structure:
struct Address { var city: String }
struct User { var address: Address; var name: String }
let addressLens = lens(\.address) // Lens<User, Address>
let cityLens = lens(\.city) // Lens<Address, String>
let userCityLens = addressLens >>> cityLens // Lens<User, String>
let user = User(address: Address(city: "New York"), name: "Alice")
userCityLens.get(user) // "New York"
userCityLens.set(user, "London") // User(address: Address(city: "London"), name: "Alice")Lenses also compose with Prisms — see Assembling Optics (AffineTraversal) for the full story.
compose — operator-free composition (CoreFP only, no operators needed)
If you import only CoreFP and not CoreFPOperators, use the compose method instead of >>>:
let userCityLens = addressLens.compose(cityLens) // Lens<User, String>All nine >>> / <<< overloads in CoreFPOperators delegate to compose, so the two forms are identical at runtime.
lift — in-place mutation with EndoMut
over returns a new S — it always copies. When S contains a large CoW buffer (an Array, Dictionary, etc.), even touching one element triggers an O(n) heap copy.
lift converts an EndoMut<A> (an in-place mutation of the focused value) into an EndoMut<S> (an in-place mutation of the whole) without copying S:
let ageReducer = EndoMut<Int> { $0 += 1 }
let personReducer: EndoMut<Person> = lens(\Person.age).lift(ageReducer)
var person = Person(name: "Alice", age: 30)
personReducer(&person) // person.age is now 31 — zero copies of PersonFor WritableKeyPath-backed lenses (those created with lens(\.property)), the entire operation is zero-copy — Swift's modify coroutine provides direct inout access to the field. For manually constructed lenses the focused value is copied once; S itself is never CoW-copied. See Lifting EndoMut through optics for the full story.
Identity lens
Lens<A, A>.id is the lens where the whole and the part are the same — get returns the value unchanged, set replaces it entirely:
Lens<Int, Int>.id.get(42) // 42
Lens<Int, Int>.id.set(0, 42) // 42It serves as the neutral element for lens composition: anyLens >>> Lens<A, A>.id == anyLens.
^ lifts a WritableKeyPath into a Lens directly:
let ageLens: Lens<User, Int> = ^\User.age
let nameLens: Lens<User, String> = ^\User.nameComposition works the same way with the lifted lenses:
let userCityLens = ^\User.address >>> ^\Address.city // Lens<User, String>For let properties, ^ returns a partial builder waiting for the setter:
let nameLens: Lens<Person, String> = (^\Person.name) { Person(name: $1, age: $0.age) }<<< is the right-to-left version of >>>:
// These are equivalent:
let cityFirst = ^\User.address >>> ^\Address.city
let cityFirst2 = ^\Address.city <<< ^\User.addressBridging to SwiftUI Binding (Apple platforms, requires CoreFP)
A Binding<Root> combined with a Lens<Root, Focus> produces a Binding<Focus>:
@State var user = User(name: "Alice", age: 30)
let nameLens: Lens<User, String> = lens(\.name)
TextField("Name", text: $user[optic: nameLens])See Binding for the full bridge API.
Just as a glass prism refracts a beam of white light into its constituent wavelengths — revealing all the colours hiding inside the one —, in Functional Programming a Prism has nothing to do with a progressive rock band album art, but instead it refracts a sum type (enum) into its individual cases, letting you focus on the one you care about. Where a Lens works on structs (where every field is always present), a Prism works on enums (where only one case is active at a time). It focuses on a specific case and lets you extract or construct values for that case.
It has three operations:
preview— tries to extract the associated value; returnsnilif the enum is a different casereview— constructs an enum value from the focused typeover— applies a transform to the focused value; leaves the structure unchanged if the case is inactive
enum Shape {
case circle(Double)
case rectangle(Double, Double)
}
let circlePrism = prism(
preview: { if case .circle(let r) = $0 { return r } else { return nil } },
review: Shape.circle
)
circlePrism.preview(.circle(5.0)) // Optional(5.0)
circlePrism.preview(.rectangle(3, 4)) // nil
circlePrism.review(7.0) // Shape.circle(7.0)
circlePrism.over { $0 * 2 }(.circle(5)) // Shape.circle(10.0)
circlePrism.over { $0 * 2 }(.rectangle(3, 4)) // Shape.rectangle(3, 4) — unchangedIf your enum has optional-returning computed properties, the keyPath shorthand is more concise:
extension Shape {
var circleRadius: Double? {
guard case .circle(let r) = self else { return nil }
return r
}
}
let circlePrism: Prism<Shape, Double> = prism(\.circleRadius, review: Shape.circle)set
set replaces the focused associated value if the prism matches the current case; it is a no-op otherwise:
circlePrism.set(.circle(3.14), 5.0) // Shape.circle(5.0)
circlePrism.set(.rectangle(1, 2), 5.0) // Shape.rectangle(1, 2) — unchangedIdentity prism
Prism<A, A>.id is the prism where preview always succeeds and review is the identity:
Prism<Int, Int>.id.preview(42) // Optional(42)
Prism<Int, Int>.id.review(42) // 42lift — in-place mutation with EndoMut
Like Lens.lift, Prism.lift converts an EndoMut<A> into an EndoMut<S>. When the prism doesn't match the current case, the resulting EndoMut is a no-op and S is left unchanged:
let doubleRadius = EndoMut<Double> { $0 *= 2 }
let shapeReducer: EndoMut<Shape> = circlePrism.lift(doubleRadius)
var shape = Shape.circle(5.0)
shapeReducer(&shape) // Shape.circle(10.0)
var rect = Shape.rectangle(3, 4)
shapeReducer(&rect) // Shape.rectangle(3, 4) — no-op, no copiesBecause Swift has no inout access to enum case values, the associated value is always copied once. The outer Shape (or whatever S is) is kept inout and is never CoW-copied.
Prisms compose with other prisms and with lenses — see Assembling Optics (AffineTraversal) for the full story.
>>> and <<< work for Prism composition the same way as for Lens:
// Prism >>> Prism → Prism
let deepCasePrism = outerPrism >>> innerPrism
// Prism >>> Lens → AffineTraversal
let cityInLoggedInUser = loggedInPrism >>> ^\User.address >>> ^\Address.cityBridging to SwiftUI Binding (Apple platforms, requires CoreFP)
Binding[optic: prism] returns Binding<A>? — nil when the focused case is inactive:
@State var sheet: Sheet = .settings(Settings())
if let settingsBinding = $sheet[optic: settingsPrism] {
SettingsView(settings: settingsBinding)
}See Binding for the full bridge API.
Assembling an AffineTraversal is like aligning a lens and a prism in a telescope — each brings its own focus, and together they reach deeper into a structure than either could alone. The AffineTraversal is the optic you get whenever a focus may or may not exist, combining optional extraction with structural update.
It has three operations:
preview— tries to extract the focused value; returnsnilif the focus is absentset— updates the focused value if present; leaves the structure unchanged if absentover— applies a transform to the focused value if present
Lens >>> Prism → AffineTraversal
Start with a struct and drill down to a field that is itself an enum case:
enum Shape { case circle(Double); case rectangle(Double, Double) }
struct Canvas { var shape: Shape }
let shapeLens: Lens<Canvas, Shape> = lens(\.shape)
let circlePrism: Prism<Shape, Double> = prism(
preview: { if case .circle(let r) = $0 { return r } else { return nil } },
review: Shape.circle
)
// Lens >>> Prism = AffineTraversal<Canvas, Double>
let circleRadiusTraversal = shapeLens >>> circlePrism
let canvas = Canvas(shape: .circle(5.0))
circleRadiusTraversal.preview(canvas) // Optional(5.0)
circleRadiusTraversal.set(canvas, 10.0) // Canvas(shape: .circle(10.0))
circleRadiusTraversal.over { $0 * 2 }(canvas) // Canvas(shape: .circle(10.0))
let rectCanvas = Canvas(shape: .rectangle(3, 4))
circleRadiusTraversal.preview(rectCanvas) // nil
circleRadiusTraversal.set(rectCanvas, 10.0) // Canvas(shape: .rectangle(3, 4)) — unchangedPrism >>> Lens → AffineTraversal
Go the other direction: start with an enum case and drill further into the associated value:
enum App { case loggedIn(User); case guest }
struct User { var address: Address }
struct Address { var city: String }
let loggedInPrism: Prism<App, User> = prism(
preview: { if case .loggedIn(let u) = $0 { return u } else { return nil } },
review: App.loggedIn
)
let cityLens: Lens<User, String> = lens(\.address) >>> lens(\.city)
// Prism >>> Lens = AffineTraversal<App, String>
let cityInLoggedInUser = loggedInPrism >>> cityLens
cityInLoggedInUser.preview(.loggedIn(User(address: Address(city: "Paris")))) // Optional("Paris")
cityInLoggedInUser.preview(.guest) // nil
cityInLoggedInUser.set(.loggedIn(User(address: Address(city: "Paris"))), "London")
// .loggedIn(User(address: Address(city: "London")))Building a longer pipeline
Because all three optic types compose via >>>, you can chain freely:
let radiusTraversal = loggedInPrism >>> lens(\.avatar) >>> circlePrism
// AffineTraversal<App, Double><<< is the right-to-left version:
let radiusTraversal2 = circlePrism <<< lens(\.avatar) <<< loggedInPrismBridging to SwiftUI Binding (Apple platforms, requires CoreFP)
Binding[optic: affineTraversal] returns Binding<A>? — nil when the focus is absent:
@State var app: App = .loggedIn(User(...))
if let cityBinding = $app[optic: loggedInPrism >>> ^\User.address >>> ^\Address.city] {
TextField("City", text: cityBinding)
}See Binding for the full bridge API.
affineTraversal from a WritableKeyPath to an optional
When a stored property is itself optional, a WritableKeyPath<S, A?> already captures both get and set — lift it directly into an AffineTraversal:
struct Profile { var nickname: String? }
let nicknameFocus = affineTraversal(\Profile.nickname) // AffineTraversal<Profile, String>
nicknameFocus.preview(Profile(nickname: "ace")) // Optional("ace")
nicknameFocus.preview(Profile(nickname: nil)) // nil
nicknameFocus.set(Profile(nickname: nil), "ace") // Profile(nickname: Optional("ace"))For concrete collection types this is the subscript form of ix:
affineTraversal(\[Int][safe: 2]) // identical to [Int].ix(2)lift — in-place mutation with EndoMut
AffineTraversal.lift works the same way as Lens.lift and Prism.lift. When the focus is absent the resulting EndoMut is a no-op:
let scaleRadius = EndoMut<Double> { $0 *= 2 }
let canvasReducer: EndoMut<Canvas> = circleRadiusTraversal.lift(scaleRadius)
var canvas = Canvas(shape: .circle(5.0))
canvasReducer(&canvas) // Canvas(shape: .circle(10.0))The copy cost depends on which optic sits at each link of the chain. See Lifting EndoMut through optics for the full breakdown.
Every optic type has a static .id property constrained to S == A — the optic where the whole and the part are the same type. These are the neutral elements for composition.
Lens<Int, Int>.id.get(42) // 42
Lens<Int, Int>.id.set(0, 42) // 42
Prism<Int, Int>.id.preview(42) // Optional(42)
Prism<Int, Int>.id.review(42) // 42
AffineTraversal<Int, Int>.id.preview(42) // Optional(42)
AffineTraversal<Int, Int>.id.set(0, 42) // 42
Iso<Int, Int>.id.get(42) // 42
Iso<Int, Int>.id.reverseGet(42) // 42
Iso<Int, Int>.id.asLens // equivalent to Lens<Int, Int>.idIso<A, A>.id is the strongest — it downcasts to all weaker forms via .asLens, .asPrism, .asAffineTraversal.
Collections gain a [safe:] subscript that returns Element? instead of crashing on out-of-bounds access. For MutableCollection the setter is available and is a no-op when the index is out of bounds or the new value is nil:
let xs = [10, 20, 30]
xs[safe: 1] // Optional(20)
xs[safe: 9] // nil
var ys = [10, 20, 30]
ys[safe: 1] = 99 // [10, 99, 30]
ys[safe: 9] = 99 // no-op — out of bounds
ys[safe: 1] = nil // no-op — nil is ignoredCollections of Identifiable elements gain an [id:] subscript that returns the first element whose id matches, or nil. For RangeReplaceableCollection (Array, ArraySlice, ContiguousArray) the setter is available with Dictionary-style add/remove semantics:
struct User: Identifiable { let id: Int; let name: String }
let users = [User(id: 1, name: "Alice"), User(id: 2, name: "Bob")]
users[id: 2] // Optional(User(id: 2, name: "Bob"))
users[id: 9] // nil
var roster = users
roster[id: 2] = User(id: 2, name: "Robert") // replace in place
roster[id: 3] = User(id: 3, name: "Carol") // append to end (id not found)
roster[id: 1] = nil // remove
roster[id: 9] = nil // no-op (not present)
roster[id: 2] = User(id: 99, name: "X") // no-op (id-mismatch guard)Setter dispatch table:
newValue |
existing match | result |
|---|---|---|
nil |
yes | remove |
nil |
no | no-op |
v where v.id ==id |
yes | replace in place |
v where v.id ==id |
no | append to end |
v where v.id !=id |
— | no-op (id-mismatch guard) |
The id-mismatch guard protects against accidental swaps — assigning an element whose id doesn't match the subscript key is almost always a bug. Lookup is linear (first(where:) / firstIndex(where:)); for hot paths with large collections, consider keying by a Dictionary.
The setter requires RangeReplaceableCollection because the add/remove cases must change the collection's count — MutableCollection alone can only replace in place. Array and its slice variants cover the practical targets.
ix lifts safe element access into an AffineTraversal, making it composable with the rest of the optics pipeline. It is a static method on the collection type so the compiler always knows which collection is being addressed.
By integer index — any MutableCollection:
[Int].ix(1).preview([10, 20, 30]) // Optional(20)
[Int].ix(9).preview([10, 20, 30]) // nil
[Int].ix(1).set([10, 20, 30], 99) // [10, 99, 30]
[Int].ix(0).over({ $0 * 2 })([10, 20, 30]) // [20, 20, 30]By Identifiable ID — MutableCollection where Element: Identifiable:
struct Item: Identifiable { let id: Int; var name: String }
let items = [Item(id: 1, name: "A"), Item(id: 2, name: "B")]
[Item].ix(id: 2).preview(items)?.name // "B"
[Item].ix(id: 2).set(items, Item(id: 2, name: "Z")).map(\.name) // ["A", "Z"]
[Item].ix(id: 99).set(items, Item(id: 99, name: "X")) // items — no-opBy dictionary key — Dictionary:
let dict = ["a": 1, "b": 2]
[String: Int].ix(key: "b").preview(dict) // Optional(2)
[String: Int].ix(key: "z").preview(dict) // nil
[String: Int].ix(key: "b").set(dict, 99) // ["a": 1, "b": 99]
[String: Int].ix(key: "z").set(dict, 99) // dict — no-op (key absent)Composing ix with other optics
Because ix returns an AffineTraversal it slots into any >>> pipeline:
struct Team { var members: [Item] }
// AffineTraversal<Team, String>
let memberNameFocus = lens(\.members) >>> [Item].ix(id: 2) >>> lens(\.name)
let team = Team(members: items)
memberNameFocus.preview(team) // Optional("B")
memberNameFocus.set(team, "Updated") // updates the member whose id == 2Zero-copy element mutation with lift
ix.lift mutates the element at the focused index in place without CoW-copying the collection:
// Array: inout subscript — zero copies of the collection buffer
let itemReducer = EndoMut<Item> { $0.name = $0.name.uppercased() }
let teamReducer: EndoMut<Team> =
lens(\.members)
.compose([Item].ix(id: 2))
.lift(itemReducer)
teamReducer(&team) // only the one Item is mutated; the [Item] buffer is not copiedix on MutableCollection passes inout collection[index] directly to the closure — Swift's subscript modify coroutine makes this genuinely zero-copy. ix on Dictionary copies the Value once (because the dictionary subscript returns Value?, not inout Value), but the dictionary buffer itself is not copied.
Subscripts vs ix — two faces of the same concept
For concrete collection types, [Int].ix(2) is equivalent to affineTraversal(\[Int][safe: 2]), and [User].ix(id: 2) mirrors [User][id: 2] for the replace-in-place case. Use the subscripts ([safe:], [id:]) for direct element access; use ix when you need an optic you can compose. ix(id:) is limited to replace-in-place semantics — for the add-or-remove behaviour, use the [id:] subscript directly.
An Iso<S, A> is a pair of total, invertible functions: get: (S) -> A and reverseGet: (A) -> S. Unlike a Lens, there is no notion of "focusing on a part" — the whole structure converts losslessly in both directions.
let metersToFeet = iso(get: { $0 * 3.28084 }, reverseGet: { $0 / 3.28084 }) // Iso<Double, Double>
metersToFeet.get(1.0) // 3.28084
metersToFeet.reverseGet(3.28084) // 1.0
metersToFeet.reverse // Iso<Double, Double> with get/reverseGet swappedover applies a transform through the round-trip:
metersToFeet.over { $0 + 10 }(1.0) // convert to feet, add 10, convert backEvery Iso is also a valid Lens, Prism, and AffineTraversal — use .asLens, .asPrism, or .asAffineTraversal to downcast when needed. The identity iso Iso<A, A>.id is the strongest neutral element — see Identity Optic.
Iso as Monoid — endomorphism isos (Iso<A, A>) form a Monoid under composition. Use mconcat to chain a sequence of lossless transforms into one:
let rotate = iso(get: rotatePoint, reverseGet: rotatePointBack)
let scale = iso(get: scalePoint, reverseGet: scalePointBack)
let translate = iso(get: translatePoint, reverseGet: translatePointBack)
let transform: Iso<Point, Point> = mconcat([rotate, scale, translate])
transform.get(point) // all three applied in order
transform.reverse.get(point) // all three reversed, in reverse order>>> and <<< compose an Iso with any other optic, returning the strongest optic the combination allows:
| Composition | Result |
|---|---|
Iso >>> Iso |
Iso |
Iso >>> Lens / Lens >>> Iso |
Lens |
Iso >>> Prism / Prism >>> Iso |
Prism |
Iso >>> AffineTraversal / AffineTraversal >>> Iso |
AffineTraversal |
let addOne = iso(get: { $0 + 1 }, reverseGet: { $0 - 1 })
let timesTwo = iso(get: { $0 * 2 }, reverseGet: { $0 / 2 })
let combined = addOne >>> timesTwo // Iso<Int, Int>
combined.get(5) // (5+1)*2 = 12
combined.reverseGet(12) // 12/2 - 1 = 5Bridging to SwiftUI Binding (Apple platforms, requires CoreFP)
Binding[optic: iso] always returns a Binding<A> (never optional — Iso is total):
@State var meters: Double = 1.0
// Editing in feet while storing in meters:
TextField("Feet", value: $meters[optic: metersToFeet], format: .number)See Binding for the full bridge API.
Endo<A> wraps an endomorphism — a function (A) -> A — and gives it a Monoid instance under left-to-right composition. The identity element is the do-nothing function.
let trim = Endo<String> { $0.trimmingCharacters(in: .whitespaces) }
let lower = Endo<String> { $0.lowercased() }
let exclaim = Endo<String> { $0 + "!" }
let normalize: Endo<String> = mconcat([trim, lower, exclaim])
normalize.runEndo(" HELLO ") // "hello!"
normalize(" HELLO ") // "hello!" — callAsFunction works tooEndo.combine(f, g) applies f first, then g — the same left-to-right order as >>>. The <> operator and mconcat follow from the Semigroup/Monoid conformances:
let pipeline = trim <> lower <> exclaim // same as mconcat([trim, lower, exclaim])
pipeline(" HELLO ") // "hello!"Endo vs Iso<A, A>
Both are endomorphisms and both form a Monoid under composition, but they differ in one key way:
Endo<A> |
Iso<A, A> |
|
|---|---|---|
| Stores | (A) -> A |
(A) -> A + inverse (A) -> A |
| Reversible | no | yes — .reverse gives the undo |
| Use when | trimming, clamping, normalizing | rotating, scaling, unit conversion |
You can always extract an Endo from an Iso<A, A> via .get, but not vice versa — invertibility requires both directions up front.
EndoMut<A> is the in-place companion to Endo<A>. It wraps (inout A) -> Void instead of (A) -> A. The algebra is identical — EndoMut is still a Monoid under sequential application — but for Swift value types with Copy-on-Write (CoW) internals, the performance characteristics differ fundamentally.
Swift's CoW types — Array, Dictionary, Set, String — store their contents in a heap buffer tracked by a reference count. Mutation is in-place only when the reference count of that buffer is exactly 1. The moment it reaches 2, Swift copies the entire buffer before mutating.
When you call a pure (A) -> A function:
let newState = reducer(action)(state)
// ^^^^^
// At this point `state` in the caller still holds a reference to every
// CoW buffer. The function argument holds a second reference.
// Reference count = 2 → any mutation inside copies the whole buffer.This means that for every reducer call on a state containing a 100 000-element array, touching even a single element triggers an O(n) heap copy — even if nothing else in the state changes.
EndoMut passes the value by exclusive reference:
reducer(action)(&state)
// ^^^^^^
// Swift's Law of Exclusivity (SE-0176) statically guarantees no other
// code holds an alias to `state` for the duration of this call.
// Reference count = 1 → CoW mutates the buffer in place.The exclusivity guarantee is enforced by the compiler, not convention. You cannot hold another reference to the same value while an inout borrow is active — the compiler rejects the code at compile time. This makes EndoMut semantically pure: there is no shared mutable state, and the transformation is referentially transparent at the call site.
var items = Array(0..<10_000)
let clamp = EndoMut<[Int]> { xs in for i in xs.indices { xs[i] = min(xs[i], 100) } }
let sort = EndoMut<[Int]> { $0.sort() }
let normalise: EndoMut<[Int]> = mconcat([clamp, sort])
normalise.runEndoMut(&items) // clamps first, then sorts — no copies
normalise(&items) // callAsFunction also worksEndoMut.combine(f, g) applies f first, then g — g sees every mutation f made. The <> operator and mconcat follow from the Semigroup/Monoid conformances:
(clamp <> sort)(&items) // same as mconcat([clamp, sort])The two types are isomorphic as monoids. Converting Endo → EndoMut is free (no extra copy). Converting EndoMut → Endo always makes one copy — that copy is precisely what pure-function semantics require.
// Endo → EndoMut (free — no extra copy)
let mutating: EndoMut<Int> = Endo<Int> { $0 + 1 }.toEndoMut()
// EndoMut → Endo (one copy of the value)
let pure: Endo<Int> = EndoMut<Int> { $0 += 1 }.toEndo()
// Round-trips preserve semantics
let original = EndoMut<Int> { $0 *= 2 }
let roundTripped = original.toEndo().toEndoMut()
// original and roundTripped produce the same result for any inputEndo vs EndoMut
Endo<A> |
EndoMut<A> |
|
|---|---|---|
| Function type | (A) -> A |
(inout A) -> Void |
| CoW containers | copied on mutation | mutated in place |
| Composable | yes — Monoid |
yes — same Monoid |
| Bridgeable | .toEndoMut() (free) |
.toEndo() (one copy) |
| Use when | values are small / opaque | Array, Dictionary, large structs |
Every optic (Lens, Prism, AffineTraversal) has a lift method that zooms an EndoMut<A> out to an EndoMut<S> through the optic's focus. This is the idiomatic way to write reducers over large states without triggering CoW copies.
// A reducer over a sub-state
let itemReducer = EndoMut<Item> { item in item.views += 1 }
// Lift it to the full AppState using a composed optic chain
let appReducer: EndoMut<AppState> =
lens(\AppState.feed)
.compose([Item].ix(id: selectedId))
.lift(itemReducer)
appReducer(&appState)
// ✓ AppState is not copied
// ✓ [Item] buffer is not copied (direct inout subscript)
// ✓ Only the one Item is mutated in placeCopy cost at each link
The cost of a composed chain is the sum of its links:
| Optic | lift copy cost |
|---|---|
lens(\.varProp) — WritableKeyPath |
zero-copy (Swift modify coroutine) |
lens(\.letProp) { … } — computed setter |
copies focused value A once |
ix on MutableCollection |
zero-copy (direct inout subscript) |
ix on Dictionary |
copies Value once; dictionary buffer not copied |
Prism (enum case) |
copies associated value once; outer S not copied |
| Composition chain | propagates per link — outer S is always inout |
The outer S is always kept as inout throughout the chain — only the focused sub-value at each link is ever extracted and written back.
compose without operators
If you import only CoreFP (no CoreFPOperators), use compose in place of >>>:
let appReducer: EndoMut<AppState> =
lens(\AppState.feed)
.compose([Item].ix(id: selectedId))
.lift(itemReducer)
// identical to the >>> form aboveStateful<S, Void> ↔ EndoMut<S>
Stateful<S, Void> and EndoMut<S> wrap the same closure type (inout S) -> Void. Convert freely between them at zero cost:
let endoMut = EndoMut<AppState> { $0.counter += 1 }
let stateful: Stateful<AppState, Void> = endoMut.toStateful() // free
let backToEndo: EndoMut<AppState> = stateful.toEndoMut() // freeZooming Stateful computations through optics
When a computation needs to return a value in addition to mutating state, use zoom instead of lift. zoom lifts a Stateful<A, Result> to a Stateful<S, Result> through the optic's focus. For Prism and AffineTraversal, the result is Result? — nil when the focus is absent:
let pop = Stateful<[Item], Item?> { items in
guard !items.isEmpty else { return nil }
return items.removeLast()
}
// Zoom into the feed array inside AppState
let appPop: Stateful<AppState, Item?> = lens(\AppState.feed).zoom(pop)
let (removedItem, newState) = appPop.runStateful(appState)The outer S is always inout; only the focused Part is extracted and written back.
Either, Result, and similar two-case types all conform to the SumType2<A, B> protocol, which gives them a uniform interface without duplicating switch statements everywhere.
// match — exhaustive elimination without a switch
let e: Either<String, Int> = .right(42)
e.match(
caseLeft: { "Error: \($0)" },
caseRight: { "Value: \($0)" }
) // "Value: 42"
// .a / .b — optional projections
Either<String, Int>.left("oops").a // Optional("oops")
Either<String, Int>.right(42).b // Optional(42)
// .isA / .isB — predicate checks
Result<Int, Error>.success(42).isB // true
// from — convert between any two conforming types with the same type parameters
let r = Result<Int, String>.from(Either<String, Int>.right(42)) // .success(42)The protocol defines three requirements — left(_:), right(_:), match(caseLeft:caseRight:), and from(_:) — and provides .a, .b, .isA, .isB as extensions. Use SumType2 in your own generic functions to work over Either, Result, and any custom two-case type simultaneously.
FP is a Sendable-first library. Composition (functor / applicative / monad / transformer operators, function helpers, optics, etc.) requires @Sendable closures end-to-end; side effects live at the boundary, outside the composition layer.
This is a deliberate FP discipline rather than a Swift concurrency quirk: composition should be pure, and @Sendable is the strongest static guarantee Swift gives us that a closure carries no side-channel state.
@Sendable rules out closures that capture non-Sendable values (mutable view-controller state, services, classes), and that's the first line of defence. But the deeper FP principle is stronger: the closures you pass to map / flatMap / <*> / compose / >=> ideally shouldn't capture anything at all — not even Sendable values.
A closure that captures a captured variable introduces hidden inputs (or hidden outputs). Two flavours:
- Effect — the closure writes to something outside itself: increments a counter, mutates a class property, logs, sends a network request. The function's output depends on more than its arguments; it changes the world.
- Co-effect — the closure reads from something outside itself: a global, a singleton, the current time, a flag stored in
self. The function's output depends on more than its arguments; it observes the world.
Either kind of capture breaks referential transparency — the property that f(x) always returns the same value for the same x, and that replacing f(x) with its result anywhere in the program doesn't change behaviour. Without referential transparency, equational reasoning collapses: you can no longer refactor by substitution, test by passing arguments, or rely on the functor/applicative/monad laws.
The library can't enforce capture-freeness at the type level — Swift has no @Pure attribute — so it does the next-best thing and requires @Sendable, which at least blocks non-Sendable captures. The intent is for you to go further:
// ❌ Co-effect: reads `formatter` from the enclosing scope
let formatter = DateFormatter() // (Sendable struct, would compile)
let render = { (date: Date) -> String in formatter.string(from: date) }
// ❌ Effect: writes `count` from the enclosing scope
var count = 0
let increment = { (n: Int) -> Int in count += 1; return n + count }
// ✅ Capture-free: depends only on its argument
let render = { (date: Date) -> String in
DateFormatter().string(from: date) // or pass the formatter as an argument
}
// ✅ State threaded explicitly through the type system
let increment: @Sendable (Int) -> Stateful<Int, Int> = { n in
Stateful<Int, Int> { state in state += 1; return n + state }
}Where state, dependencies, environments, or accumulated logs are unavoidable, the library gives you a type to represent them: Stateful for mutation, Reader for dependencies, Writer for accumulated logs, Either / Validation for failure, DeferredTask for async IO. Lift the would-be capture into one of those, and the closure stays capture-free while the dependency becomes visible in the function's type signature.
Closures that capture nothing can't surprise you. That's the bar; @Sendable is the floor.
| Layer | Sendable status |
|---|---|
All algebraic types (Either, Validation, Reader, Stateful, Writer, Loading, NonEmpty, Newtype, Endo, EndoMut, Iso, Lens, Prism, AffineTraversal, DeferredTask, DeferredStream, ZIO, ZIOKleisli, …) |
Conditionally Sendable when their type parameters are Sendable |
Semigroup, Monoid, SumType2, FunctionWrapper, CaseMatchable, HasCases, HasMax, HasMin, SIMDMonoidScalar |
Refine Sendable (conformers must be Sendable) |
apply / <*> / flatMap / >>- / >=> / liftA2 / fmap / <£> / >>> / composition helpers (compose, curry, flip, withArg, …) |
Take and return @Sendable closures |
KeyPath / WritableKeyPath |
Retroactively @unchecked Sendable (immutable metadata, safe to share) |
Swift's implicit KeyPath → (Root) -> Value conversion produces a closure that is not @Sendable, even though KeyPath itself is Sendable. To pass a key path into composition, lift it explicitly:
import CoreFP // `get(_:)` — unambiguous free function
import CoreFPOperators // prefix `^` — terse form
let predicate = compose(get(\User.name), equals("Alice")) // 1. Unambiguous
let predicate = compose(^\User.name, equals("Alice")) // 2. Operator
let predicate = compose({ $0.name }, equals("Alice")) // 3. Explicit closureThe ^ prefix operator is overloaded:
^\Person.ageon aWritableKeyPathreturns aLens<Person, Int>.^\Person.nameon aKeyPath(letproperty) returns either a curried Lens builder or a@Sendable (Person) -> String, picked by call-site context. When the context is ambiguous, fall back toget(_:).
A @Sendable closure can capture self only if self is itself Sendable. View controllers, view models, and most reference types aren't — and shouldn't be smuggled into composition. The library uses three boundary patterns:
// 1. Combine — `sink` is non-@Sendable, accepts non-Sendable self
publisher
.map(parseUser) // pure composition, @Sendable closures
.sink { [weak self] user in self?.update(user) } // boundary
// 2. DeferredTask / DeferredStream — build the body Sendable, do the
// side effect outside in a Task that captures self
Task { @MainActor in
let user = await fetchUserTask.run()
self.updateLabel(user.name)
}
// 3. Reader — pass dependencies through the environment, not via capture
reader.runReader(env) // returns a value; act on `self` next to itStateful<S, A> stores @Sendable (inout S) -> A and threads state through flatMap. The @Sendable requirement forbids capturing an inout from an enclosing scope into the closure body, so applicative / monad combinators evaluate each sub-Stateful before wrapping the next @Sendable block:
Stateful<S, B> { s in
let f = sf.run(&s) // run sf first (state advances)
let a = sa.run(&s) // run sa second (state advances again)
return ... // combine results inside the @Sendable body
}This matches the standard left-to-right applicative semantics for State.
Because the algebra layer is intended for value types you compose and pass around, Semigroup (and therefore Monoid, SumType2, etc.) refine Sendable. The standard numeric, string, array, set, dictionary, and option types satisfy this trivially. If you write a custom Semigroup, the conforming type must be Sendable — usually free for value types.
Function composition — >>> and <<<
>>> chains functions left-to-right; <<< chains right-to-left. Both produce a single function from the chain:
let trim: (String) -> String = { $0.trimmingCharacters(in: .whitespaces) }
let uppercased: (String) -> String = { $0.uppercased() }
let exclaim: (String) -> String = { $0 + "!" }
let shout = trim >>> uppercased >>> exclaim // left-to-right
let shout2 = exclaim <<< uppercased <<< trim // right-to-left — equivalent
shout(" hello ") // "HELLO!"
shout2(" hello ") // "HELLO!"Key paths are also composable — useful when building point-free transformations over nested types:
struct Company { var ceo: Person }
struct Person { var name: String }
let ceoName: (Company) -> String = \.ceo >>> \.name
companies.map(ceoName) // [String]Function application — £ / <| and |>
£ and <| both apply a function to a value with the function on the left. They use the same precedence group — lower than every other operator — so they eliminate wrapping parentheses. Both are right-associative, so chains nest naturally:
uppercased £ trim £ " hello " // "HELLO" — evaluated right-to-left: trim first, then uppercasedThe practical difference is in readability and conflict risk: £ is a single Unicode character that never clashes with any other Swift operator. <| is ASCII, but the < and | characters appear in comparison and bitwise-OR operators, so it can cause parse ambiguity when placed directly adjacent to expressions involving < or |. Prefer £ inside complex expressions; use <| where clarity is sufficient.
|> is the value-left flip — left-associative at the same level — ideal for pipelines:
" hello "
|> trim
|> uppercased
|> exclaim // "HELLO!"id — identity function
id returns its argument unchanged. It replaces { $0 } or \.self in any position that expects a function, enabling point-free style:
id("hello") // "hello"
// Use instead of { $0 } in map/flatMap/filter:
[Optional(1), nil, Optional(3)].compactMap(id) // [1, 3]
["a", "b", "c"].map(id) // ["a", "b", "c"] — no-op map
// Use as a default closure parameter:
func process(_ transform: (String) -> String = id) -> String { ... }
// Use in Optional.fold to pass values through the some branch unchanged:
optional.fold(onNone: "", onSome: id)const — ignore arguments, return a fixed value
const produces a function that ignores all its arguments and returns a single value. Overloads cover zero to three ignored arguments individually; four or more use a variadic tail:
// Single-argument: replaces { _ in 42 }
[1, 2, 3].map(const(42)) // [42, 42, 42]
Optional("hello").map(const(true)) // Optional(true)
// Multi-argument: replaces { _, _ in "fixed" } or { _, _, _, _ in "fixed" }
let alwaysZero: (Int, String, Bool) -> Int = const(0)
alwaysZero(99, "ignored", true) // 0
// Combine with map to replace contents:
results.map(const(.success(()))) // all successes, structure preservedflip, curry, uncurry, partialApply
// flip — swap the two arguments of a binary function
flip(-)( 3, 10) // 7 — equivalent to 10 - 3
[1, 2, 3].reduce(0, flip(+)) // sum, argument order doesn't matter for +
// curry — (A, B) -> C into A -> B -> C
let add = curry { (a: Int, b: Int) in a + b }
let addFive = add(5) // (Int) -> Int
addFive(3) // 8
// uncurry — A -> B -> C into (A, B) -> C
let addUncurried = uncurry(add)
addUncurried(3, 4) // 7
// partialApply — fix the first argument
let triple = partialApply({ a, b in a * b }, 3)
triple(7) // 21withArg — select which argument to operate on in a multi-argument context
withArg takes a key path that picks one value from a tuple of arguments, then lets you plug a single-argument function into that position. This is useful when adapting a unary function into a binary or ternary context without a closure:
// Adapting a (String) -> Bool into (Int, String) -> Bool
// by selecting the second argument:
let isLongName: (Int, String) -> Bool = withArg(\.1)(\.count >>> { $0 > 5 })
isLongName(42, "Alexander") // true
isLongName(42, "Ali") // false
// Selecting the first argument explicitly:
let doubleFirst: (Int, String) -> Int = withArg(\.0)({ $0 * 2 })
doubleFirst(21, "ignored") // 42fanout — apply several functions to the same input
// All functions receive the same value; results are collected into a tuple:
let describe = fanout(\.count, \.first, uppercased)
let (count, first, upper) = describe("hello")
// (5, Optional("h"), "HELLO")
// Useful for building a summary from a single pass:
users.map(fanout(\.name, \.age, \.isAdmin))
// [(String, Int, Bool)]join and void
join flattens one layer of nesting; void discards the contained values while keeping the container shape:
// join — one layer in, same container out
join([[1, 2], [3, 4]]) // [1, 2, 3, 4]
join(Optional(Optional(42))) // Optional(42)
join(Result<Result<Int, E>, E>.success(.success(42))) // .success(42)
// void — like map(ignore); keeps structure, discards values
void([1, 2, 3]) // [(), (), ()]
void(Optional(42)) // Optional(())
void(Result<Int,E>.success(99)) // .success(())void differs from ignore: ignore is (A) -> Void (discards a single value entirely), while void is Container<A> -> Container<Void> (maps every element to ()). Use ignore when you want to drop a value; use void when you want to strip the values from a container but keep its structure.
Tuple utilities
mapTuple2(uppercased)("hello", "world") // ("HELLO", "WORLD")
mapTuple3({ $0 * 2 })(1, 2, 3) // (2, 4, 6)
tuple(1, "hello") // (1, "hello")
untuple { a, b in a + b }((3, 4)) // 7 — (A, B) argument → two separate argumentsType casting — curried, for pipelines
// cast — identity cast; value must already be the target type (no crash)
cast(String.self)("hello") // "hello"
// castOptionally — safe conditional cast; nil on mismatch
castOptionally(Int.self)("hello") // nil
castOptionally(Int.self)(42) // Optional(42)
// In a pipeline:
items.compactMap(castOptionally(URL.self)) // [URL] — only URLs survivelazy / unlazy — defer and force evaluation
let later: () -> Int = lazy(expensiveComputation()) // not evaluated yet
unlazy(later) // forces itBoolean predicates — curried, composable, for fully tacit style
equals, notEquals, not, and, or are overloaded for both Bool values and (A) -> Bool predicates, so they compose directly with key paths, <<</>>>, and flip to build predicates without any closure syntax:
struct User { let name: String; let age: Int; let isAdmin: Bool }
// equals / notEquals — match on a value
users.filter(equals("Alice") <<< \.name) // only "Alice"
users.filter(notEquals("Alice") <<< \.name) // everyone else
// not — negate any predicate
users.filter(not(\.isAdmin)) // non-admins only
// and / or — combine predicates
users.filter(and(equals("Alice") <<< \.name, \.isAdmin))
// Alices who are also admins
users.filter(or(equals("Alice") <<< \.name, equals("Bob") <<< \.name))
// Alices or Bobs
// Deeply composed — fully tacit with flip to avoid any closure:
users.filter(and(not(\.isAdmin), flip(>=)(18) <<< \.age))
// All even positives — fully tacit:
[0, 1, 2, -1, 4].filter(and(equals(0) <<< flip(%)(2), flip(>)(0))) // [2, 4]Mutable — builder-pattern copy for value types
struct Config: Mutable { var host: String; var port: Int }
let base = Config(host: "localhost", port: 8080)
let dev = base.mutate { $0.port = 3000 } // Config(host: "localhost", port: 3000)
let prod = base.mutate { $0.host = "prod.example.com" }clamped(to:) / within(_:) — Comparable range helpers
clamped(to:) constrains a value to a closed range, returning the nearest endpoint when it falls outside. within(_:) is a value-first phrasing of range.contains(value):
5.clamped(to: 0...10) // 5
(-3).clamped(to: 0...10) // 0
42.clamped(to: 0...10) // 10
3.5.clamped(to: 0.0...1.0) // 1.0
42.within(40...50) // true
42.within(40...42) // true
42.within(30...41) // false
// Combines with `±` (requires `Strideable`, see operator reference):
41.within(42 ± 2) // true — 41 falls inside 40...44Array.cartesian — n-ary Cartesian product
cartesian pairs every element of the input arrays into typed tuples. Unlike zip, which stops at the shortest array and only matches positions, cartesian produces every n × m × … combination:
Array.cartesian([1, 3, 5], ["a", "b"])
// [(1, "a"), (1, "b"), (3, "a"), (3, "b"), (5, "a"), (5, "b")]
Array.cartesian([1, 2], ["a"], [true, false])
// [(1, "a", true), (1, "a", false), (2, "a", true), (2, "a", false)]
// Any empty input collapses the result to []:
Array.cartesian([], [1], [1]) // []Overloads exist for 2-, 3-, and 4-arity inputs. Functionally equivalent to applying liftA2-style tuple construction over the list applicative, but the dedicated overload preserves the tuple shape without going through a closure.
ignore / absurd — structural helpers
// ignore — discard a value and return ()
[1, 2, 3].map(ignore) // [(), (), ()]
tasks.forEach(ignore) // run side effects, discard results
// absurd — exhaustively eliminate the Never type in impossible branches
func handle<A>(_ result: Either<Never, A>) -> A {
result.match(caseLeft: absurd, caseRight: id)
}All operators require CoreFPOperators (for built-in types) or DataStructureOperators (for DataStructure types). Every operator has a named-function equivalent in the core module.
| Operator | Flipped | Description | Types |
|---|---|---|---|
<£> |
<&> |
Functor map — fn left / container left | Optional, Array, Result, Publisher, AsyncSequence, DeferredTask, DeferredStream, Either, Loading, Reader, Stateful, Validation, Writer |
<£^> |
<&^> |
Transformer map (nested containers) — transformer-only, no base-type overloads | DeferredTask, DeferredStream, Either, Reader, Stateful, Validation, Writer transformer variants |
£> |
<£ |
Replace contents with a constant — container left / value left | Optional, Array, Result, Publisher, AsyncSequence, DeferredTask, DeferredStream, Either, Loading, Reader, Stateful, Validation, Writer |
<*> |
— | Applicative apply — wrapped function on left, wrapped value on right | Optional, Array, Result, Publisher, AsyncSequence, DeferredTask, DeferredStream, Either, Reader, Stateful, Validation, Writer |
*> |
<* |
Sequence two effects — keep right / keep left | Optional, Array, Result, Publisher, AsyncSequence, DeferredTask, DeferredStream, Either, Reader, Stateful, Validation, Writer |
>>- |
-<< |
Monadic bind — container left / fn left | Optional, Array, Result, Publisher, AsyncSequence, DeferredTask, DeferredStream, Either, Loading, Reader, Stateful, Writer |
->> |
<<- |
Comonad extend — container left / fn left | Writer |
>=> |
<=< |
Kleisli composition — left-to-right / right-to-left | Optional, Array, Result, DeferredTask, DeferredStream, Either, Loading, Reader, Stateful, Writer |
>>> |
<<< |
Function / optics composition — left-to-right / right-to-left | Functions, Iso, Lens, Prism, AffineTraversal |
£ / <| |
|> |
Function application — fn left / value left | Any function |
<|> |
— | Alternative / choice | Optional, Array, Result, Publisher, DeferredTask<A?>, DeferredTask<Result<A,E>>, DeferredStream |
<> |
— | Semigroup append | String, Array, Optional, Dictionary, Set, Result, Int.Monoids.*, Bool.Monoids.*, SIMD4<Int>.Monoids.*, … |
++ |
— | Concatenation | String, Array |
^ (prefix) |
— | Lift WritableKeyPath → Lens; KeyPath → partial Lens builder |
WritableKeyPath, KeyPath |
^ (infix) |
— | Numeric power — base ^ exp |
SignedNumeric |
± / +/- |
— | Symmetric range — center ± delta → ClosedRange |
Strideable (Int, Double, Float, Date, …) |
≅ |
— | Flipped range match — value ≅ range (equivalent to range ~= value) |
Comparable |
Each type in this library has a dedicated reference page with comprehensive examples covering every operation, operator, and transformer combination.
| Type | Description |
|---|---|
| Optional | Swift's built-in optional, extended with full Functor / Applicative / Monad instances |
| Array | Swift's built-in array, extended — models nondeterminism and multiple results |
| Result | Swift's built-in result, extended with bimap, Kleisli composition, and Monoid strategies |
| Publisher | Combine's AnyPublisher, extended with functional operations |
| AsyncSequence | Swift's AsyncSequence, extended with functional operations |
| DeferredTask | Lazy async computation — nothing runs until .run() is called |
| DeferredStream | Lazy async stream — nothing starts until first iteration |
| Binding | SwiftUI's Binding, extended with [optic:] subscripts for Lens, Iso, Prism, and AffineTraversal (Apple platforms only) |
| Type | Description |
|---|---|
| Either | Unconstrained sum type — both sides are equal citizens, no Error requirement |
| Loading | Four-state async lifecycle — idle / loading / loaded / failed, with previous carried through for stale-data UIs |
| Validation | Accumulating applicative — errors collect instead of short-circuiting |
| Reader | Dependency injection monad — wraps (Environment) -> Output |
| Stateful | State threading monad — wraps (inout S) -> A |
| Writer | Append-as-you-go monad — produces a value alongside an accumulated log |
| NonEmpty | Statically guaranteed non-empty sequence — Semigroup (no Monoid), full FAM + Foldable + Traversable |
| ZIO | Three-layer monad stack (Reader + Result + DeferredTask) — Env → DeferredTask<Result<Success, Failure>> |
| ZIOKleisli | First-class Kleisli arrow in the ZIO monad — (Input) → ZIO<Env, Success, Failure> |
Import FPMacros to generate Lens and Prism optics automatically from type declarations. Both macros use @attached(member) — they add code directly into the type body — so they work at any nesting level, including types nested inside other types.
import FPMacros@Lenses(_:init:) generates a memberwise initializer, a Lenses struct + static let lens accessor holding one Lens per stored property, and a with(...) copy-with-overrides helper.
Property rules:
| Property | In generated init? |
Gets a Lens? |
Lens kind |
|---|---|---|---|
let name: T |
yes — required | yes | reconstruction (calls init via the generated with(...)) |
let version = 1 |
no | no | immutable constant |
var port: Int |
yes — required | yes | WritableKeyPath |
var timeout = 30 |
yes — default = 30 |
yes | WritableKeyPath |
@Lenses(init: .public)
public struct Config {
public let host: String
public let version = 3 // constant — no lens, excluded from init
public var port: Int
public var timeout = 30
}Expanded code (simplified):
public struct Config {
public let host: String
public let version = 3
public var port: Int
public var timeout = 30
public init(host: String, port: Int, timeout: Int = 30) {
self.host = host; self.port = port; self.timeout = timeout
}
public struct Lenses: Sendable {
public let host: Lens<Config, String> = CoreFP.lens(\Config.host) { s, a in s.with(host: a) }
public let port: Lens<Config, Int> = CoreFP.lens(\Config.port)
public let timeout: Lens<Config, Int> = CoreFP.lens(\Config.timeout)
}
public static let lens = Lenses()
public func with(host: String? = nil, port: Int? = nil, timeout: Int? = nil) -> Config {
Config(
host: host ?? self.host,
port: port ?? self.port,
timeout: timeout ?? self.timeout
)
}
}The Lens<Config, String> for host (a let property) calls s.with(host: a) rather than inlining all field references — the with(...) helper is the single source of truth for reconstruction, keeping the codegen O(N) in the number of properties instead of O(N²).
For generic hosts (e.g. Container<T>), Swift forbids static let in a generic context. The macro automatically falls back to a computed static var lens: Lenses { Lenses() }. Same call-site syntax; allocates per access.
Usage:
let config = Config(host: "localhost", port: 8080)
Config.lens.host.set(config, "example.com")
// Config(host: "example.com", port: 8080, timeout: 30, version: 3)
Config.lens.port.over({ $0 + 1 })(config)
// Config(host: "localhost", port: 8081, timeout: 30, version: 3)
config.with(host: "example.com", port: 9090)
// same effect, without needing lenses
// Lenses compose as normal:
let teamConfigHost = lens(\.teamConfig) >>> Config.lens.hostOptional properties and with(...)
For properties whose type is T?, with(...) uses a double-Optional parameter under the hood so the common ergonomic call sites all behave intuitively:
@Lenses(init: .public)
public struct Server {
public let port: Int?
public let name: String
}
let s = Server(port: 8080, name: "main")
s.with() // Server(port: 8080, name: "main") — keep
s.with(port: nil) // Server(port: nil, name: "main") — clear
s.with(port: 9090) // Server(port: 9090, name: "main") — set
s.with(name: "primary") // Server(port: 8080, name: "primary") — non-Optional still worksThe trick: the parameter type is Int?? = .some(nil). The default .some(nil) means "no change"; a bare nil literal at the call site binds to outer-.none, meaning "clear"; any value v wraps to .some(.some(v)), meaning "set". Non-Optional properties use the simpler T? = nil + ?? form.
Slicing the output
Use LensesEmit to opt out of pieces you don't need:
@Lenses(.all) // default — init + lens + with
@Lenses(.initOnly) // only the memberwise init
@Lenses(.lensesOnly) // lens + with, no init at all (use when you have a custom init)If the struct already declares an init whose parameter labels match what the macro would generate, the macro skips its own init silently — the user's init wins.
Visibility skip
Properties whose declared visibility is lower than the struct's are excluded from both the lens namespace and with(...), with a diagnostic note. The init still includes them (it can legally assign lower-visibility properties from inside the struct).
@Prisms generates three things for the annotated enum:
- A
Prismsstruct +static let prismaccessor holding a typedPrismfor each case. - Per-case accessors (
shape.circle,shape.rectangle, …) — either as one computed property per case, or as a singlesubscript(dynamicMember:)if the enum is also annotated with@dynamicMemberLookup. - A nested
enum Cases: CaseMatchable(which inheritsCaseIterable) whose cases mirror the case names of the original enum (no associated values), plus afunc is(_:) -> Boolpredicate.
@dynamicMemberLookup // opt-in — collapses N computed properties into one subscript
@Prisms
public enum Shape {
case circle(Double)
case rectangle(Double, Double)
case empty
}Expanded code (simplified):
public enum Shape {
case circle(Double)
case rectangle(Double, Double)
case empty
public struct Prisms: Sendable {
public let circle: Prism<Shape, Double> = CoreFP.prism(
preview: { s in guard case .circle(let a) = s else { return nil }; return a },
review: Shape.circle
)
public let rectangle: Prism<Shape, (Double, Double)> = CoreFP.prism(
preview: { s in guard case .rectangle(let v0, let v1) = s else { return nil }; return (v0, v1) },
review: { (t: (Double, Double)) in Shape.rectangle(t.0, t.1) }
)
public let empty: Prism<Shape, Void> = CoreFP.prism(
preview: { s in guard case .empty = s else { return nil }; return () },
review: { (_: Void) in Shape.empty }
)
}
public static let prism = Prisms()
// With @dynamicMemberLookup on the enum — one subscript replaces N computed properties:
public subscript<PrismFocus>(
dynamicMember keyPath: KeyPath<Prisms, CoreFP.Prism<Shape, PrismFocus>>
) -> PrismFocus? {
Self.prism[keyPath: keyPath].preview(self)
}
public enum Cases: CoreFP.CaseMatchable {
public typealias Subject = Shape
case circle, rectangle, empty
public func matches(_ value: Shape) -> Bool { /* switch */ }
}
public func `is`(_ c: Cases) -> Bool { c.matches(self) }
}Without @dynamicMemberLookup, the macro emits one var caseName: AssociatedValue? { Self.prism.caseName.preview(self) } per case (and a build-time warning suggesting the attribute). Same call-site syntax in both modes.
For generic enums (e.g. Loading<Success, Failure>), Swift forbids static let in a generic context. The macro automatically falls back to static var prism: Prisms { Prisms() }. Same call-site syntax; allocates per access.
Usage:
let s = Shape.circle(3.14)
s.circle // Optional(3.14) — via dynamic-member subscript
s.rectangle // nil
Shape.prism.circle.preview(s) // Optional(3.14) — explicit optic
Shape.prism.circle.set(s, 5.0) // Shape.circle(5.0)
Shape.prism.circle.over({ $0 * 2 })(s) // Shape.circle(6.28)
// Case-name queries — no need to construct dummy payloads:
s.is(.circle) // true
s.is(.rectangle) // false
Shape.Cases.allCases // [.circle, .rectangle, .empty]Slicing the output
Use PrismsOptions to opt out of pieces you don't need:
@Prisms(.cases) // only the `Cases` enum + is(_:)
@Prisms(.prisms) // only the `Prisms` struct + `static prism`
@Prisms([.prisms, .properties]) // optics + accessors, no Cases / is.properties requires .prisms — auto-promoted silently if you forget.
Polymorphic HasCases
The CoreFP.HasCases protocol lets generic code write value.is(.someCase) against any type whose nested Cases enum is a CaseMatchable. @Prisms doesn't automatically add the conformance (Swift's extension-macro role can't reach into nested types), but you can opt in for any file-level or non-private-nested type:
extension MyEnum: HasCases {} // typealias inferred from the nested `Cases` enum
func currentIsFirstCase<T: HasCases>(_ value: T) -> Bool {
value.is(T.Cases.allCases.first!)
}The built-in types Loading, Either, Validation, Optional, and Result all conform out of the box.
Both macros use @attached(member) so they compose naturally with nested types — which is the typical pattern in unidirectional architectures where a Reducer owns its State and Action:
struct Reducer {
@Lenses(init: .internal)
struct State {
let userName: String
var score: Int
var isActive = false
}
@Prisms
enum Action {
case updateName(String)
case incrementScore(Int)
case reset
}
}
// State lenses — functional updates, no mutation:
let state = Reducer.State(userName: "Alice", score: 0)
Reducer.State.lens.userName.set(state, "Bob") // State(userName: "Bob", score: 0, isActive: false)
Reducer.State.lens.score.over({ $0 + 10 })(state) // State(userName: "Alice", score: 10, isActive: false)
// Action prisms — safe extraction and inspection:
let action = Reducer.Action.updateName("Bob")
action.updateName // Optional("Bob")
action.incrementScore // nil
Reducer.Action.prism.updateName.preview(action) // Optional("Bob")
// The generated optics are regular Lens / Prism values — compose and lift them
// exactly as shown in earlier sections. For example, if another struct wraps
// Reducer.State, its lens composes with the generated ones:
// lens(\AppState.game) >>> Reducer.State.lens.score // Lens<AppState, Int>
// lens(\AppState.game).compose(Reducer.State.lens.score) // same, no operatorsProperties with literal defaults (0, 3.14, "hello", true) have their types inferred automatically. For any other default, add an explicit type annotation:
var timeout: Duration = .seconds(30) // explicit annotation required
var retryPolicy: RetryPolicy = .exponential // explicit annotation required@Lenses and @Prisms cannot be applied to private declarations — the macros refuse with a compile-time error. private's type-scope semantics break the generated namespace (its stored Lens<Host, X> / Prism<Host, X> fields can't be exposed outside the host's body). Use fileprivate instead; it's functionally identical at file scope and works everywhere the macros need to. fileprivate, internal, package, public, and open all work uniformly, at file level or nested.
The following library types already ship with @Prisms-equivalent surface (hand-written to match what the macro would emit), so you can use them out of the box without applying the macro yourself:
| Type | Type.prism.… |
DML accessor (value.…) |
value.is(.…) |
|---|---|---|---|
Either<A, B> |
.left, .right |
✓ | ✓ |
Validation<E, A> |
.failure, .success |
✓ | ✓ |
Loading<S, F> |
.idle, .loading, .loaded, .failed |
✓ | ✓ |
Optional<Wrapped> |
.some, .none |
(explicit .some / .none properties — Swift stdlib types can't have @dynamicMemberLookup added) |
✓ |
Result<S, F> |
.success, .failure |
(explicit properties — same reason) | ✓ |
All five conform to HasCases, so they work with the polymorphic is(_:) extension. The legacy isSuccess / isFailure / isSome / isNone / isLeft / isRight boolean accessors have been removed — use value.is(.success) (etc.) instead.
Contributions are welcome. The architecture has a few firm rules to keep the library consistent:
- Every operator must delegate to a named function in the core module — never implement logic directly inside an operator definition
- Every directional operator has a flipped counterpart (e.g.,
<£>↔<&>); both must be added in the same commit - Operator modules (
CoreFPOperators,DataStructureOperators) are separate SPM targets and may not use custom operators internally
To contribute:
- Fork the repository
- Create a feature branch
- Make your changes, including tests
- Run the full test suite to confirm nothing is broken
- Submit a pull request
The library verifies functional programming laws (Functor, Applicative, Monad laws) and all operator behaviours across all types and their transformer combinations.
Test targets: CoreFPTests, CoreFPOperatorsTests, DataStructureTests, DataStructureOperatorsTests, FPMacrosTests.
# Run all tests
swift test
# Run a specific test by name (Swift Testing uses / as separator)
swift test --filter "DeferredTaskTests/flatMap"
# Run all tests whose name contains a word (matches across targets)
swift test --filter "CoreFP"| Platform | Minimum Version |
|---|---|
| macOS | 10.15+ |
| iOS | 13.0+ |
| tvOS | 13.0+ |
| watchOS | 6.0+ |
Combine-based features (Publisher extensions) require macOS 13.0+ / iOS 16.0+. All non-Combine modules are supported on Linux.
MIT
