You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+118-1Lines changed: 118 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,8 +11,121 @@
11
11
12
12
## Motivation
13
13
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
+
14
22
## Usage
15
23
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
+
importFunctionComposition
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
+
16
129
## Installation
17
130
18
131
### Basic
@@ -31,7 +144,7 @@ If you use SwiftPM for your project structure, add `swift-function-composition`
@@ -51,6 +164,10 @@ Do not forget about target dependencies
51
164
)
52
165
```
53
166
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
+
54
171
## License
55
172
56
173
This library is released under the MIT license. See [LICENSE](LICENSE) for details.
0 commit comments