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

时间:2010-09-26 06:02:44

标签: f# functional-programming idiomatic

Hullo all。

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

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的填充列表(为此我在代码中使用了列表理解))。任何可以改善其性能的变化?

非常感谢任何帮助。感谢。

修改

谢谢Brian。我没有得到你的第一个建议。以下是我的代码在应用您的第二个建议后的样子。 (我还抽象出了列表填充操作。)

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)

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

5 个答案:

答案 0 :(得分:5)

正如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 (+))

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

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 (+))    

答案 1 :(得分:3)

惯用解决方案是使用数组和循环,就像在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 :(得分:2)

关于零,例如

[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 (+)

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

答案 3 :(得分:1)

(fun ye -> x |> List.map ((*) ye))

真的吗?

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

答案 4 :(得分:1)

你可以做的另一件事是融合前两张地图。 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 (+))