Apple выпустила пакет алгоритмов async swift, в котором представлены полезные способы работы с асинхронными последовательностями.

Около месяца назад Apple выпустила первую версию пакета асинхронные быстрые алгоритмы. Он предоставляет инструменты и алгоритмы для использования с введенной не так давно асинхронной последовательностью. Пакет фокусируется на реализации уже известных инструментов, таких как zip, а также на новых функциях, которые работают вовремя (вау). Это также делает доступными более сложные способы создания и управления асинхронными последовательностями.

💥 Последняя версия модуля 0.0.1, что означает, что он все еще находится в разработке. Итак, некоторые методы пока недоступны, некоторые могут измениться или появиться.

В основном эта статья предназначена для того, чтобы познакомиться с новыми функциями и, возможно, спланировать свой код, имея в виду, что такие функции будут появиться в будущем

Монтаж

Новый пакет распространяется через Swift PM. Чтобы добавить его в свой проект, вам нужно добавить его как зависимость в проект Xcode File > Add Packages.

Или добавьте его в свой Package.swift файл:

.package(url: "https://github.com/apple/swift-async-algorithms"),

Не забудьте также добавить зависимость к исполняемому файлу:

.target(name: "<target>", dependencies: [
    .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
]),

Модуль будет доступен в вашем проекте после добавления import AsyncAlgorithms.

💥 Как я уже говорил, модуль все еще находится в разработке. Итак, вам нужно установить Swift Trunk Development toolchain, чтобы иметь доступ ко всем функциям.
Однако некоторые из них доступны уже сейчас!

Создание асинхронных последовательностей

Чтобы протестировать все прекрасные функции, которые предоставляет новый модуль, нам нужно сначала создать асинхронную последовательность. И пакет представляет новые способы сделать это.

Недвижимость async

Модуль добавляет следующее расширение к протоколу Sequence.

extension Sequence {
  public var async: AsyncLazySequence<Self> { get }
}

Где AsyncLazySequence соответствует AsyncSequence.

public struct AsyncLazySequence<Base: Sequence>: AsyncSequence {
}

extension AsyncLazySequence: Sendable where Base: Sendable {
	...
}
extension AsyncLazySequence.Iterator: Sendable where Base.Iterator: Sendable {
}

💡 Используя свойство async, мы можем превратить любую существующую последовательность в AsyncSequence, чтобы использовать их, например, в каком-нибудь асинхронном API.

let numbers = [1, 2, 3, 4].async
let characters = "Hello, world".async
let items = [1: "one", 2: "two", 3: "three"].async

Тем не менее, создание AsyncSequence таким образом не приносит пользы, так как все элементы уже здесь и доступны сразу. Есть более полезные способы создания AsyncSequence.

AsyncChannel и AsyncThrowingChannel

Если вы знаете, что такое Future или Promise в других языках, то AsyncChannel будет вам знакомо. За исключением того, что он предоставляет способ передачи последовательности значений.

❗Элемент Channel должен соответствовать протоколу Sendable, что в основном означает, что общедоступный API безопасно использовать в доменах параллелизма.

Все основные типы автоматически соответствуют ему. Для пользовательских типов необходимо добавить соответствие перед использованием.

Вот довольно простой пример использования AsyncChannel.

let channel = AsyncChannel<String>()
Task {
    for word in ["Hello", "from", "async", "channel"] {
      await channel.send(word)
    }
    await channel.finish()
}

for await message in channel {
    print(message)
}

Выход:

Hello
from
async
channel

Обратите внимание, что ключевое слово await используется с отправкой и завершением. Это связано с тем, что канал фактически синхронизирован в обоих направлениях. Это означает, что send ожидает потребления и наоборот.

💡 await channel.send() ждет, пока отправленное значение не будет использовано каким-либо образом. Таким образом, тот, кто производит значения для канала, не будет генерировать больше значений, чем может потреблять получатель.

AsyncThrowingStream почти такой же, за исключением того, что он предоставляет метод fail(_ error: Error), который можно использовать для выдачи исключения потребителю канала.

let channel = AsyncThrowingChannel<String, Error>()

...

for try await message in channel {
    print(message)
}

И преобразование обратно

Модуль добавляет инициализаторы для трех основных типов: Array, Dictionary и Set, которые позволяют преобразовать асинхронную последовательность в обычную путем выборки всех элементов во время инициализации.

let table = await Dictionary(uniqueKeysWithValues: zip(keys, values))
let allItems = await Set(items.prefix(10))
let allMessages = await Array(channel)

Управление асинхронными последовательностями

Модуль также предоставляет новые способы объединения асинхронных последовательностей. Эти функции довольно просты.

  • chain(_ s1: AsyncSequence, _ s2: AsyncSequence)

Последовательно соединяет вместе две или три асинхронные последовательности, в которых элементы результата состоят из элементов первой асинхронной последовательности, а затем второй (и т. д.) или до возникновения ошибки. Последовательности должны иметь одинаковый тип Element.

💥 Apple отмечает, что его можно использовать для двухили болеепоследовательностей. Правда, сейчас доступны только два или три аргумента.

  • joined() or joined(separator: AsyncSequence)

Объединяет асинхронную последовательность асинхронных последовательностей вместе, где результат состоит из элементов первой асинхронной последовательности, а затем второй (и так далее) или до возникновения ошибки. Аналогично chain()за исключением того, что количество асинхронных последовательностей для объединения заранее неизвестно. Также можно указать разделитель.

  • combineLatest(_ base1: AsyncSequence, _ base2: AsyncSequence)

Объединяет две последовательности или более, создавая кортежи последних значений, доступных из последовательности.

  • merge(_ base1: AsyncSequence, _ base2: AsyncSequence)

Объединяет последовательности в новую. Результат представляет собой комбинацию результатов двух последовательностей. Последовательности должны иметь одинаковый тип Element.

💡 Учитывая, что не определено, из какого элемента последовательности появится быстрее, порядок элементов может быть любым

  • zip(_ base1: AsyncSequence, _ base2: AsyncSequence)

То же, что и обычный zip, но для AsyncSequence. Отличается от combineLatest тем, что ожидает появления второго значения и не использует последнее значение.

Функции, связанные со временем

Звучит круто, но Swift недостаточно мощен, чтобы поставить await перед самим временем. Когда события потенциально могут происходить быстрее, чем желаемая скорость потребления, есть способы справиться с ситуацией. Эти функции позволяют связать AsyncSequences со временем. Их можно применить к любому AsyncSequence.

Для обоих перечисленных методов можно указать собственные часы. По умолчанию это ContinuousClock

Отказаться

public func debounce<C: Clock>(
    for interval: C.Instant.Duration, 
    tolerance: C.Instant.Duration? = nil, 
    clock: C
  ) -> AsyncDebounceSequence<Self, C>

Алгоритм устранения дребезга создает элементы после того, как между событиями прошло определенное время. Если происходит много событий, debounce будет ждать, пока не пройдет не менее interval времени с момента последнего события, прежде чем выдать значение.

seq.debounce(for: .seconds(1))

В этом случае он преобразует потенциально быструю асинхронную последовательность событий в последовательность, ожидающую 1-секундного окна без событий перед отправкой значения.

Дроссель

Алгоритм дросселя создает такие элементы, между которыми прошел как минимум определенный интервал. Если значения создаются базой AsyncSequence, дроссель не возобновляет свой следующий итератор до тех пор, пока не истечет период или пока не произойдет терминальное событие. Аналогично debounce можно указать пользовательские часы.

seq.throttle(for: .seconds(1))

В этом случае дроссель преобразует потенциально быструю асинхронную последовательность событий в такую, которая ждет, пока истечет окно в 1 секунду, прежде чем выдать значение.

💡 Обратите внимание, что debounce ожидает окна без событий, а Throttle просто ожидает окна.

Заключительные заметки

На самом деле откровенно интересно наблюдать за тем, как Swift раскрывает новые функции и как они разрабатываются. Обязательно проверьте GitHub проекта, упомянутый в ссылках, чтобы проверить исходный код модуля.

Если вы не очень уверены в относительно новых функциях параллелизма в Swift, ознакомьтесь с моим кратким руководством по async/await в Swift.



Want to Connect?
This post was originally published on alexdremov.me

Рекомендации