这很简单:traverseChoiceAsync
不使用foldBack
. 是foldBack
的,最后一个项目将首先被处理,所以当你到达第一个项目并发现它的结果是Error
你已经触发了每个项目的副作用。我认为,这正是为什么traverseChoiceAsync
在 FSharpx 中编写的人选择不使用foldBack
的原因,因为他们希望确保按顺序触发副作用,并首先停止Error
(或者,在Choice
函数版本的情况下,首先Choice2Of2
——但从这一点开始,我会假装该函数是为使用该Result
类型而编写的。)
让我们看一下traverseChoieAsync
您链接到的代码中的函数,并逐步阅读它。我还将重写它以使用Result
代替,因为这两种类型在功能上基本相同,但在 DU 中具有不同的名称,如果调用 DU 案例而不是代替,Choice
则更容易判断发生了什么和。这是原始代码: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
,该名称今后将隐藏原始参数。当您等待 a 的结果时AsyncSeq
,您得到的只是第一个元素,而其余元素仍被包裹在需要进一步等待的异步中。您可以通过查看match
表达式的结果或查看AsyncSeq
类型的定义来了解这一点:
type AsyncSeq<'T> = Async<AsyncSeqInner<'T>>
and AsyncSeqInner<'T> =
| Nil
| Cons of 'T * AsyncSeq<'T>
因此,当您执行let! x = s
when s
is of type时AsyncSeq<'T>
,值x
将是Nil
(当序列运行到其末尾时)或者它将是Cons(head, tail)
where head
is of type'T
和tail
is of type AsyncSeq<'T>
。
因此,在这一let! s = s
行之后,我们的本地名称s
现在引用了一个AsyncSeqInner
类型,它包含序列的头项(或者Nil
如果序列为空),并且序列的其余部分仍然包装在 an 中AsyncSeq
,因此尚未对其进行评估 (而且,至关重要的是,它的副作用还没有发生)。
match s with
| Nil -> return Ok (Nil |> async.Return)
这一行发生了很多事情,因此需要进行一些解包,但要点是,如果输入序列s
以Nil
它为头部,即已经到达结尾,那么这不是错误,我们返回一个空序列.
现在来解包。外部return
位于async
关键字中,因此它采用Result
(其值为Ok something
)并将其转换为Async<Result<something>>
. 记住函数的返回类型被声明为Async<Result<AsyncSeq>>
,innersomething
显然是一个AsyncSeq
类型。那么这是怎么回事Nil |> async.Return
呢?嗯,async
不是 F# 关键字,它是AsyncBuilder
. 在一个计算表达式里面foo { ... }
,return x
被翻译成foo.Return(x)
。所以调用async.Return x
和写是一样的async { return x }
,除了它避免将一个计算表达式嵌套在另一个计算表达式中,这在精神上尝试和解析会有点讨厌(而且我不是 100% 确定 F# 编译器在语法上允许它)。所以Nil |> async.Return
isasync.Return Nil
这意味着它产生一个值Async<x>
wherex
是 value 的类型Nil
。正如我们刚刚看到的,这Nil
是一个类型的值AsyncSeqInner
,所以Nil |> async.Return
产生一个Async<AsyncSeqInner>
。另一个名称Async<AsyncSeqInner>
是AsyncSeq
。所以这整个表达式产生了一个Async<Result<AsyncSeq>>
意思是“我们在这里完成了,序列中没有更多的项目,并且没有错误”。
呸。现在为下一行:
| Cons(a,tl) ->
很简单:如果AsyncSeq
named中的下一个项目s
是 a Cons
,我们解构它,以便现在调用实际项目a
,并调用尾部(另一个AsyncSeq
)tl
。
let! b = f a
这会调用f
我们刚刚得到的值s
,然后解开的返回值Async
部分f
,所以b
现在是 a Result<'b, 'e>
。
match b with
| Ok b ->
更多的阴影名称。在的这个分支中match
,b
现在将值命名为 type'b
而不是 a 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
必须是 an AsyncSeq
)并将其转换为一个AsyncSeq
后面b
跟有尾部的. 即,这就像在一个列表中b :: tl
:它贴b
在.AsyncSeq
从最内在的表达中迈出的一步是:
Result.map
请记住,map
可以通过两种方式来考虑该函数:一种是“获取一个函数并针对该包装“内部”的任何内容运行它”。另一种是“取一个作用于其上'T
的函数,使其成为一个作用于其上的函数Wrapper<'T>
”。(如果你还没有清楚这两个概念,https ://sidburn.github.io/blog/2016/03/27/understanding-map 是一篇很好的文章,可以帮助你理解这个概念)。因此,它所做的是将 typeAsyncSeq -> AsyncSeq
的函数转换为 type 的函数Result<AsyncSeq> -> Result<AsyncSeq>
。或者,您可以将其视为采用 a并针对该结果进行Result<tail>
调用,然后将该函数的结果重新包装在一个 new 中。重要的:fun tail -> ...
tail
Result
Result.map
(Choice.mapl
在原文中)我们知道如果tail
是一个Error
值(或者如果在原文中Choice
是 a ),则不会调用Choice2Of2
该函数。因此,如果产生一个以值开头的结果,它将产生一个值为 an 的地方,因此尾部的值将被丢弃。请记住这一点,以备后用。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
。所以现在我们需要处理剩下的部分序列类似地,然后,如果序列的其余部分产生Ok
结果,我们将粘贴b
在它的前面并返回一个Ok
包含内容的序列b :: tail
。但是如果序列的其余部分产生一个Error
,我们将丢弃该值的b
,只是Error
原封不动地返回。”
return!
这只是获取我们刚刚得到的结果(anError
或 an Ok (b :: tail)
,已经包含在 an 中Async
)并原封不动地返回它。但请注意,对的调用traverseResultAsync
不是尾递归的,因为它的值必须首先传递到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
- 第一次
f
返回,停止单步执行Error
,丢弃任何以前的Ok
结果,并将其Error
作为整个结果返回。
- 如果
f
从不返回Error
而是Ok b
每次都返回,则返回一个Ok
包含AsyncSeq
所有这些b
值中的一个的结果,按照它们的原始顺序。
为什么它们按原来的顺序排列?因为Ok
案例中的逻辑是:
- 如果序列为空,则返回一个空序列。
- 分为头尾。
b
从中获取价值f head
。
- 处理尾巴。
- 将值粘贴
b
在尾部处理结果的前面。
因此,如果我们从 (conceptually) 开始[a1; a2; a3]
,实际上看起来Cons (a1, Cons (a2, Cons (a3, Nil)))
我们最终会Cons (b1, Cons (b2, Cons (b3, Nil)))
转换为概念 sequence [b1; b2; b3]
。