Skip to content

Commit c638c64

Browse files
committed
feat: Update README.md
1 parent d0f99e3 commit c638c64

1 file changed

Lines changed: 118 additions & 1 deletion

File tree

README.md

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,121 @@
1111

1212
## Motivation
1313

14+
Function composition is a practical way to build larger behavior from small, focused transformations. Keeping each step small makes it easier to test, reuse, and reason about, while composition keeps the final pipeline readable and local to the call site.
15+
16+
In Swift, that style is often expressed with plain closure types and lightweight helpers. That works well for straightforward cases, and similar composition tools already exist in the ecosystem.
17+
18+
The harder problem appears when composition and overload selection need to account for more than a raw `(A) -> B` shape. Once `async`, `throws`, `Sendable`, or `MainActor` isolation become part of the contract, structural function types do not give the compiler a strong nominal surface to resolve against reliably.
19+
20+
`swift-function-composition` approaches that problem by wrapping closures in nominal types and composing those explicit wrapper types instead of raw signatures. That gives overloads something concrete to target and lets composed values preserve the strongest semantics available in the chain.
21+
1422
## Usage
1523

24+
The examples below assume you import `FunctionComposition` and enable `NominalTypes` plus the corresponding `Operators`, `Methods`, or `Functions` traits in SwiftPM.
25+
26+
### 1. Choose a wrapper
27+
28+
Use the base wrappers when you only need the core effect model:
29+
30+
- `SyncFunc<Input, Output>`
31+
- `SyncThrowingFunc<Input, Output, Failure>`
32+
- `AsyncFunc<Input, Output>`
33+
- `AsyncThrowingFunc<Input, Output, Failure>`
34+
35+
Specialized variants refine those same families:
36+
37+
- `Sendable...` variants represent functions whose value is sendable.
38+
- `MainActor...` variants represent functions isolated to `MainActor`.
39+
40+
All wrappers expose `run(with:)` and `callAsFunction(_:)`, so they can be invoked explicitly or used much like ordinary closures.
41+
42+
### 2. Compose wrapped functions
43+
44+
Start by wrapping the functions you want to combine:
45+
46+
```swift
47+
import FunctionComposition
48+
49+
let isNotZero = SyncFunc<Int, Bool> { $0 != 0 }
50+
let describe = SyncFunc<Bool, String> { $0 ? "true" : "false" }
51+
```
52+
53+
The same composition model is available through operators, methods, and free functions:
54+
55+
Operators:
56+
57+
```swift
58+
let f = SyncFunc(\.count) <<< describe <<< isNotZero
59+
let result = f.run(with: 10) // 2
60+
```
61+
62+
Methods:
63+
64+
```swift
65+
let f = describe.compose(isNotZero).compose(SyncFunc(\.count))
66+
let result = f.run(with: 10) // 2
67+
```
68+
69+
Functions:
70+
71+
```swift
72+
let f = compose(SyncFunc(\.count), describe, isNotZero)
73+
let result = f.run(with: 10) // 2
74+
```
75+
76+
> [!Note]
77+
>
78+
> _Functions API is more limited for chaining than Methods or Operators APIs:_
79+
>
80+
> - _Max variadic parameters count is 4_
81+
> - _Variadic parameters overloads will preserve MainActor only if all accepted wrappers are MainActor wrappers_
82+
83+
### 3. Convert into stronger wrappers when needed
84+
85+
Base wrappers can be promoted into stronger variants when you know more about the function than the original type encodes.
86+
87+
Use `.uncheckedSendable()` to wrap a base function as a sendable one when you know that crossing concurrency boundaries is safe:
88+
89+
```swift
90+
let sendableIsNotZero = SyncFunc<Int, Bool> { $0 != 0 }
91+
.uncheckedSendable()
92+
```
93+
94+
Use `.mainActor()` on sendable wrappers when `Input` and `Output` are `Sendable` and the function should be treated as main-actor isolated:
95+
96+
```swift
97+
let mainActorDescribe = SendableSyncFunc<Bool, String> { $0 ? "true" : "false" }
98+
.mainActor()
99+
```
100+
101+
These conversion helpers are intentionally explicit. `uncheckedSendable()` is an unchecked promise made by the caller, while `mainActor()` upgrades a sendable wrapper into the corresponding `MainActor` variant.
102+
103+
### 4. Preservation rules
104+
105+
Composition returns the nominal wrapper that matches the strongest semantics required by the chain. The effect model combines monotonically:
106+
107+
- `sync` + `async``async`
108+
- `non-throwing` + `throwing``throwing`
109+
- `Sendable` + `~Sendable``~Sendable`
110+
- `Sendable` + `MainActor` becomes `MainActor`
111+
- `~Sendable` + `MainActor` is not supported
112+
113+
For example:
114+
115+
```swift
116+
let loadFlag = SendableAsyncThrowingFunc<Int, Bool, Never> { $0 != 0 }
117+
let describe = SendableSyncFunc<Bool, String> { $0 ? "true" : "false" }
118+
119+
// SendableAsyncThrowingFunc<Int, String, Never>
120+
let f = describe <<< loadFlag
121+
```
122+
123+
When two throwing functions use different failure types, the composed failure is represented with `Either`. `MainActor` wrappers preserve main-actor isolation for the compatible sendable compositions supported by the library.
124+
125+
### 5. Choose an API surface
126+
127+
`FunctionComposition` re-exports the nominal composition modules that match the traits enabled in your package. Enable `Operators`, `Methods`, or `Functions` to choose the surface you prefer, and use `pipe(...)` when you want the free-function API in forward order.
128+
16129
## Installation
17130

18131
### Basic
@@ -31,7 +144,7 @@ If you use SwiftPM for your project structure, add `swift-function-composition`
31144
.package(
32145
url: "https://github.com/capturecontext/swift-function-composition.git",
33146
.upToNextMinor(from: "0.0.1"),
34-
traits: [<#Traits#>]
147+
traits: [<#Traits#>] // swift-tools-version>=6.1
35148
)
36149
```
37150

@@ -51,6 +164,10 @@ Do not forget about target dependencies
51164
)
52165
```
53166

167+
> [!Note]
168+
>
169+
> _Some products like `FunctionComposition` require `swift-tools-version>=6.1`, if you're using older toolchain, refer to [Package@swift-6.0.swift](Package@swift-6.0.swift) to figure out supported products_
170+
54171
## License
55172

56173
This library is released under the MIT license. See [LICENSE](LICENSE) for details.

0 commit comments

Comments
 (0)