如何避免多次迭代作为模式?

时间:2014-07-23 00:55:19

标签: f# functional-programming theory

在函数式语言中(使用F#),我努力在功能组合的优点与单一责任之间找到平衡,并在序列上获得单次迭代的性能。任何代码模式建议/示例都可以实现两者?

我没有扎实的计算理论背景,我一遍又一遍地遇到这种一般模式:迭代一个集合,想要在迭代时做副作用,以避免在同一个集合或其结果集上进一步迭代

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

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

  1. 命名输入数组
  2. 管道传输到“isValid”功能过滤器
  3. 副作用:记录无效的输入名称
  4. 管道有效输入以进一步执行
  5. 问题示例

    在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
    

2 个答案:

答案 0 :(得分:5)

关于F#中日志记录和其他副作用的组成my other answer,在本例中,您可以编写更高级别的日志记录功能,如下所示:

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,因此它适合f函数的log参数,并且您可以部分应用上述日志函数。

答案 1 :(得分:2)

这并没有解决你只关注迭代序列的问题(对于一个数组来说,无论如何都非常便宜),但我认为,更容易阅读和更清楚:

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