Skip to content

Commit bdecd86

Browse files
committed
Allow "ad-hoc" mapping of promises
1 parent 1fa924f commit bdecd86

File tree

4 files changed

+131
-12
lines changed

4 files changed

+131
-12
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All that's new is also compatible with message authors and ad-hoc transform chai
66
* Promises:
77
* `Promise<Value>` is a `Messenger<Value>` with some conveniences for async returns
88
* Promise composition functions `promise`, `then` and `and`
9+
* Promise value mapping functions `map(...)`, `unwrap(default)` and `new()`
910
* Free Observers:
1011
* Class for adhoc observers `FreeObserver`
1112
* Global function `observe(...)`, and `observed(...)` on observables, both use `FreeObserver.shared`
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
public extension Promise
2+
{
3+
func map<Mapped>(_ map: @escaping (Value) -> Mapped) -> Promise<Mapped>
4+
{
5+
Promise<Mapped>
6+
{
7+
promise in
8+
9+
observedOnce
10+
{
11+
promise.fulfill(map($0), as: $1)
12+
}
13+
}
14+
}
15+
16+
func unwrap<Wrapped>(_ defaultValue: Wrapped) -> Promise<Wrapped>
17+
where Value == Wrapped?
18+
{
19+
Promise<Wrapped>
20+
{
21+
promise in
22+
23+
observedOnce
24+
{
25+
promise.fulfill($0 ?? defaultValue, as: $1)
26+
}
27+
}
28+
}
29+
30+
func new<UpdateValue>() -> Promise<UpdateValue>
31+
where Value == Update<UpdateValue>
32+
{
33+
Promise<UpdateValue>
34+
{
35+
promise in
36+
37+
observedOnce
38+
{
39+
promise.fulfill($0.new, as: $1)
40+
}
41+
}
42+
}
43+
}

README.md

+30-12
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ SwiftObserver diverges from convention as it doesn't inherit the metaphors, term
2828
* [Introduction](#introduction)
2929
* [Messengers](#messengers)
3030
* [Understand Observables](#understand-observables)
31-
* [Use Messengers as Promises](#use-messengers-as-promises)
31+
* [Promises](#promises)
32+
* [Receive a Promised Value](#receive-a-promised-value)
33+
* [Compose Promises](#compose-promises)
3234
* [Variables](#variables)
3335
* [Observe Variables](#observe-variables)
3436
* [Use Variable Values](#use-variable-values)
@@ -149,7 +151,7 @@ class Sky: Observable {
149151

150152
1. Create a [`Messenger<Message>`](#messengers). It's a mediator through which other entities communicate.
151153
2. Create an object of a [custom `Observable`](#understand-observables) class that utilizes `Messenger<Message>`.
152-
3. Create a [`Promise<Value>`](#use-messengers-as-promises). It's a Messenger with conveniences for asynchronous returns.
154+
3. Create a [`Promise<Value>`](#promises). It's a Messenger with conveniences for asynchronous returns.
153155
4. Create a [`Variable<Value>`](#variables) (a.k.a. `Var<Value>`). It holds a value and sends value updates.
154156
5. Create a [*transform*](#make-transforms-observable) object. It wraps and transforms another `Observable`.
155157

@@ -245,13 +247,15 @@ class Model: SuperModel, Observable {
245247
}
246248
~~~
247249

248-
## Use Messengers as Promises
250+
# Promises
249251

250-
A `Promise<Value>` is basically just a `Messenger<Value>`. It makes our intention more explicit when we use messengers for managing and chaining asynchronous returns.
252+
A `Promise<Value>` is basically just a `Messenger<Value>`. It helps managing asynchronous returns and makes that intention more explicit.
251253

252254
> **Side Note:** `Promise` is part of SwiftObserver because [Combine's `Future`](https://developer.apple.com/documentation/combine/future) is unfortunately not a practical solution for one-shot asynchronous calls, to depend on [PromiseKit](https://github.com/mxcl/PromiseKit) might be unnecessary in reasonably simple contexts, and [Vapor/NIO's Async](https://docs.vapor.codes/4.0/async/) might also be too server-specific. Anyway, integrating promises as regular observables yields some consistency, simplicity and synergy here. However, at some point *all* promise/future implementations will be obsolete due to [Swift's async/await](https://github.com/DougGregor/swift-evolution/blob/async-await/proposals/nnnn-async-await.md).
253255
254-
### Receive a Promised Value
256+
## Receive a Promised Value
257+
258+
### Receive It Once
255259

256260
```swift
257261
func getID() -> Promise<Int> { // getID() promises an Int
@@ -267,9 +271,9 @@ getID().observed { id in // observation by FreeObserver.shared
267271
}
268272
```
269273

270-
Typically, promises are shortlived observables that you don't store anywhere. That works fine since an asynchronous function like `getID()` that returns a promise keeps that promise alive in order to fulfill it. So you can (globally) observe such a promise without even storing it, and the promise as well as its observations get cleaned up automatically when the promise is fulfilled and dies.
274+
Typically, promises are shortlived observables that you don't hold on to. That works fine since an asynchronous function like `getID()` that returns a promise keeps that promise alive in order to fulfill it. So you can (globally) observe such a promise without even holding it anywhere, and the promise as well as its observations get cleaned up automatically when the promise is fulfilled and dies.
271275

272-
### Receive a Promised Value Again
276+
### Receive It Again
273277

274278
Sometimes, you want to do multiple things with an asynchronous result (long) after receiving it. In that case you may keep an [`ObservableCache`](#cached-messages) of the promise, so the promised value will be cached:
275279

@@ -285,14 +289,14 @@ idCache.whenCached { id in
285289
}
286290
```
287291

288-
### Compose Promises
292+
## Compose Promises
289293

290294
Inspired by PromiseKit, SwiftObserver allows to compose asynchronous calls using promises.
291295

292-
#### Sequential Composition
296+
### Sequential Composition
293297

294298
```swift
295-
promise { // just for nice consistent closure syntax
299+
promise { // establish context and increase readability
296300
getInt() // return Promise<Int>
297301
}.then { // chain another promise sequentially
298302
getString(takeInt: $0) // take Int sent by 'promise', return Promise<String>
@@ -301,11 +305,11 @@ promise { // just for nice consistent closure syntax
301305
}
302306
```
303307

304-
`promise` is only for readability. It takes a closure that returns a `Promise` and simply returns that `Promise`.
308+
`promise` is for readability. It allows for nice consistent closure syntax and makes it clear that we're working with promises. It takes a closure that returns a `Promise` and simply returns that `Promise`.
305309

306310
You call `then` on a first `Promise` and pass it a closure that returns the second `Promise`. That closure takes the value of the first promise, allowing the second promise to depend on it. `then` returns a new `Promise` that provides the value of the second promise.
307311

308-
#### Concurrent Composition
312+
### Concurrent Composition
309313

310314
```swift
311315
promise {
@@ -320,6 +324,20 @@ promise {
320324

321325
You call `and` on a `Promise` and pass it a closure that returns another `Promise`. This immediatly observes both promises. `and` returns a new `Promise` that provides the combined values of both promises.
322326

327+
### Value Mapping
328+
329+
A transform function that neither filters messages nor exclusively creates a standalone transform will create a new `Promise` when called on a `Promise`. These functions are `map(...)`, `unwrap(default)` and `new()`. The advantage here is, as with any function that returns a promise, that you don't need to keep that observable alive in order to observe it:
330+
331+
```swift
332+
promise {
333+
getInt()
334+
}.map { // chain a mapping promise sequentially
335+
"\($0)" // map Int to String
336+
}.observed {
337+
print($0) // print String sent by 'map'
338+
}
339+
```
340+
323341
# Variables
324342

325343
`Var<Value>` is an `Observable` that has a property `value: Value`.

Tests/SwiftObserverTests/PromiseTests.swift

+57
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,63 @@ class PromiseTests: XCTestCase
161161
waitForExpectations(timeout: 3)
162162
}
163163

164+
func testMapPromiseWithoutHoldingMapping()
165+
{
166+
let receivedValue = expectation(description: "received value")
167+
168+
promise
169+
{
170+
asyncFunc(returnValue: 42)
171+
}
172+
.map
173+
{
174+
"\($0)"
175+
}
176+
.observed
177+
{
178+
XCTAssertEqual($0, "42")
179+
receivedValue.fulfill()
180+
}
181+
182+
waitForExpectations(timeout: 3)
183+
}
184+
185+
func testUnwrapWithDefaultOnPromiseWithoutHoldingTransform()
186+
{
187+
let receivedValue = expectation(description: "received value")
188+
189+
Promise<Int?>
190+
{
191+
promise in DispatchQueue.main.async { promise.fulfill(nil) }
192+
}
193+
.unwrap(42)
194+
.observed
195+
{
196+
XCTAssertEqual($0, 42)
197+
receivedValue.fulfill()
198+
}
199+
200+
waitForExpectations(timeout: 3)
201+
}
202+
203+
func testNewOnPromiseWithoutHoldingTransform()
204+
{
205+
let receivedValue = expectation(description: "received value")
206+
207+
Promise<Update<Int>>
208+
{
209+
promise in DispatchQueue.main.async { promise.fulfill(Update(23, 42)) }
210+
}
211+
.new()
212+
.observed
213+
{
214+
XCTAssertEqual($0, 42)
215+
receivedValue.fulfill()
216+
}
217+
218+
waitForExpectations(timeout: 3)
219+
}
220+
164221
func asyncFunc() -> Promise<Void>
165222
{
166223
Promise { promise in DispatchQueue.main.async { promise.fulfill(()) } }

0 commit comments

Comments
 (0)