Все довольно просто: traverseChoiceAsync
не использует foldBack
. Да, с foldBack
последний элемент будет обработан первым, так что к тому времени, как вы доберетесь до первого элемента и обнаружите, что его результат равен Error
, вы бы вызвали побочные эффекты каждого элемента. Именно поэтому, я думаю, именно поэтому тот, кто написал traverseChoiceAsync
в FSharpx, предпочел не использовать foldBack
, потому что он хотел убедиться, что побочные эффекты будут срабатывать по порядку и останавливаться на первых Error
(или в случай Choice
версии функции, первый Choice2Of2
- но с этого момента я буду делать вид, что эта функция была написана для использования типа Result
.)
Давайте посмотрим на функцию traverseChoieAsync
в коде, на который вы ссылаетесь, и прочитайте его пошагово. Я также перепишу его, чтобы использовать Result
вместо Choice
, потому что эти два типа в основном идентичны по функциям, но с разными именами в DU, и будет немного легче сказать, что происходит, если DU дела называются Ok
и Error
вместо Choice1Of2
и Choice2Of2
. Вот оригинальный код:
let rec traverseChoiceAsync (f:'a -> Async<Choice<'b, 'e>>) (s:AsyncSeq<'a>) : Async<Choice<AsyncSeq<'b>, 'e>> = async {
let! s = s
match s with
| Nil -> return Choice1Of2 (Nil |> async.Return)
| Cons(a,tl) ->
let! b = f a
match b with
| Choice1Of2 b ->
return! traverseChoiceAsync f tl |> Async.map (Choice.mapl (fun tl -> Cons(b, tl) |> async.Return))
| Choice2Of2 e ->
return Choice2Of2 e }
А вот оригинальный код, переписанный для использования Result
. Обратите внимание, что это простое переименование, и никакая логика не нуждается в изменении:
let rec traverseResultAsync (f:'a -> Async<Result<'b, 'e>>) (s:AsyncSeq<'a>) : Async<Result<AsyncSeq<'b>, 'e>> = async {
let! s = s
match s with
| Nil -> return Ok (Nil |> async.Return)
| Cons(a,tl) ->
let! b = f a
match b with
| Ok b ->
return! traverseChoiceAsync f tl |> Async.map (Result.map (fun tl -> Cons(b, tl) |> async.Return))
| Error e ->
return Error e }
Теперь давайте пройдем через это. Вся функция заключена в блок async { }
, поэтому let!
внутри этой функции означает «развернуть» в асинхронном контексте (по сути, «ожидать»).
let! s = s
Он принимает параметр s
(типа AsyncSeq<'a>
) и разворачивает его, связывая результат с локальным именем s
, которое отныне будет затенять исходный параметр. Когда вы ожидаете результата AsyncSeq
, вы получаете только первый элемент, в то время как остальная часть все еще находится в асинхронном режиме, который требует дальнейшего ожидания. Вы можете увидеть это, посмотрев на результат выражения match
или взглянув на определение типа AsyncSeq
:
type AsyncSeq<'T> = Async<AsyncSeqInner<'T>>
and AsyncSeqInner<'T> =
| Nil
| Cons of 'T * AsyncSeq<'T>
Поэтому, когда вы делаете let! x = s
, когда s
имеет тип AsyncSeq<'T>
, значение x
будет либо Nil
(когда последовательность завершится), либо будет Cons(head, tail)
где head
имеет тип 'T
и tail
имеет тип AsyncSeq<'T>
.
Итак, после этой строки let! s = s
наше локальное имя s
теперь относится к типу AsyncSeqInner
, который содержит элемент заголовка последовательности (или Nil
, если последовательность была пустой). ), а остальная часть последовательности по-прежнему заключена в в AsyncSeq
, поэтому ее еще предстоит оценить (и, что особенно важно, ее побочные эффекты еще не произошли).
match s with
| Nil -> return Ok (Nil |> async.Return)
В этой строке много чего происходит, поэтому потребуется немного распаковать, но суть в том, что если входная последовательность s
имеет Nil
в качестве заголовка, то есть достигла конца, то это не так ошибка, и мы возвращаем пустую последовательность.
Теперь распаковать. Внешний return
находится в ключевом слове async
, поэтому он принимает Result
(значение которого Ok something
) и превращает его в Async<Result<something>>
. Помня, что возвращаемый тип функции объявлен как Async<Result<AsyncSeq>>
, внутренний something
явно является AsyncSeq
типом. Так что же происходит с этим Nil |> async.Return
? Ну, async
- это не ключевое слово F #, это имя экземпляра AsyncBuilder
. Внутри вычислительного выражения foo { ... }
, return x
переводится в foo.Return(x)
. Таким образом, вызов async.Return x
- это то же самое, что и запись async { return x }
, за исключением того, что он избегает вложения вычислительного выражения в другое вычислительное выражение, что было бы немного неприятно пытаться проанализировать мысленно (и я не уверен на 100%, что синтаксически это позволяет компилятор F #). Таким образом, Nil |> async.Return
- это async.Return Nil
, что означает, что он создает значение Async<x>
, где x
- это тип значения Nil
. И, как мы только что видели, это Nil
является значением типа AsyncSeqInner
, поэтому Nil |> async.Return
выдает Async<AsyncSeqInner>
. И еще одно имя для Async<AsyncSeqInner>
- AsyncSeq
. Таким образом, все это выражение выдает Async<Result<AsyncSeq>>
, которое имеет значение «Мы закончили, в последовательности больше нет элементов и не было ошибки».
Уф. Теперь для следующей строки:
| Cons(a,tl) ->
Простой: если следующий элемент в AsyncSeq
с именем s
был Cons
, мы деконструируем его так, что фактический элемент теперь называется a
, а хвост (другой *)1112 *) называется tl
.
let! b = f a
Это вызывает f
для значения, которое мы только что получили из s
, а затем разворачивает Async
часть возврата f
значение, так что b
теперь является Result<'b, 'e>
.
match b with
| Ok b ->
Более теневыми именами.Внутри этой ветви match
, b
теперь присваивается имя типа 'b
, а не Result<'b, 'e>
.
return! traverseResultAsync f tl |> Async.map (Result.map (fun tl -> Cons(b, tl) |> async.Return))
Ху-мальчик.Это слишком много, чтобы заняться сразу.Давайте напишем это так, как будто операторы |>
были выстроены в отдельные строки, а затем мы будем проходить каждый шаг по одному за раз.(Обратите внимание, что я обернул вокруг него лишнюю пару скобок, просто чтобы уточнить, что это конечный результат всего этого выражения, который будет передан в ключевое слово return!
).
return! (
traverseResultAsync f tl
|> Async.map (
Result.map (
fun tl -> Cons(b, tl) |> async.Return)))
Я собираюсь разобраться с этим выражением изнутри.Внутренняя линия:
fun tl -> Cons(b, tl) |> async.Return
async.Return
вещь, которую мы уже видели.Это функция, которая берет хвост (в настоящее время мы не знаем или не заботимся о том, что находится внутри этого хвоста, за исключением того, что из-за необходимости сигнатуры типа Cons
это должен быть AsyncSeq
) и превращает его вAsyncSeq
то есть b
с последующим хвостом.То есть это похоже на b :: tl
в списке: оно b
вставляется в front из AsyncSeq
.
В одном шаге от этого внутреннего выражения:
Result.map
Помните, что о функции map
можно думать двумя способами: один - «взять функцию и запустить ее против того, что находится« внутри »этой оболочки».Другой - «возьмите функцию, которая работает на 'T
, и превратите ее в функцию, которая работает на Wrapper<'T>
».(Если у вас еще нет ясности обо всех этих вещах, https://sidburn.github.io/blog/2016/03/27/understanding-map - довольно хорошая статья, чтобы помочь воплотить эту концепцию).Итак, для этого нужно взять функцию типа AsyncSeq -> AsyncSeq
и превратить ее в функцию типа Result<AsyncSeq> -> Result<AsyncSeq>
.С другой стороны, вы могли бы думать о том, что вы берете Result<tail>
и вызываете fun tail -> ...
против этого результата tail
, а затем повторно упаковываете результат этой функции в новый Result
. Важно: Поскольку для этого используется Result.map
(Choice.mapl
в оригинале), мы знаем, что если tail
является значением Error
(или если Choice
было Choice2Of2
воригинал), функция не будет вызываться .Таким образом, если traverseResultAsync
выдаст результат, который начинается со значения Error
, он выдаст <Async<Result<foo>>>
, где значение Result<foo>
равно Error
, и поэтому значение хвоста будет отброшено.Запомните это позже.
Хорошо, следующий шаг.
Async.map
Здесь у нас есть функция Result<AsyncSeq> -> Result<AsyncSeq>
, созданная внутренним выражением, и она преобразует ее в Async<Result<AsyncSeq>> -> Async<Result<AsyncSeq>>
функция.Мы только что говорили об этом, поэтому нам не нужно рассказывать, как работает map
снова.Просто помните, что эффект этой функции Async<Result<AsyncSeq>> -> Async<Result<AsyncSeq>>
, который мы создали, будет следующим:
- Ожидание внешнего
async
. - Если результат равен
Error
, верните это значение Error
. - Если результат равен
Ok tail
, выведите Ok (Cons (b, tail))
.
Следующая строка:
traverseResultAsync f tl
Мне, вероятно, следовало бы начать с этого, потому что это на самом деле будет запускать сначала , а затем его значение будет передано в функцию Async<Result<AsyncSeq>> -> Async<Result<AsyncSeq>>
, которую мы только что проанализировали.
Итак, все, что нам нужно сделать, это сказать: «Хорошо, мы взяли первую часть AsyncSeq
, которую нам передали, и передали ее f
, а f
дал результат Ok
со значениеммы вызываем b
. Так что теперь нам нужно обработать rest последовательности аналогичным образом, а затем, , если остальная часть последовательности даст результат Ok
, мымы прикрепим b
к передней части и вернем последовательность Ok
с содержанием b :: tail
. НО, если остальная часть последовательности выдаст Error
, мыl выбросить значение b
и просто вернуть Error
без изменений. "
return!
Это просто берет только что полученный нами результат (либо Error
, либо Ok (b :: tail)
, уже заключенный в Async
) и возвращает его без изменений.Но обратите внимание, что вызов traverseResultAsync
является NOT хвост-рекурсивным, потому что его значение нужно было сначала передать в выражение Async.map (...)
.
И теперь у нас еще есть еще один битtraverseResultAsync
на что посмотреть.Помните, когда я сказал: «Имейте это в виду на потом»?Что ж, время пришло.
| Error e ->
return Error e }
Вот мы и вернулись в выражении match b with
.Если b
был результатом Error
, то дальнейшие рекурсивные вызовы не выполняются, и весь traverseResultAsync
возвращает Async<Result>
, где значение Result
равно Error
.И если мы в настоящее время вложены глубоко внутрь рекурсии (то есть мы находимся в выражении return! traverseResultAsync ...
), тогда возвращаемое значение будет Error
, что означает результат «внешнего» вызова, как мы сохранилив виду, также будет Error
, отбрасывая любые другие Ok
результаты, которые могли бы произойти "до".
Заключение
И поэтому эффект всего этого таков:
- Пройдите через
AsyncSeq
, вызывая f
для каждого элемента по очереди. - first time
f
возвращает Error
, прекратите проходить через, отбросить все предыдущие Ok
результаты и вернуть Error
как результат всего этого. - Если
f
никогда не возвращает Error
и вместо этого возвращает Ok b
каждый раз, возвращаетOk
результат, содержащий AsyncSeq
всех этих b
значений, в их первоначальном порядке.
Почему они в своем первоначальном порядке?Поскольку логика в случае Ok
такова:
- Если последовательность была пустой, вернуть пустую последовательность.
- Разделить на голову и хвост.
- Получить значение
b
из f head
. - Обработка хвоста.
- Стик-значение
b
в перед результата обработки хвоста.
Итак, если мы начали с (концептуально) [a1; a2; a3]
, который на самом деле выглядит как Cons (a1, Cons (a2, Cons (a3, Nil)))
, мы получим Cons (b1, Cons (b2, Cons (b3, Nil)))
, что означает концептуальную последовательность [b1; b2; b3]
.