o

ohmycloud

V1

2022/09/29阅读:18主题:橙心

Swift Combine vs. Raku Supply

Swift Combine

Combine 框架是 Apple 的声明式异步处理框架。

In Apple’s own words: “The Combine framework provides a declarative approach for how your app processes events. Rather than potentially implementing multiple delegate callbacks or completion handler closures, you can create a single processing chain for a given event source. Each part of the chain is a Combine operator that performs a distinct action on the elements received from the previous step.”

Raku Supply

Raku 中内置了异步处理, 即 Supply。

Asynchronous data stream with multiple subscribers

A supply is a thread-safe, asynchronous data stream like a Channel, but it can have multiple subscribers (taps) that all get the same values flowing through the supply.

It is a thread-safe implementation of the Observer Pattern, and central to supporting reactive programming in Raku.

Swift Combine vs. Raku Supply

下面的 Swift 代码来自《Combine Asynchronous Programming with Swift》第三版, Combine 框架实现了很多转换操作符(transforming operators), 比 Raku 自带的异步转换函数还要丰富。

我们先看一看响应式流处理在 Swift 和 Raku 中分别是什么样的:

["A""B""C""D""E"].publisher
  .sink(receiveCompletion: { print($0) },
        receiveValue: { print($0) })
  .store(in: &subscriptions)

上面的代码使用 publisher 方法从数组上创建一个 Publisher, sink 方法用来订阅, 它可以接收两个参数: receiveCompletionreceiveValue, 这两个参数的数据类型都是闭包。

等价的 Raku 代码如下, from-list 从列表 'A' .. 'E' 创建一个 Supply:

my Supply $s = Supply.from-list('A' .. 'E');

$s.tap(
    &say,
    done => { say "finished" },
    quit => { say "quit"     }
);

tap 子例程相当于 Swift Combine 框架中的 sink 方法, 它创建了一个订阅(subscription)。第一个位置参数是一块儿代码, 每当发起 emit 调用而有一个可用的新值时, 第一个位置参数的代码块儿就会执行。

了解了 Swift Combine 和 Raku Supply 的基本知识之后, 我们下面使用 Raku 来实现 Swift Combine 框架中的部分转换操作符。

  • first

Combine 中的 first 转换函数接收一个 closure, 即返回第一个满足条件的值:

let numbers = (1...9).publisher

numbers
  .first(where: { $0 % 2 == 0 })
  .sink(receiveCompletion: { print("Completed with: \($0)") },
        receiveValue: { print($0) })
  .store(in: &subscriptions)

Raku Supply 中的 first 子例程可以接收一个 WhateverCode:

my Supply $supply = Supply.from-list(1..9);
my Supply $first = $supply.first(* % 2 == 0);

$first.tap(
    &say,
    done => { say 'finished' },
    quit => { say 'quit'     }
);

输出:

2
finished
  • collect

Swift Combine 框架中的 collect 操作符提供了一种便捷的方式把单独的值转换成单个数组:

["A""B""C""D""E"].publisher
.collect(2)
.sink(receiveCompletion: { print($0) },
      receiveValue: { print($0) })
.store(in: &subscriptions)

Raku 的 Supply 没有 collect 子例程, 但是有一个 rotor 子例程可以实现同样的效果:

my Supply $supply = Supply.from-list('A' .. 'E').rotor(2 , :partial);

$supply.tap(
    &say,
    done => { say "finished" },
    quit => { say "quit"     }
);

输出:

[A B]
[C D]
[E]
finished
  • map

Swift Combine 框架中的 map 就像标准库中的 map 一样, 只不过 Combine 中的 map 操作的是来自 Publisher 发出的值:

[1,2,3].publisher
  .map { $0 * 2 }
  .sink(receiveCompletion: { print($0) },
        receiveValue: { print($0) })
  .store(in: &subscriptions)

在上面的代码中, map 接收一个闭包, 这个闭包进行的操作是把传入的值加倍。

类似地, Raku 中的 map 接收一个闭包:

my Supply $supply  = Supply.from-list(1..3);
my Supply $doubled = $supply.map: -> $value { $value * 2 };

$doubled.tap(
    &say,
    done => { say 'finished' },
    quit => { say 'quit'     }
);

输出:

2
4
6
finished

此外, 上面的 map 还能写成这样:

$supply.map(* * 2)
  • flatMap

在 Swift Combine 中, flatMap 操作符把多个上游的 Publisher 展平成单个下游 Publisher。或者, 说得更具体一点, flatMap 操作符把这些 Publisher 发出的元素展平了:

func decode(_ codes: [Int]) -> AnyPublisher<StringNever> {
  Just(
    codes
      .compactMap { code in
        guard (32...255).contains(code) else { return nil }
        return String(UnicodeScalar(code) ?? " ")
      }.joined()
  ).eraseToAnyPublisher()
}

[7210110810811144328711111410810033]
.publisher
.collect()
.flatMap(decode)
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)

flatMap 返回的 Publisher 的类型通常和它接收的上游 Publisher 的类型不同。

Raku 的 Supply 中没有 flatMap 子例程, 下面的代码同时使用了 map 和 reduce 得到和 Swift Combine 的 flatMap 同样的输出结果:

my Supply $supply = Supply.from-list: [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33];
my $flat-supply = $supply.map(&decode).reduce({$^a ~ $^b });

$flat-supply.tap(
        &say,
        done => { say 'finished' },
        quit => { say 'quit'     }
);

sub decode($x) {
  chr($x)
}

输出:

Hello, World!
finished
  • mapping key paths

map 运算符还可以使用 key paths 把值映射到属性里面:

let publisher = PassthroughSubject<CoordinateNever>()

publisher
  .map(\.x, \.y)
  .sink(receiveCompletion: { print($0) },
        receiveValue: {x, y in
          print("The coordinate at (\(x), \(y)) is in quadrant", quadrantOf(x: x, y: y))
        })
  .store(in: &subscriptions)

  publisher.send(Coordinate(x: 10, y: -8))
  publisher.send(Coordinate(x:  0, y:  5))

Raku 中是没有 Swift 中的 key path 概念的, 但是我尝试模拟了一下:

class Coordinate {
    has $.x;
    has $.y;

    # 计算点所在的象限
    method quadrantOf(--> Int) {
        with self {
            when .x > 0 && .y > 0 { 1 }
            when .x < 0 && .y > 0 { 2 }
            when .x < 0 && .y < 0 { 3 }
            when .x > 0 && .y < 0 { 4 }
            -1
        }
    }

    # 重写 gist 方法, 个性化打印
    method gist() {
        return "The coordinate at ({self.x}, {self.y}) is in quadrant " ~ self.quadrantOf()
    }
}

my Supplier $supplier = Supplier.new;
my Supply $supply = $supplier.Supply;

$supply.tap({ say $_ });
$supplier.emit(Coordinate.new(x =>  10, y =>  8));
$supplier.emit(Coordinate.new(x => -10, y =>  8));
$supplier.emit(Coordinate.new(x => -10, y => -8));
$supplier.emit(Coordinate.new(x =>  10, y => -8));
$supplier.emit(Coordinate.new(x =>   0, y =>  5));
$supplier.emit(Coordinate.new(x =>   0, y =>  0));
  • compactMap

compactMap 在进行 map 操作的同时会过滤掉 nil 值:

let strings = ["a""1.24""3""def""45""0.23"].publisher

strings
  .compactMap { Float($0) }
  .sink(receiveValue: {
    print($0)
  })
  .store(in: &subscriptions)
)

上面的 Swift 代码使用 compactMap 尝试从每个单独的字符串初始化一个 Float 值。如果 Float 的初始化构造器不知道如何转换所提供的字符串, 就会返回 nil。那些 nil 值会被 compactMap 操作符自动过滤掉。

Raku 中没有 compactMap, 但是可以使用 map 和 Empty 实现类似的效果:

my @strings = ["a", "1.24", "3", "def", "45", "0.23"];
my Supply $supply = Supply.from-list(@strings);
my Supply $compact = $supply.map(-> $value {
  try { Num($value) }
  $! ?? Empty !! Num($value)
 }).grep(Num);

$compact.tap(
    &say,
    done => { say 'finished' },
    quit => { say 'done'     }
);

但是最后还需要用 grep 过滤掉空列表, 因为 Supply 中的 map 和标准库中的 map 行为表现不一致。

  • filter

filter 函数接收一个闭包, 这个闭包返回一个 Bool 值。filter 只把匹配了所提供的断言的值向下传递:

let numbers = (1...10).publisher

numbers
  .filter { $0.isMultiple(of: 3) }
  .sink(receiveValue: { n in
    print("\(n) is a multiple of 3!")
  })
  .store(in: &subscriptions)

在 Raku 中没有 filter, 而是叫 grep, 它可以接收一个 WhateverCode:

my Supplier $supplier = Supplier.new;
my Supply $all = $supplier.Supply;
my Supply $multiple-of-three = $all.grep(* % 3 == 0);

$multiple-of-three.tap(
    &say,
    done => { say 'finished' },
    quit => { say 'done'     }
);

$supplier.emit($_) for 1..10;
$supplier.done;
  • dropFirst

dropFirst 运算符接收一个 count 参数, 忽略 Publisher 发出的头几个值:

let numbers = (1...10).publisher

numbers
  .dropFirst(8)
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)

Raku 中使用 skip 跳过前 N 个值:

my Supply $supply = Supply.from-list(1..10);
my Supply $drop = $supply.skip(8);

$drop.tap(
    &say,
    done => { say 'finished' },
    quit => { say 'quit'     }
);
  • merge

在 Swift Combine 框架中, merge 用于合并两个 Publisher:

let publisher1 = PassthroughSubject<IntNever>()
let publisher2 = PassthroughSubject<IntNever>()

publisher1
  .merge(with: publisher2)
  .sink(
    receiveCompletion: { _ in print("Completed") },
    receiveValue: { print($0) }
  )
  .store(in: &subscriptions)

publisher1.send(1)
publisher1.send(2)
publisher2.send(3)
publisher1.send(4)
publisher2.send(5)

publisher1.send(completion: .finished)
publisher2.send(completion: .finished)

Raku 中也有个叫 merge 的子例程, 这个子里程接收两个 Supply, 合并完之后返回的也是 Supply:

my Supplier $supplier1 = Supplier.new;
my Supplier $supplier2 = Supplier.new;

my Supply $publisher1 = $supplier1.Supply;
my Supply $publisher2 = $supplier2.Supply;
my Supply $merged     = $publisher1.merge($publisher2);

$merged.tap(
    &say,
    done => { say 'finished' },
    quit => { say 'quit'     }
);

$supplier1.emit(1);
$supplier1.emit(2);
$supplier2.emit(3);
$supplier1.emit(4);
$supplier2.emit(5);

$supplier1.done;
$supplier2.done;
  • combineLatest

combineLatest 运算符可以组合不同的 Publisher。它也可以让你组合不同值类型的 Publisher, 这相当有用。
每当 Publisher 中的任意一个发出值时, combineLatest 就会发出一个元组, 这个元组中的值是所有 Publisher 中发出的最新值:

let publisher1 = PassthroughSubject<IntNever>()
let publisher2 = PassthroughSubject<StringNever>()

publisher1
  .combineLatest(publisher2)
  .sink(
    receiveCompletion: { _ in print("Completed") },
    receiveValue: { print("P1: \($0), P2: \($1)") }
  )
  .store(in: &subscriptions)

publisher1.send(1)
publisher1.send(2)
publisher2.send("a")
publisher2.send("b")
publisher1.send(3)
publisher2.send("c")

publisher1.send(completion: .finished)
publisher2.send(completion: .finished)

Raku 中与 combineLatest 类似的子例程是 zip-latest:

my Supplier $supplier1 = Supplier.new;
my Supplier $supplier2 = Supplier.new;

my Supply $publisher1 = $supplier1.Supply;
my Supply $publisher2 = $supplier2.Supply;
my Supply $merged     = $publisher1.zip-latest($publisher2);

$merged.tap(
    &say,
    done => { say 'finished' },
    quit => { say 'quit'     }
);

$supplier1.emit(1);
$supplier1.emit(2);
$supplier2.emit("a");
$supplier2.emit("b");
$supplier1.emit(3);
$supplier2.emit("c");

$supplier1.done;
$supplier2.done;
  • removeDuplicates

在 Swift Combine 中, removeDuplicates 用于移除 Publisher 中重复的值:

let words = "hey hey there! want to listen to mister mister ?"
  .components(separatedBy: " ")
  .publisher

words
  .removeDuplicates()
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)

Raku 中 unique 子例程的作用类似于 Swift Combine 框架中的 removeDuplicates 函数:

my $words = "hey hey there! want to listen to mister mister ?".words;
my Supply $supply = Supply.from-list($words);
my Supply $unique = $supply.unique(:as(&lc));

$unique.tap(
    &say,
    done => { say 'finished' },
    quit => { say 'quit' }
);

其中 unique 接收一个 Colon Pair, as 作为键, 子例程 &lc 作为值。

  • scan

scan 会把上游 Publisher 发出的当前值(current)提供给闭包, 与当前值一块儿提供给闭包的还有闭包返回的上一个值(latest):

var dailyGainLoss: Int { .random(in: -10...10) }

let august2019 = (0..<22)
  .map { _ in dailyGainLoss }
  .publisher

august2019.scan(50) { latest, current in
  max(0, latest + current)
}
.sink(receiveValue: { _ in })
.store(in: &subscriptions)

Raku 中的 produce 子例程相当于 Swift Combine 框架中的 scan:

my @dailyGainLoss = (-10..10).pick(21);
my Supply $supply = Supply.from-list(@dailyGainLoss);
my Supply $produce = $supply.produce({ max(0, $^a + $^b) }).map(-> $value { $value + 50});
$produce.tap(
    &say,
    done => { say 'finished' },
    quit => {say 'quit'      }
);
  • zip

这个操作符在相同的索引中发出成对的值的元组。它等待每个 Publisher 发出一个条目(item),然后在所有 Publisher 都在当前索引中发出一个值之后,发出单个条目元组。

let publisher1 = PassthroughSubject<IntNever>()
let publisher2 = PassthroughSubject<StringNever>()

publisher1
  .zip(publisher2)
  .sink(
    receiveCompletion: { _ in print("Completed") },
    receiveValue: { print("P1: \($0), P2: \($1)") }
  )
  .store(in: &subscriptions)

publisher1.send(1)
publisher1.send(2)
publisher2.send("a")
publisher2.send("b")
publisher1.send(3)
publisher2.send("c")
publisher2.send("d")

publisher1.send(completion: .finished)
publisher2.send(completion: .finished)

类似地, Raku 中也有个叫 zip 的子例程:

my Supplier $supplier1 = Supplier.new;
my Supplier $supplier2 = Supplier.new;

my Supply $publisher1 = $supplier1.Supply;
my Supply $publisher2 = $supplier2.Supply;
my Supply $zipped     = $publisher1.zip($publisher2);

$zipped.tap(
    &say,
    done => { say 'finished' },
    quit => { say 'quit'     }
);

$supplier1.emit(1);
$supplier1.emit(2);
$supplier2.emit("a");
$supplier2.emit("b");
$supplier1.emit(3);
$supplier2.emit("c");

$supplier1.done;
$supplier2.done;

总结

Swift 的 Combine 异步处理框架比 Raku 的 Supply 异步处理的实现更加完整。

分类:

后端

标签:

后端

作者介绍

o
ohmycloud
V1