【问题标题】:How to avoid multiple iterations as a pattern?如何避免多次迭代作为一种模式?
【发布时间】:2014-07-23 00:55:18
【问题描述】:

在函数式语言(使用 F#)中,我正在努力在具有单一职责的函数式组合的优势和获得单次迭代优于序列的性能之间找到平衡。有什么代码模式建议/例子可以实现这两者?

我在计算理论方面没有扎实的背景,我一遍又一遍地遇到这种一般模式:迭代集合并希望在迭代时产生副作用以避免对同一集合或其结果集的进一步迭代.

一个典型的例子是“减少”或“过滤”功能:过滤时有很多次我想根据过滤器的结果采取额外的步骤,但我想避免过滤后的第二次枚举结果。

让我们将输入验证作为一个简单的问题陈述:

  1. 命名输入数组
  2. 通过管道传递给“isValid”函数过滤器
  3. 副作用:记录无效的输入名称
  4. 管道有效输入以进一步执行

问题示例

在 F# 中,我最初可能会这样写:

inputs
// how to log invalid or other side-effects without messing up isValid??
|> Seq.filter isValid
|> execution

解决方案示例 #1

有一个内联的副作用,我需要类似的东西:

inputs
|> Seq.filter (fun (name,value) -> 
    let valid = isValid (name,value)
    // side-effect
    if not valid then
        printfn "Invalid argument %s" name
    valid
|> execution

解决方案示例#2

我可以使用元组进行更纯粹的关注点分离,但需要第二次迭代:

let validationResults =
    inputs
    // initial iteration
    |> Seq.filter (fun (name,value) -> 
        let valid = isValid (name,value)
        (name,value,valid)
    |> execution

// one example of a 2nd iteration...
validationResults
|> Seq.filter (fun (_,_,valid) -> not valid)
|> Seq.map (fun (name,_,_) -> printfn "Invalid argument %s" name)
|> ignore

// another example of a 2nd iteration...
for validationResult in validationResults do
    if not valid then
        printfn "Invalid argument %s" name

每个答案更新 2014-07-23

我用这个作为每个答案的解决方案。该模式是使用包含条件的聚合函数。可能有更优雅简洁的方式来表达这一点......

open System

let inputs = [("name","my name");("number","123456");("invalid","")]

let isValidValue (name,value) =
    not (String.IsNullOrWhiteSpace(value))

let logInvalidArg (name,value) =
    printfn "Invalid argument %s" name

let execution (name,value) =
    printfn "Valid argument %s: %s" name value

let inputPipeline input =
    match isValidValue input with
    | true -> execution input
    | false -> logInvalidArg input

inputs |> Seq.iter inputPipeline

【问题讨论】:

  • 此答案专门回答有关日志记录的问题,但应该与您的问题足够接近,以便您可以从中推断出一般解决方案:stackoverflow.com/a/24756810/126014

标签: f# functional-programming theory


【解决方案1】:

跟进my other answer 关于F# 中日志记录和其他副作用的组合,在此示例中,您可以编写更高级别的日志记录函数,如下所示:

let log f (name, value) =
    let valid = f (name, value)
    if not valid then
        printfn "Invalid argument %s" name
    valid

它有这个签名:

f:(string * 'a -> bool) -> name:string * value:'a -> bool

所以你现在可以像这样使用“真实的”isValid 函数来组合它:

inputs
|> Seq.filter (log isValid)
|> execution

由于isValid 函数具有签名name:'a * value:int -> bool,它适合log 函数的f 参数,并且您可以部分应用上述日志函数。

【讨论】:

  • 您可以定义它let log f x = ... 使其更通用。
  • @Daniel 如果您需要遵循 OP,则不需要,因为在 OP 代码中,它想要记录元组的 name 元素,而不是整个值。
  • 这让我走上了这条路。我尝试使用传统上应用于 OOP 的 GoF“装饰器”模式。我还尝试使用函数组合运算符>> 和流水线运算符|>,但条件阻止了用简单的函数链解决这个问题。最终,我使用了一个简单的聚合函数。我会用我的最新消息更新帖子。尝试重新考虑设计模式何时以及如何在 OOP 和纯函数式方法之间发生变化是一个有趣的旅程!
【解决方案2】:

这并没有解决您对仅迭代一次序列的担忧(对于数组而言,无论如何这非常便宜),但我认为它更易于阅读和清晰:

let valid, invalid = Array.partition isValid inputs
for name, _ in invalid do printfn "Invalid argument %s" name
execution valid

【讨论】:

  • 我之前没用过Array.partition。这很棒。我可以用它来分叉执行。会从马克的回答中更多地考虑我在上面更新的解决方案。
猜你喜欢
  • 2011-11-18
  • 1970-01-01
  • 2018-12-06
  • 1970-01-01
  • 2016-07-15
  • 2017-10-28
  • 1970-01-01
  • 1970-01-01
  • 2012-07-09
相关资源
最近更新 更多