This is not a full fledged package for functional programming in Swift. This will have to wait until Higher Kinded Types are part of the language. However this will make it easier to write functional code using built-in Swift Result and Optional types.
A lightweight functional programming toolkit for Swift, providing composable utilities for working with Result, Optional, and Array types in both synchronous and asynchronous contexts.
- Swift 6.2+
- macOS 15+ / iOS 18+
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/velocityzen/fp-swift.git", from: "1.7.0")
]Then import it:
import FPForward pipe operators for function composition:
// Basic pipe: pass value to function
let result = 5 |> double |> toString // "10"
// Pipe into second argument
let result = value |>> (function, firstArg)
// Pipe into third argument
let result = value |>>> (function, firstArg, secondArg)
// Flow operator: create function from another function
let transform = |> double // (Int) -> Intfunc createOrder(userId: Int, itemId: Int) -> Result<Order, AppError> {
ResultDo<AppError>()
.bind { fetchUser(id: userId) }
.bind { user in fetchItem(id: itemId) }
.let { user, item in item.price * user.discountRate }
.bind { user, item, price in
validateOrder(user: user, item: item, price: price)
}
.map { user, item, price, validation in
Order(user: user, item: item, price: price)
}
}
// Async variant with mixed sync/async steps
func createOrderAsync(userId: Int, itemId: Int) async -> Result<Order, AppError> {
await ResultDo<AppError>()
.bindAsync { await fetchUser(id: userId) }
.bindAsync { user in await fetchItem(id: itemId) }
.let { user, item in item.price * user.discountRate }
.bindAsync { user, item, price in
await validateOrder(user: user, item: item, price: price)
}
.map { user, item, price, validation in
Order(user: user, item: item, price: price)
}
}func processUser(id: Int) async -> Result<ProcessedUser, Error> {
await Result.fromAsync { try await api.fetchUser(id: id) }
.tapAsync { user in await analytics.track(.userFetched(user)) }
.mapAsync { user in await enrichUserData(user) }
.flatMapAsync { user in await validateUser(user) }
.tapError { error in logger.error("Failed: \(error)") }
}let result: Result<Int, Error> = .success(42)
// Transform success value asynchronously
let mapped = await result.mapAsync { value in
await fetchData(for: value)
}
// FlatMap for chaining Result-returning async operations
let chained = await result.flatMapAsync { value in
await validateAndTransform(value) // Returns Result<T, Error>
}
// Create Result from async throwing operation
let result = await Result.fromAsync {
try await networkCall()
}
// Create Result from Task
let task = Task { try await networkCall() }
let result = await Result.fromTask(task)
// Or with closure syntax
let result = await Result.fromTask {
Task { try await networkCall() }
}
// Also works with Tasks returning Results
let task = Task { await someResultOperation() }
let result: Result<Value, Error> = await Result.fromTask(task)Replace the success value with a constant or discard it entirely:
let result: Result<Int, AppError> = .success(42)
// Map to a specific constant
let mapped = result.as("done") // .success("done")
// Map to Void (discard the success value)
let unit = result.asUnit() // .success(())
// Useful in chains where you only care about success/failure
fetchUser(id: 1)
.tap { user in saveToCache(user) }
.asUnit() // Result<Void, AppError>Perform side effects while keeping the Result chain flowing:
someOperation()
.tap { value in saveToCache(value) }
.tapError { error in logError(error) }
.map { value in transform(value) }
// Async variants
await result
.tapAsync { value in await sendAnalytics(value) }
.tapErrorAsync { error in await reportError(error) }Tap variants:
.tap()- Sync side effect on success.tapAsync()- Async side effect on success.tapError()- Sync side effect on failure.tapErrorAsync()- Async side effect on failure
Mirrors fp-ts Either.alt. If self is a success, keep it; otherwise return the lazily-evaluated alternative. The alternative may itself succeed (recovery) or fail (in which case its failure replaces the original):
fetchUser(id: 1)
.alt { fetchUserFromCache(id: 1) }
.alt { .success(.guest) }
// Async variant
await fetchUser(id: 1)
.altAsync { await fetchUserFromCache(id: 1) }Mirrors fp-ts Either.getOrElse. Returns the success value, or computes/returns a fallback when the result is a failure. Unlike alt, this returns the unwrapped Success rather than another Result:
// Closure-based — receives the error
let count = parse(input).getOrElse { _ in 0 }
// Constant default (lazily evaluated)
let count = parse(input).getOrElse(0)
// Async variant — closure receives the error
let user = await fetchUser(id: 1).getOrElseAsync { _ in
await loadGuestUser()
}
// Async variant — lazily-evaluated async default
let user = await fetchUser(id: 1).getOrElseAsync(await loadGuestUser())- Throwing variants convert
Failuretype toError
Branch on a Result without switching manually:
let message = result.match(
{ "value: \($0)" },
{ "error: \($0)" }
)
let fallback = result.match("ok", "error")
let mixed = result.match(
{ "value: \($0)" },
"error"
)
let asyncMessage = await result.matchAsync(
{ value in
await Task.yield()
return "value: \(value)"
},
{ error in
await Task.yield()
return "error: \(error)"
}
)
let asyncMixed = await result.matchAsync(
"ok",
{ error in
await Task.yield()
return "error: \(error)"
}
)
// Use match for side effects without capturing the result
result.match(
{ value in print("Success: \(value)") },
{ error in print("Error: \(error)") }
)Convert optionals to Results:
// With static error
let result = Result<User, AppError>.fromOptional(user, error: .notFound)
// With lazy error (only evaluated if nil)
let result = Result<User, AppError>.fromOptional(user) {
.notFound(id: userId)
}Convert Result to boolean for simple success/failure checks:
let result: Result<User, AppError> = fetchUser(id: 1)
if result.toBool {
print("User fetched successfully")
}
// Or in ternary expressions
let message = result.toBool ? "ok" : "error"Combine multiple Results into a single Result with a tuple of values:
let userResult: Result<User, AppError> = fetchUser(id: 1)
let profileResult: Result<Profile, AppError> = fetchProfile(id: 1)
let settingsResult: Result<Settings, AppError> = fetchSettings(id: 1)
// Sync flatten - combine already-computed Results
let combined = flatten(userResult, profileResult, settingsResult)
// Result<(User, Profile, Settings), AppError>
// Use map to create named tuple for easier access
let namedResult = flatten(userResult, profileResult)
.map { (user: $0, profile: $1) }
// Result<(user: User, profile: Profile), AppError>
if case .success(let data) = namedResult {
print(data.user.name)
print(data.profile.bio)
}Run multiple async operations in parallel and combine their results:
func loadDashboard(userId: Int) async -> Result<Dashboard, AppError> {
// All three operations run in parallel
let result = await flattenAsync(
await fetchUser(id: userId),
await fetchNotifications(for: userId),
await fetchRecommendations(for: userId)
)
// Result<(User, [Notification], [Recommendation]), AppError>
return result.map { user, notifications, recommendations in
Dashboard(user: user, notifications: notifications, recommendations: recommendations)
}
}Apply a Result-returning transform to each element, short-circuiting on first failure:
let userIds = [1, 2, 3]
// Sync traverse
let result = userIds.traverse { id -> Result<User, AppError> in
fetchUser(id: id)
}
// Returns .success([User]) or .failure on first error
// Async traverse
let result = await userIds.traverseAsync { id in
await fetchUserAsync(id: id)
}Asynchronous versions of map, flatMap, and compactMap:
let items = [1, 2, 3, 4, 5]
let mapped = await items.mapAsync { item in
"v\(item)"
}
let flattened = await items.flatMapAsync { item in
[item, item * 10]
}
let compacted = await items.compactMapAsync { item -> String? in
await processItem(item) // Returns nil for items to filter out
}
enum ParseError: Error {
case invalid
}
let resultMapped = await items.mapAsync { item -> Result<String, ParseError> in
.success("item-\(item)")
}
let resultFlattened = await items.flatMapAsync { item -> Result<[Int], ParseError> in
.success([item, item + 100])
}
let resultCompacted = await items.compactMapAsync { item -> Result<String?, ParseError> in
.success(item.isMultiple(of: 2) ? "even-\(item)" : nil)
}Process streams of Results with familiar functional operations:
let stream = AsyncStream<Result<Int, AppError>> { continuation in
continuation.success(1)
continuation.success(2)
continuation.failure(.invalid)
continuation.success(3)
continuation.finish()
}
// Filter to just success values
for await value in stream.successes() {
print(value) // 1, 2, 3
}
// Transform, tap, and chain
for await result in stream
.tap { value in logger.info("got \(value)") }
.tapError { error in logger.error("\(error)") }
.mapAsync { value in await enrich(value) }
.flatMap { value in validate(value) }
{
// ...
}Map elements through an async transform in parallel while preserving source order. Each transform runs in its own Task, so wall-clock time is bounded by the slowest element rather than the sum of all transforms — but emission still follows source arrival order. Useful when an upstream provider streams items that should be processed concurrently but consumed in order (e.g. SSE image references that need to be fetched in parallel and rendered in order):
for await image in references.mapAsyncKeepOrder({ ref in
await downloader.fetch(ref)
}) {
render(image)
}When the source is a stream of Result, an overload transforms only the success values and passes failures through unchanged — preserving order across both:
for await result in events.mapAsyncKeepOrder({ event in
await enrich(event)
}) {
handle(result) // Result<EnrichedEvent, MyError>
}Create single-element Result streams or use convenience methods on continuations:
// Static factories — single-element streams
let success: AsyncStream<Result<Int, MyError>> = .success(42)
let failure: AsyncStream<Result<Int, MyError>> = .failure(.someError)
// Continuation helpers
let stream = AsyncStream<Result<Int, MyError>> { continuation in
continuation.success(1)
continuation.success(2)
continuation.failure(.someError)
continuation.finish()
}
// Or finish with a final value
let stream = AsyncStream<Result<String, MyError>> { continuation in
continuation.success("processing...")
continuation.finishWithSuccess("done") // Yields and finishes
}
// Finish with error
let stream = AsyncStream<Result<Data, NetworkError>> { continuation in
continuation.finishWithFailure(.connectionLost) // Yields error and finishes
}let optional: String? = "hello"
let message = optional.match(
{ "got: \($0)" },
"nothing"
)
// "got: hello"
let missing: String? = nil
missing.match(
{ value in print(value) },
()
)
// Does nothinglet optional: Int? = 42
let mapped = await optional.mapAsync { value in
await fetchDetails(for: value)
}
// Returns nil if optional was nil, otherwise the transformed value
let flatMapped = await optional.flatMapAsync { value -> String? in
value > 0 ? "id-\(value)" : nil
}let optional: Int? = nil
let fallback = optional.orElse(99) // Returns 99 when nil, nil when has valueMonadic do-notation for composing multiple Result operations with an accumulating context (like fp-ts Do / bind / let):
// ResultDo starts the chain, bind adds Result values, let adds pure values
let result = ResultDo<MyError>()
.bind { getUser() } // Result<User, MyError>
.bind { user in getProfile(user) } // Result<(User, Profile), MyError>
.let { _, profile in profile.name } // Result<(User, Profile, String), MyError>
.map { user, _, name in "\(user.id): \(name)" } // Result<String, MyError>
// Short-circuits on the first failure
let result = ResultDo<MyError>()
.bind { getUser() } // .failure(.notFound) → stops here
.bind { user in getProfile(user) } // never called
.map { user, profile in profile } // never called
// result == .failure(.notFound)API:
// Start the chain
ResultDo<Failure>()
// Bind: add a Result value, accumulates into a growing tuple
func bind<A>(_ f: () -> Result<A, Failure>) -> Result<A, Failure>
func bind<B>(_ f: (A) -> Result<B, Failure>) -> Result<(A, B), Failure>
func bind<C>(_ f: (A, B) -> Result<C, Failure>) -> Result<(A, B, C), Failure>
// ... up to 10 accumulated values
// Let: add a pure (non-Result) value
func `let`<A>(_ f: () -> A) -> Result<A, Failure>
func `let`<B>(_ f: (A) -> B) -> Result<(A, B), Failure>
func `let`<C>(_ f: (A, B) -> C) -> Result<(A, B, C), Failure>
// ... up to 10 accumulated values
// Async variants: bindAsync / letAsync
func bindAsync<A>(_ f: () async -> Result<A, Failure>) async -> Result<A, Failure>
func bindAsync<B>(_ f: (A) async -> Result<B, Failure>) async -> Result<(A, B), Failure>
// ... up to 10 accumulated values
func letAsync<A>(_ f: () async -> A) async -> Result<A, Failure>
func letAsync<B>(_ f: (A) async -> B) async -> Result<(A, B), Failure>
// ... up to 10 accumulated valuesSync and async can be freely mixed in the same chain:
let result = await ResultDo<MyError>()
.bind { getCachedUser() } // sync
.bindAsync { user in await fetchProfile(user) } // async
.let { user, profile in profile.name } // sync
.mapAsync { user, profile, name in // async
await formatDisplay(user, name)
}// Non-throwing
func mapAsync<T>(_ transform: (Success) async -> T) async -> Result<T, Failure>
func mapFailureAsync<E: Error>(_ transform: (Failure) async -> E) async -> Result<Success, E>
func flatMapAsync<T>(_ transform: (Success) async -> Result<T, Failure>) async -> Result<T, Failure>
// Throwing (requires Failure == Error)
func mapAsync<T>(_ transform: (Success) async throws -> T) async -> Result<T, Error>
// Map to constant value
func `as`<T>(_ value: T) -> Result<T, Failure>
func asUnit() -> Result<Void, Failure>// Lazily provides an alternative on failure (analogue of fp-ts Either.alt)
func alt(_ alternative: () -> Result<Success, Failure>) -> Result<Success, Failure>
func altAsync(_ alternative: () async -> Result<Success, Failure>) async -> Result<Success, Failure>// Unwrap or fall back (analogue of fp-ts Either.getOrElse)
func getOrElse(_ onFailure: (Failure) -> Success) -> Success
func getOrElse(_ defaultValue: @autoclosure () -> Success) -> Success
func getOrElseAsync(_ onFailure: (Failure) async -> Success) async -> Success
func getOrElseAsync(_ defaultValue: @autoclosure @escaping () async -> Success) async -> SuccessAll match variants are marked @discardableResult, so you can use them both for transforming values and for side effects without assigning the result.
@discardableResult func match<T>(_ onSuccess: (Success) -> T, _ onFailure: (Failure) -> T) -> T
@discardableResult func match<T>(_ onSuccess: (Success) -> T, _ failure: @autoclosure () -> T) -> T
@discardableResult func match<T>(_ success: @autoclosure () -> T, _ onFailure: (Failure) -> T) -> T
@discardableResult func match<T>(_ success: @autoclosure () -> T, _ failure: @autoclosure () -> T) -> T
@discardableResult func matchAsync<T>(_ onSuccess: (Success) async -> T, _ onFailure: (Failure) async -> T) async -> T
@discardableResult func matchAsync<T>(_ onSuccess: (Success) async -> T, _ failure: @autoclosure () -> T) async -> T
@discardableResult func matchAsync<T>(_ success: @autoclosure () -> T, _ onFailure: (Failure) async -> T) async -> T// Throwing (requires Failure == Error)
static func fromAsync(_ operation: () async throws -> Success) async -> Result<Success, Error>// Throwing Task (requires Failure == Error)
static func fromTask(_ task: Task<Success, Error>) async -> Result<Success, Error>
static func fromTask(_ task: () -> Task<Success, Error>) async -> Result<Success, Error>
// Non-throwing Task (requires Failure == Never)
static func fromTask(_ task: Task<Success, Never>) async -> Result<Success, Never>
static func fromTask(_ task: () -> Task<Success, Never>) async -> Result<Success, Never>
// Task returning Result (requires Failure == Error)
static func fromTask<S>(_ task: Task<Result<S, Error>, Never>) async -> Result<S, Error>
static func fromTask<S>(_ task: () -> Task<Result<S, Error>, Never>) async -> Result<S, Error>static func fromOptional(_ optional: Success?, error: Failure) -> Result<Success, Failure>
static func fromOptional(_ optional: Success?, onError: () -> Failure) -> Result<Success, Failure>
static func fromOptional(error: Failure) -> (Success?) -> Result<Success, Failure>
static func fromOptional(onError: () -> Failure) -> (Success?) -> Result<Success, Failure>var toBool: Bool // true for success, false for failure// Non-throwing
func tap(_ action: (Success) -> Void) -> Result<Success, Failure>
func tap<T>(_ action: (Success) -> T) -> Result<Success, Failure>
func tap<E>(_ action: (Success) -> Result<Void, E>) -> Result<Success, E>
func tap<T, E>(_ action: (Success) -> Result<T, E>) -> Result<Success, E>
// Throwing
func tap(_ action: (Success) throws -> Void) -> Result<Success, Error>
func tap<T>(_ action: (Success) throws -> T) -> Result<Success, Error>
// Async non-throwing
func tapAsync(_ action: (Success) async -> Void) async -> Result<Success, Failure>
func tapAsync<T>(_ action: (Success) async -> T) async -> Result<Success, Failure>
func tapAsync<E>(_ action: (Success) async -> Result<Void, E>) async -> Result<Success, E>
func tapAsync<T, E>(_ action: (Success) async -> Result<T, E>) async -> Result<Success, E>
// Async throwing
func tapAsync(_ action: (Success) async throws -> Void) async -> Result<Success, Error>
func tapAsync<T>(_ action: (Success) async throws -> T) async -> Result<Success, Error>// Non-throwing
func tapError(_ action: (Failure) -> Void) -> Result<Success, Failure>
func tapError<T>(_ action: (Failure) -> T) -> Result<Success, Failure>
func tapError<T, E>(_ action: (Failure) -> Result<T, E>) -> Result<Success, E>
// Throwing
func tapError(_ action: (Failure) throws -> Void) -> Result<Success, Error>
func tapError<T>(_ action: (Failure) throws -> T) -> Result<Success, Error>
// Async non-throwing
func tapErrorAsync(_ action: (Failure) async -> Void) async -> Result<Success, Failure>
func tapErrorAsync<T>(_ action: (Failure) async -> T) async -> Result<Success, Failure>
func tapErrorAsync<T, E>(_ action: (Failure) async -> Result<T, E>) async -> Result<Success, E>
// Async throwing
func tapErrorAsync(_ action: (Failure) async throws -> Void) async -> Result<Success, Error>
func tapErrorAsync<T>(_ action: (Failure) async throws -> T) async -> Result<Success, Error>Combine multiple Results into a single Result containing a tuple of all success values. If any Result fails, returns the first failure.
// Supports 2-10 arguments
func flatten<A, B, E: Error>(_ a: Result<A, E>, _ b: Result<B, E>) -> Result<(A, B), E>
func flatten<A, B, C, E: Error>(_ a: Result<A, E>, _ b: Result<B, E>, _ c: Result<C, E>) -> Result<(A, B, C), E>
// ... up to 10 arguments// Supports 2-10 arguments, runs all operations in parallel
func flattenAsync<A: Sendable, B: Sendable, E: Error>(
_ a: @Sendable @autoclosure @escaping () async -> Result<A, E>,
_ b: @Sendable @autoclosure @escaping () async -> Result<B, E>
) async -> Result<(A, B), E>
// ... up to 10 argumentsfunc traverse<Success>(_ transform: (Element) -> Success) -> Result<[Success], Never>
func traverse<Success, Failure>(_ transform: (Element) -> Result<Success, Failure>) -> Result<[Success], Failure>
func traverseAsync<Success>(_ transform: (Element) async -> Success) async -> Result<[Success], Never>
func traverseAsync<Success, Failure>(_ transform: (Element) async -> Result<Success, Failure>) async -> Result<[Success], Failure>func separate<Success, Failure>() -> (successes: [Success], failures: [Failure])
where Element == Result<Success, Failure>// mapAsync
func mapAsync<T>(_ transform: (Element) async -> T) async -> [T]
func mapAsync<T, Failure: Error>(
_ transform: (Element) async -> Result<T, Failure>
) async -> Result<[T], Failure>
// flatMapAsync
func flatMapAsync<S: Sequence>(
_ transform: (Element) async -> S
) async -> [S.Element]
func flatMapAsync<S: Sequence, Failure: Error>(
_ transform: (Element) async -> Result<S, Failure>
) async -> Result<[S.Element], Failure>
// compactMapAsync
func compactMapAsync<T>(_ transform: (Element) async -> T?) async -> [T]
func compactMapAsync<T, Failure: Error>(
_ transform: (Element) async -> Result<T?, Failure>
) async -> Result<[T], Failure>Extensions for any AsyncSequence where Element == Result<Success, Failure>:
func successes() -> AsyncCompactMapSequence // unwraps success values
func failures() -> AsyncCompactMapSequence // unwraps failure errors// Sync
func map<T>(_ transform: (Success) -> T) -> AsyncMapSequence<Self, Result<T, Failure>>
func mapFailure<E>(_ transform: (Failure) -> E) -> AsyncMapSequence<Self, Result<Success, E>>
func flatMap<T>(_ transform: (Success) -> Result<T, Failure>) -> AsyncMapSequence<Self, Result<T, Failure>>
// Async
func mapAsync<T>(_ transform: (Success) async -> T) -> AsyncMapSequence<Self, Result<T, Failure>>
func mapFailureAsync<E>(_ transform: (Failure) async -> E) -> AsyncMapSequence<Self, Result<Success, E>>
func flatMapAsync<T>(_ transform: (Success) async -> Result<T, Failure>) -> AsyncMapSequence<Self, Result<T, Failure>>// Sync
func tap(_ action: (Success) -> Void) -> AsyncMapSequence<Self, Result<Success, Failure>>
func tapError(_ action: (Failure) -> Void) -> AsyncMapSequence<Self, Result<Success, Failure>>
// Async
func tapAsync(_ action: (Success) async -> Void) -> AsyncMapSequence<Self, Result<Success, Failure>>
func tapErrorAsync(_ action: (Failure) async -> Void) -> AsyncMapSequence<Self, Result<Success, Failure>>Element transforms run in parallel; output preserves source arrival order.
// General form
func mapAsyncKeepOrder<T: Sendable>(
_ transform: @Sendable @escaping (Element) async -> T
) -> AsyncStream<T>
where Self: Sendable, Failure == Never, Element: Sendable
// Result overload — transforms successes, passes failures through
func mapAsyncKeepOrder<Success: Sendable, E: Error, T: Sendable>(
_ transform: @Sendable @escaping (Success) async -> T
) -> AsyncStream<Result<T, E>>
where Self: Sendable, Failure == Never, Element == Result<Success, E>Create single-element Result streams:
static func success<Success, Failure>(_ value: Success) -> AsyncStream<Result<Success, Failure>>
static func failure<Success, Failure>(_ error: Failure) -> AsyncStream<Result<Success, Failure>>Extensions for AsyncStream.Continuation when the element type is Result<Success, Failure>:
// Yield success/failure values
func success<Success, Failure>(_ value: Success) -> YieldResult
func failure<Success, Failure>(_ error: Failure) -> YieldResult
// Yield and finish the stream
func finishWithSuccess<Success, Failure>(_ value: Success)
func finishWithFailure<Success, Failure>(_ error: Failure)@discardableResult func match<T>(_ onSome: (Wrapped) -> T, _ onNone: @autoclosure () -> T) -> T
@discardableResult func matchAsync<T>(_ onSome: (Wrapped) async -> T, _ onNone: @autoclosure () -> T) async -> Tfunc mapAsync<T>(_ transform: (Wrapped) async -> T) async -> T?
func flatMapAsync<T>(_ transform: (Wrapped) async -> T?) async -> T?func orElse<T>(_ defaultValue: T) -> T?MIT