Functional programming 了解一元遍历的副作用

Functional programming 了解一元遍历的副作用,functional-programming,f#,monads,traversal,fsharpx,Functional Programming,F#,Monads,Traversal,Fsharpx,我正试图按照Scott的指导,正确理解在F中使用一元风格遍历列表时副作用是如何工作的 我有一个AsyncSeq的项目,和一个副作用的函数,可以返回一个结果,它是保存项目到磁盘 我得到了一个大致的想法-将头部和尾部分开,将func应用于头部。如果返回Ok,则通过尾部递归,执行相同的操作。如果在任何点返回错误,则短路并返回 我也明白了为什么Scott的最终解决方案使用折叠而不是折叠-它将输出列表保持在与输入相同的顺序,因为每个处理的项目都在前一个项目之前 我也可以遵循以下逻辑: 当我们使用折叠时,首

我正试图按照Scott的指导,正确理解在F中使用一元风格遍历列表时副作用是如何工作的

我有一个AsyncSeq的项目,和一个副作用的函数,可以返回一个结果,它是保存项目到磁盘

我得到了一个大致的想法-将头部和尾部分开,将func应用于头部。如果返回Ok,则通过尾部递归,执行相同的操作。如果在任何点返回错误,则短路并返回

我也明白了为什么Scott的最终解决方案使用折叠而不是折叠-它将输出列表保持在与输入相同的顺序,因为每个处理的项目都在前一个项目之前

我也可以遵循以下逻辑:

当我们使用折叠时,首先处理列表最后一项的结果将作为累加器传递给下一项

如果是错误且下一项正常,则下一项将被丢弃

如果下一项是错误,它将替换以前的任何结果并成为累加器

这意味着,当您从右到左递归整个列表并在开始时结束时,您要么以正确的顺序确定所有结果,要么出现最新的错误,如果我们从左到右,这将是第一个出现的错误

让我困惑的是,既然我们从列表的末尾开始,处理每一项的所有副作用都会发生,即使我们只返回最后一个创建的错误

这似乎得到了确认,因为打印输出从[5]开始,然后是[4,5],然后是[3,4,5]等等

让我困惑的是,当我使用FSharpx库(我包装它以处理结果而不是选择)时,我看到的并不是这种情况。我看到副作用从左到右发生,在第一个错误时停止,这就是我想要发生的

它看起来也像Scott的非尾部递归版本,不使用折叠,只是从左到右递归列表?AsyncSeq版本也是如此。这可以解释为什么我在第一个错误时看到它短路,但如果它完成正常,那么输出项就会反转,这就是为什么我们通常使用折回

我觉得我误解或误读了一些显而易见的东西!谁能给我解释一下吗

编辑: rmunn对下面的AsyncSeq遍历给出了非常全面的解释。TLDR就是这样

Scott的初始实现和AsyncSeq遍历都像我所想的那样从左到右进行,因此只有在遇到错误之前才会进行处理

它们通过将头部前置到处理过的尾部,而不是将每个处理过的结果前置到前一个结果来保持内容的有序性,这是内置的F折叠所做的

折叠将使事情保持有序,但实际上会使用异步seq执行每一个可能永远需要的情况

很简单:traverseChoiceAsync没有使用折叠。是的,通过折叠,最后一个项目将首先被处理,因此当您到达第一个项目并发现其结果是错误时,您将触发每个项目的副作用。我认为,这就是为什么在FSharpx中编写traverseChoiceAsync的人选择不使用foldBack的原因,因为他们希望确保按顺序触发副作用,并在第一个错误时停止,或者在函数的选择版本中,第一个选项是2of2——但我将从这一点开始假装该函数是为使用结果类型而编写的

让我们看看您链接到的代码中的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 }
现在让我们一步一步地看一下。整个函数包装在一个异步{}块中,所以让我们来看看!在这个函数中,表示在异步上下文中展开,本质上是等待

let! s = s
这将采用AsyncSeq Result类型的s参数。或者,你可以把它想象成一个结果,并称之为有趣的尾巴->。。。针对该尾部结果,然后在新结果中重新包装该函数的结果。重要提示:因为这是在原始文件中使用Result.map Choice.mapl,所以我们知道,如果tail是错误值,或者如果选择是原始文件中的Choice2Of2,则不会调用该函数。因此,如果traverseResultAsync生成一个以错误值开头的结果,它将生成一个结果,其中result的值为错误,因此尾部的值将被丢弃。以后要记住这一点

好的,下一步

Async.map
这里,我们有一个由内部表达式生成的Result->Result函数,它将 将其转换为Async->Async函数。我们刚刚讨论过这一点,所以我们不需要再次讨论map是如何工作的。请记住,我们构建的这个Async->Async函数的效果如下:

等待外部异步。 如果结果为错误,则返回该错误。 如果结果为Ok tail,则生成Ok Cons b tail。 下一行:

traverseResultAsync f tl
我可能应该从这个开始,因为它实际上首先运行,然后它的值将被传递到我们刚刚分析过的Async->Async函数中

所以这整件事要做的就是说,好的,我们得到了AsyncSeq的第一部分,并将其传递给f,f产生了一个Ok结果,其值我们称为b。所以现在我们需要以类似的方式处理序列的其余部分,然后,如果序列的其余部分产生一个Ok结果,我们将把b粘贴在它的前面,并返回一个包含内容b::tail的Ok序列。但是,如果序列的其余部分产生错误,我们将丢弃b的值,只返回该错误,而不做任何更改

return!
这只是获取我们刚刚得到的一个错误或Ok b::tail的结果,它已经包装在一个Async中,并返回不变的结果。但是请注意,对traverseResultAsync的调用不是尾部递归的,因为它的值必须传递到Async.map中。。。表达优先

现在我们还有一点traverseResultAsync要看。记得我说过以后要记住这一点吗?嗯,时间到了

    | Error e -> 
      return Error e }
在这里,我们回到了与表达式匹配的b。如果b是一个错误结果,则不会进行进一步的递归调用,整个traverseResultAsync将返回一个结果值为Error的Async。如果我们当前嵌套在递归的深处,也就是说,我们在返回中!traverseResultAsync。。。表达式,那么我们的返回值将是Error,这意味着外部调用的结果,正如我们所记住的,也将是Error,丢弃以前可能发生的任何其他Ok结果

结论 所有这些的影响是:

逐步执行AsyncSeq,依次对每个项调用f。 第一次f返回错误时,停止单步执行,扔掉之前的所有Ok结果,并将该错误作为整个事件的结果返回。 如果f从不返回错误,而是每次返回Ok b,则返回一个Ok结果,该结果包含所有这些b值的AsyncSeq(按原始顺序)。 为什么它们按原来的顺序排列?因为Ok情况下的逻辑是:

如果序列为空,则返回一个空序列。 分成头和尾。 从f头中获取值b。 处理尾部。 将值b粘贴在尾部处理结果的前面。
因此,如果我们从概念上开始[a1;a2;a3],它实际上看起来像Cons a1,Cons a2,Cons a3,Nil,那么我们将以Cons b1,Cons b2,Cons b3,Nil结束,它将转换为概念序列[b1;b2;b3]。

有关解释,请参见上文@rmunn的伟大答案。我只是想为将来阅读本文的任何人发布一个小帮助,它允许您使用AsyncSeq traverse和结果,而不是它编写时使用的旧选择类型:

let traverseResultAsyncM (mapping : 'a -> Async<Result<'b,'c>>) source = 
    let mapping' = 
        mapping
        >> Async.map (function
            | Ok x -> Choice1Of2 x
            | Error e -> Choice2Of2 e)

    AsyncSeq.traverseChoiceAsync mapping' source
    |> Async.map (function
        | Choice1Of2 x -> Ok x
        | Choice2Of2 e -> Error e)
这里还有一个非异步映射的版本:

let traverseResultM (mapping : 'a -> Result<'b,'c>) source = 
    let mapping' x = async { 
        return 
            mapping x
            |> function
            | Ok x -> Choice1Of2 x
            | Error e -> Choice2Of2 e
    }

    AsyncSeq.traverseChoiceAsync mapping' source
    |> Async.map (function
        | Choice1Of2 x -> Ok x
        | Choice2Of2 e -> Error e)

谢谢@rmunn,这是一个伟大而全面的答案。事实上,我已经理解了99%的内容,但我很感激这对其他人来说是件好事。就我个人而言,我需要的关键点是a。我是对的,递归func Scott显示和AsyncSeq do从左向右,这是我和b所看到的。我们需要foldback而不是fold的原因只是fold的内部实现会反转项,但是当我们自己实现时,我们可以控制追加的方式。异步序列支持异常。我认为,只要抛出并捕获异常,您的生活就会变得更加轻松。如果不是您建议@Tomas,我不会对此进行太多思考,但这是一个很好的观点,遍历可能会过度设计代码。但我不确定,我已经将seq设置为从分页API中提取,然后将其从dto映射到域模型,将页面加入到单个流中,然后发布到数据库。这是一大堆需要开始抛出而不是返回结果的地方。遍历似乎是一种很好的处理方法,但这是我第一次使用AsyncSeq,我刚刚学习了遍历,所以我可能正在使用大锤敲开一个螺母!我想一般来说很难说什么是最好的——使用诸如Result之类的东西的好处是,您可以用复杂的方式编写代码,例如收集非关键错误,但仍然可以继续运行。异常只提供了一种排序方法-如果出现任何错误,请停止!如果这就是你所需要的,那么我会考虑他们-是的,他们不是类型检查,但F不是纯粹的,所以我认为这很好。我通常更喜欢通过使用内置语言功能获得的简单性和可读性,而不是更高级但更安全的
有些丑陋和复杂的方法。绝对。我发现使用应用程序风格的遍历使表单验证等变得轻而易举。我还喜欢使用asyncResult CE对我的工作流进行排序,因此我的许多函数都返回结果类型。我刚刚了解了事件源EventStore是我正在使用的分页API,显然,折叠事件列表来创建预测也是其中的一个重要部分。我无法想象尝试用OO语言实现它。我在很大程度上使用C和F来制作Xamarin应用程序,因此我当然不是一个纯粹主义者,我只是在很大程度上爱上了FP的魅力:我还有你的《深海潜水》一书,这本书非常棒,感谢你和其他人的共同努力:
      return! traverseResultAsync f tl |> Async.map (Result.map (fun tl -> Cons(b, tl) |> async.Return))
      return! (
          traverseResultAsync f tl
          |> Async.map (
              Result.map (
                  fun tl -> Cons(b, tl) |> async.Return)))
fun tl -> Cons(b, tl) |> async.Return
Result.map
Async.map
traverseResultAsync f tl
return!
    | Error e -> 
      return Error e }
let traverseResultAsyncM (mapping : 'a -> Async<Result<'b,'c>>) source = 
    let mapping' = 
        mapping
        >> Async.map (function
            | Ok x -> Choice1Of2 x
            | Error e -> Choice2Of2 e)

    AsyncSeq.traverseChoiceAsync mapping' source
    |> Async.map (function
        | Choice1Of2 x -> Ok x
        | Choice2Of2 e -> Error e)
let traverseResultM (mapping : 'a -> Result<'b,'c>) source = 
    let mapping' x = async { 
        return 
            mapping x
            |> function
            | Ok x -> Choice1Of2 x
            | Error e -> Choice2Of2 e
    }

    AsyncSeq.traverseChoiceAsync mapping' source
    |> Async.map (function
        | Choice1Of2 x -> Ok x
        | Choice2Of2 e -> Error e)