Skip to content

Latest commit

 

History

History
316 lines (209 loc) · 9.79 KB

File metadata and controls

316 lines (209 loc) · 9.79 KB

DeferredTask

DeferredTask<A> is a wrapper for a lazy async computation () async -> A.

It solves the deferred execution problem: a Task in Swift starts running the moment you create it. DeferredTask lets you describe an async computation as a value — compose it, transform it, chain it — without running anything until you explicitly call .run(). This makes async code as composable and testable as pure functions.

import CoreFP

let fetchUser = DeferredTask<User> {
    await api.getUser(id: 42)
}

// Nothing has happened yet. The network call hasn't started.
let user = await fetchUser.run()  // Only now does it execute

<£> and <&> — Map

Transform the result of a task without running it.

let fetchName = { $0.name } <£> fetchUser   // DeferredTask<String> — still deferred
let name = await fetchName.run()            // runs and maps in one shot

fetchUser <&> { $0.name }  // same, container on left

// Named function
fetchUser.fmap { $0.name }
DeferredTask.fmap { $0.name }(fetchUser)

£> and — Replace

Replace the result with a constant. The task still runs (for its side effects), but its value is discarded.

let pingOnly = fetchUser £> ()   // DeferredTask<Void> — runs but discards result
await pingOnly.run()

()  fetchUser  // same

// Named function
fetchUser.replace(())

<*> — Apply (parallel execution)

Apply a task-wrapped function to a task-wrapped value. Both tasks start concurrently.

let applyTransform = DeferredTask<(User) -> String> { await loadFormatter() }
let fetchedUser    = fetchUser

let result = await (applyTransform <*> fetchedUser).run()
// Both loadFormatter() and getUser() run concurrently

// Named function
applyDeferredTask(applyTransform, fetchedUser)

*> and <* — Sequence

Run two tasks (concurrently if possible), keeping only one result.

let log    = DeferredTask<Void>   { await analytics.track("fetch") }
let fetch  = DeferredTask<String> { await api.getName() }

await (log *> fetch).run()   // both run, only fetch result returned
await (fetch <* log).run()   // both run, only fetch result returned

// Named functions
fetch.seqRight(log)
fetch.seqLeft(log)

>>- and -<< — Bind (flatMap)

Chain two tasks where the second depends on the result of the first. They run serially.

let fetchPermissions = fetchUser >>- { user in
    DeferredTask { await api.getPermissions(for: user) }
}
// user must finish before permissions fetch can start

let permissions = await fetchPermissions.run()

// Contrast with <*>: use >>- when step 2 depends on step 1's result
//                    use <*> / zip when steps are independent

// Named functions
fetchUser.flatMap { user in DeferredTask { await api.getPermissions(for: user) } }
DeferredTask.flatMap { user in DeferredTask { await api.getPermissions(for: user) } }(fetchUser)

>=> — Kleisli composition

Compose two async functions into one pipeline.

let fetchUser:   (Int)  -> DeferredTask<User>    = { id in DeferredTask { await api.getUser(id: id) } }
let fetchOrders: (User) -> DeferredTask<[Order]> = { user in DeferredTask { await api.getOrders(for: user) } }
let summarise:   ([Order]) -> DeferredTask<String> = { orders in DeferredTask { "\(orders.count) orders" } }

let pipeline = fetchUser >=> fetchOrders >=> summarise
let summary = await pipeline(42).run()  // "N orders"

// Named function
DeferredTask.kleisli(fetchUser, fetchOrders)(42)

race — concurrent competition

race runs two tasks concurrently and returns the result of whichever completes first. The slower task is cancelled.

let cdn    = DeferredTask<Data> { await cdn.fetch(url) }
let origin = DeferredTask<Data> { await origin.fetch(url) }

let data = await race(cdn, origin).run()
// Whichever responds first wins; the other is cancelled.

race is a free function (not <|>) because DeferredTask<A> has no empty — a task that never resolves would deadlock, so a lawful Alternative instance is impossible on the base type.

For fallback semantics (first non-nil / first success), use <|> on the transformer variants — see Alternative below.


Zip — parallel combination

zip runs multiple independent tasks concurrently and combines their results.

// Two tasks run at the same time
let (user, config) = await DeferredTask.zip(fetchUser, fetchConfig).run()

// Three independent tasks
let (user, config, flags) = await DeferredTask.zip(fetchUser, fetchConfig, fetchFlags).run()

// liftA2 — combine results with a function
let greeting = await DeferredTask.liftA2 { user, config in
    "\(config.greeting), \(user.name)!"
}(fetchUser, fetchConfig).run()

Running a DeferredTask

// run() — await the result directly
let result = await myTask.run()

// eraseToTask() — convert to a Swift Task if you need to store it or cancel it
let task: Task<A, Never> = myTask.eraseToTask()
let result = await task.value
task.cancel()

// eraseToThrowingTask() — convert DeferredTask<Result<S, E>> to a throwing Task
// Bridges the Result-typed deferred world into Swift's async/throws world
let throwingTask: Task<String, any Error> =
    DeferredTask<Result<String, APIError>> { ... }.eraseToThrowingTask()
let value = try await throwingTask.value   // throws if the result was .failure

catching — construct from a throwing async closure

Wraps a throws-typed async closure into a DeferredTask<Result<S, E>>, bridging Swift's typed-throws world into the Result-based deferred world.

let safeTask: DeferredTask<Result<String, APIError>> = .catching {
    try await api.getUser(id: 42)  // async throws(APIError) -> String
}

let result = await safeTask.run()   // Result<String, APIError>

This is the counterpart to eraseToThrowingTask: catching converts into DeferredTask<Result<…>>, while eraseToThrowingTask converts out of it.


Monad Transformers

DeferredTaskTOptionalDeferredTask<A?> (outer = DeferredTask, inner = Optional)

A deferred computation that may or may not produce a value.

let maybeUser: DeferredTask<User?> = DeferredTask { await api.findUser(name: "Alice") }

{ $0.name } <£^> maybeUser   // DeferredTask<String?> — maps inside Optional
let name = await ({ $0.name } <£^> maybeUser).run()  // String?

DeferredTaskTResultDeferredTask<Result<A, E>> (outer = DeferredTask, inner = Result)

A deferred computation that may fail with a typed error.

let fetchResult: DeferredTask<Result<User, APIError>> = DeferredTask {
    do { return .success(try await api.getUser(id: 42)) }
    catch { return .failure(error as! APIError) }
}

{ $0.name } <£^> fetchResult   // DeferredTask<Result<String, APIError>>

Alternative — <|> on transformer variants

Both DeferredTaskTOptional and DeferredTaskTResult support <|>. Unlike race, these have a meaningful empty value (nil and .failure(…) respectively), so they form lawful Alternative instances.

Both start concurrently; the slower task is cancelled when the first winner is found.

// DeferredTask<A?> — first non-nil wins; both-nil returns nil
let primary:  DeferredTask<User?> = DeferredTask { await primaryDB.find(id: 42) }
let fallback: DeferredTask<User?> = DeferredTask { await replicaDB.find(id: 42) }
let user = await (primary <|> fallback).run()   // User? — whichever is non-nil first

// Named function
altDeferredTaskOptional(primary, fallback)

// DeferredTask<Result<A,E>> — first .success wins; both-fail returns last failure
let fast: DeferredTask<Result<Data, APIError>> = DeferredTask { await cdn.fetch() }
let slow: DeferredTask<Result<Data, APIError>> = DeferredTask { await origin.fetch() }
let data = await (fast <|> slow).run()   // Result<Data, APIError>

// Named function
altDeferredTaskResult(fast, slow)

For racing two DeferredTask<A> values with no empty/fallback semantics, use race instead.


DeferredTaskTArrayDeferredTask<[A]> (outer = DeferredTask, inner = Array)

A deferred computation that produces a collection.

let fetchAll: DeferredTask<[User]> = DeferredTask { await api.getAllUsers() }

{ $0.name } <£^> fetchAll   // DeferredTask<[String]>

Combine bridges (macOS 12+ / iOS 15+ / Apple platforms only)

Convert between DeferredTask and Combine's AnyPublisher. Both directions preserve the deferred/lazy contract — no work starts until a subscriber attaches (publisher direction) or .run() is called (task direction).

DeferredTaskAnyPublisher

Emit the single task result as a publisher element, then complete.

let task = DeferredTask<Int> { 42 }

let publisher: AnyPublisher<Int, Never> = task.toPublisher()

// Lazy: no task runs until a subscriber attaches
publisher.sink(
    receiveCompletion: { _ in },
    receiveValue: { print($0) }   // prints 42
).store(in: &cancellables)

// Each subscriber gets its own independent execution
publisher.sink(...)   // separate task run
publisher.sink(...)   // another separate task run

Cancelling the subscription cancels the underlying Task.

AnyPublisherDeferredTask

Collect publisher emissions into a DeferredTask. The publisher is not subscribed until .run() is called.

let publisher: AnyPublisher<Int, Never> = somePublisher()

// First emitted value only (nil if publisher completes empty)
let firstTask: DeferredTask<Int?> = publisher.toDeferredTask()
let first = await firstTask.run()   // Int?

// All emitted values collected into an array
let allTask: DeferredTask<[Int]> = publisher.toDeferredTaskArray()
let all = await allTask.run()   // [Int]

Module

import CoreFP          // DeferredTask type + named functions
import CoreFPOperators // Operators (<£>, <*>, >>-, >=>…)