如何使这段代码更紧凑和惯用?

Posted

技术标签:

【中文标题】如何使这段代码更紧凑和惯用?【英文标题】:How to make this code more compact and idiomatic? 【发布时间】:2011-04-17 08:12:35 【问题描述】:

大家好。

我是一名 C# 程序员,在空闲时间探索 F#。我编写了以下二维图像卷积的小程序。

open System

let convolve y x = 
  y |> List.map (fun ye -> x |> List.map ((*) ye))
    |> List.mapi (fun i l -> [for q in 1..i -> 0] @ l @ [for q in 1..(l.Length - i - 1) -> 0])
    |> List.reduce (fun r c -> List.zip r c |> List.map (fun (a, b) -> a + b))   

let y = [2; 3; 1; 4]
let x = [4; 1; 2; 3]
printfn "%A" (convolve y x)

我的问题是:上面的代码是惯用的 F# 吗?可以更简洁吗? (例如,是否有一些更短的方法来生成填充的 0 列表(为此我在代码中使用了列表理解))。是否有任何可以提高其性能的更改?

任何帮助将不胜感激。谢谢。

编辑:

谢谢布赖恩。我没有得到你的第一个建议。这是应用您的第二个建议后我的代码的外观。 (我也抽象出了list-fill操作。)

open System

let listFill howMany withWhat = [for i in 1..howMany -> withWhat]

let convolve y x = 
  y |> List.map (fun ye -> x |> List.map ((*) ye))
    |> List.mapi (fun i l -> (listFill i 0) @ l @ (listFill (l.Length - i - 1) 0))
    |> List.reduce (List.map2 (+))

let y = [2; 3; 1; 4]
let x = [4; 1; 2; 3]
printfn "%A" (convolve y x)

还有什么可以改进的吗?等待更多建议...

【问题讨论】:

【参考方案1】:

惯用的解决方案是像在 C 中一样使用数组和循环。但是,您可能对以下使用模式匹配的替代解决方案感兴趣:

  let dot xs ys =
     Seq.map2 (*) xs ys
     |> Seq.sum

  let convolve xs ys =
     let rec loop vs xs ys zs =
        match xs, ys with
        | x::xs, ys -> loop (dot ys (x::zs) :: vs) xs ys (x::zs)
        | [], _::(_::_ as ys) -> loop (dot ys zs :: vs) [] ys zs
        | _ -> List.rev vs
     loop [] xs ys []

  convolve [2; 3; 1; 4] [4; 1; 2; 3]

【讨论】:

【参考方案2】:

您可以做的另一件事是融合前两张地图。 l |> List.map f |> List.mapi g = l |> List.mapi (fun i x -> g i (f x)),所以结合 Tomas 和 Brian 的建议,你可以得到类似:

let convolve y x = 
  let N = List.length x
  y
  |> List.mapi (fun i ye -> 
      [for _ in 1..i -> 0
       yield! List.map ((*) ye) x
       for _ in 1..(N-i-1) -> 0])    
  |> List.reduce (List.map2 (+))  

【讨论】:

【参考方案3】:

正如 Brian 所提到的,使用 @ 通常是有问题的,因为对于(简单)函数列表,无法有效地实现运算符 - 它需要复制整个第一个列表。

我认为 Brians 的建议是编写一个可以立即生成列表的序列生成器,但这有点复杂。您必须将列表转换为数组,然后编写如下内容:

let convolve y x =  
  y |> List.map (fun ye -> x |> List.map ((*) ye) |> Array.ofList) 
    |> List.mapi (fun i l -> Array.init (2 * l.Length - 1) (fun n -> 
        if n < i || n - i >= l.Length then 0 else l.[n - i]))
    |> List.reduce (Array.map2 (+))

一般来说,如果性能是一个重要问题,那么您可能无论如何都需要使用数组(因为这种问题最好通过按索引访问元素来解决)。使用数组有点困难(你需要正确地建立索引),但在 F# 中是非常好的方法。

无论如何,如果你想用列表来写这个,那么这里有一些选项。您可以在任何地方使用序列表达式,如下所示:

let convolve y (x:_ list) =  
  [ for i, v1 in x |> List.zip [ 0 .. x.Length - 1] ->
      [ yield! listFill i 0
        for v2 in y do yield v1 * v2
        yield! listFill (x.Length - i - 1) 0 ] ]
  |> List.reduce (List.map2 (+))

...或者您也可以组合这两个选项并在传递给List.mapi 的 lambda 函数中使用嵌套序列表达式(使用 yield! 生成零和列表):

let convolve y x =  
  y |> List.map (fun ye -> x |> List.map ((*) ye)) 
    |> List.mapi (fun i l -> 
         [ for _ in 1 .. i do yield 0
           yield! l 
           for _ in 1 .. (l.Length - i - 1) do yield 0 ])
    |> List.reduce (List.map2 (+))    

【讨论】:

【参考方案4】:

(fun ye -&gt; x |&gt; List.map ((*) ye))

真的吗?

我承认 |> 很漂亮,但你可以写: (fun ye -&gt; List.map ((*) ye) x)

【讨论】:

【参考方案5】:

关于零,例如怎么样

[for q in 0..l.Length-1 -> if q=i then l else 0]

(我还没有测试来验证它是否完全正确,但希望这个想法很清楚。)一般来说,@ 的任何使用都是代码异味。

关于整体性能,对于小型列表,这可能没问题;对于较大的,您可以考虑使用Seq 而不是List 进行一些中间计算,以避免在此过程中分配尽可能多的临时列表。

看起来最终的 zip-then-map 可能只需要调用 map2 就可以替换,类似于

... fun r c -> (r,c) ||> List.map2 (+)

甚至可能只是

... List.map2 (+)

但我远离编译器,所以没有仔细检查它。

【讨论】:

恐怕您的第一个建议没有进行类型检查(if .. then 表达式在一个分支中返回 int list,在另一个分支中返回 int)。它需要返回l的元素,这就有点棘手了…… 啊,真可惜;为什么我很少发布我没有编译的代码的一个例子。

以上是关于如何使这段代码更紧凑和惯用?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 for 循环使这段代码更短

惯用列表构造

如何在 Swift 上使这段代码异步

如何使这段代码简短易读?

如何使用减少每个单独元素的多重设置/取消设置功能

回文字符串问题:为啥我必须放 +1 而不是 -1 才能使这段代码工作?