如何使这段代码更紧凑和惯用?
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 -> x |> List.map ((*) ye))
真的吗?
我承认 |> 很漂亮,但你可以写:
(fun ye -> 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
的元素,这就有点棘手了……
啊,真可惜;为什么我很少发布我没有编译的代码的一个例子。以上是关于如何使这段代码更紧凑和惯用?的主要内容,如果未能解决你的问题,请参考以下文章