Выполнять несколько одновременных задач (async let) и выдавать ошибку только в случае сбоя всех из них

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

Вот распространенный пример, но он выдает ошибку, если какой-либо из запросов не выполнен:

func test() async throws -> [Any] {
    async let t1 = getArr()
    async let t2 = getArr2()
    return try await t1 + t2
}

func getArr() async throws -> [Any] {
    []
}

func getArr2() async throws -> [Any] {
    []
}

В то же время, если я использую try?, я теряю ошибку, возвращаемую запросом.

Я также мог бы удалить throws и заменить возвращаемый результат на Result<[Any], Error>, но это выглядит странно.

Как правильно решить эту проблему?

Чтобы внести ясность: вы хотите test выдать ошибку тогда и только тогда, когда оба getArr и getArr2 выдают ошибки? Если только один из них выдает ошибку, то test должен вернуть массив, который вернул другой метод (который не выдал ошибку). Это правильно?

Sweeper 29.04.2024 10:29

И почему судебный иск Result<[Any], Error> кажется вам «странным»? Что еще выглядит странно?

Sweeper 29.04.2024 10:32

@Sweeper да, ты меня правильно понял. Result<[Any], Error> выглядит странно, потому что я частично избегаю throws, который обеспечивается быстрым параллелизмом, и использую решение из другого фреймворка.

Gargo 29.04.2024 10:33
Result не является «еще одним фреймворком». Это часть стандартной библиотеки Swift. Эта проблема требует сохранения перехваченных ошибок (если только вам не нужно выдать только последнюю ошибку). Даже если вы не используете Result, в конечном итоге вы заново изобретеете что-то похожее.
Sweeper 29.04.2024 11:08
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
82
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Здесь вам обязательно стоит использовать Result. Для удобства сначала напишите инициализатор Result, который перехватывает ошибки при асинхронном замыкании.

extension Result {
    init(asyncCatching block: () async throws -> Success) async where Failure == Error {
        do {
            self = .success(try await block())
        } catch {
            self = .failure(error)
        }
    }
}

Предположим, вы хотите test выдавать все ошибки, когда все async let выбрасывали, создайте [Error] также тип Error.

extension Array: Error where Element: Error {}

Тогда это так же просто, как:

func test() async throws -> [Any] {
    async let t1 = Result { try await getArr() }
    async let t2 = Result { try await getArr2() }
    switch (await t1, await t2) {
    case let (.failure(e1), .failure(e2)):
        return [e1, e2]
    case let (.success(s), .failure), let (.failure, .success(s)):
        return s
    case let (.success(s1), .success(s2)):
        return s1 + s2
    }
}

Для более общей версии операции «сокращение набора Result в одно значение или бросок» вы можете написать:

func accumulateResults<T, U, Failure: Error>(_ results: [Result<T, Failure>], identity: U, reduce: (U, T) -> U) throws -> U {
    var ret = identity
    var success = false
    var failures = [Failure]()
    for result in results {
        switch result {
        case .success(let s):
            ret = reduce(ret, s)
        case .failure(let e):
            if success { break }
            failures.append(e)
        }
    }
    return if success { ret } else { throw failures }
}

Без использования Result (или изобретения чего-то подобного) test может выдать только последнюю ошибку, потому что вы не можете сохранить предыдущие ошибки (точная работа Result).

func testWithoutResult() async throws -> [Any] {
    async let t1 = getArr()
    async let t2 = getArr2()
    var result = [Any]()
    var success = false
    do {
        result.append(try await t1)
        success = true
    } catch {}
    do {
        result.append(try await t2)
    } catch {
        if !success { throw error }
    }
    return result
}

Для каждой нефинальной асинхронной операции напишите блок do и установите success = true. Для последней асинхронной операции вместо этого используйте if !success { throw error } в блоке catch.

Хотя параллелизм Swift сводит к минимуму нашу зависимость от типов Result (имеющих свой собственный, более естественный способ выдавать ошибки и возвращать успех), типы Result по-прежнему широко поддерживаются.

Например, у Task есть свое свойство result. Итак, я мог бы использовать это в данном случае и вернуть массив значений успеха/неудачи:

func test() async -> [Result<[Int], Error>] {
    let t1 = Task { try await getArr() }
    let t2 = Task { try await getArr2() }
    
    return await withTaskCancellationHandler { 
        await [t1.result, t2.result]
    } onCancel: { 
        t1.cancel()
        t2.cancel()
    }
}

func getArr() async throws -> [Int] {
    if Bool.random() {
        return [1, 2, 3]
    } else {
        throw RandomChanceError.badLuck
    }
}

func getArr2() async throws -> [Int] {
    if Bool.random() {
        return [4, 5, 6]
    } else {
        throw RandomChanceError.badLuck
    }
}

enum RandomChanceError: Error {
    case badLuck
}

Единственный трюк с неструктурированным параллелизмом заключается в том, что вы несете ответственность за добавление обработки отмены, как показано выше с помощью withTaskCancellationHandler. (Возможно, в настоящее время вы не ожидаете отмены этого метода test, но, тем не менее, разумно всегда добавлять обработку отмены при написании неструктурированного параллелизма.)

В любом случае, вызывающая сторона может перебирать эти результаты:


for result in await test() {
    switch result {
    case .success(let value): print(value)
    case .failure(let error): print(error)
    }
}

В вашем примере вы возвращаете [Any]. Действительно ли getArr и getArr2 возвращают гетерогенный массив? Или вы используете [Any], потому что две функции на самом деле просто возвращают два разных однородных массива разных типов?

Если последнее верно, я бы посоветовал удалить [Any], поскольку это фактически не отражает природу двух отдельных однородных массивов. Например, ниже я рассматриваю сценарий, в котором первый (который я переименовал в getNumbers) возвращает однородный массив одного типа, а второй (который я переименовал в getStrings) возвращает однородный массив, но другого типа:

func test() async throws {
    let numbersAndStrings = try await getNumbersAndStrings()

    switch numbersAndStrings.numbers {
    case .success(let numbers): print(numbers)
    case .failure(let error):   print(error)
    }

    switch numbersAndStrings.strings {
    case .success(let strings): print(strings)
    case .failure(let error):   print(error)
    }
}

func getNumbersAndStrings() async throws -> (numbers: Result<[Int], Error>, strings: Result<[String], Error>) {
    let t1 = Task { try await getNumbers() }
    let t2 = Task { try await getStrings() }

    return try await withTaskCancellationHandler {
        let results = await (
            numbers: t1.result,
            strings: t2.result
        )
        try Task.checkCancellation()
        return results
    } onCancel: {
        t1.cancel()
        t2.cancel()
    }
}

func getNumbers() async throws -> [Int] {
    try await Task.sleep(for: .seconds(.random(in: 1...3)))
    if Bool.random() {
        return [1, 2, 3]
    } else {
        throw SomeError.badLuck
    }
}

func getStrings() async throws -> [String] {
    try await Task.sleep(for: .seconds(.random(in: 1...3)))
    if Bool.random() {
        return ["four", "five", "six"]
    } else {
        throw SomeError.badLuck
    }
}

enum SomeError: Error {
    case badLuck
}

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

Другие вопросы по теме