【问题标题】:Transformation of a list of positions to a 2D array of positions using functional programming (F#)使用函数式编程 (F#) 将位置列表转换为 2D 位置数组
【发布时间】:2011-02-01 06:56:32
【问题描述】:

您如何使以下代码以相同的速度运行?一般来说,作为输入,我有一个包含位置坐标和其他内容的对象列表,我需要创建一个包含这些对象的二维数组。

let m = Matrix.Generic.create 6 6 []
let pos = [(1.3,4.3); (5.6,5.4); (1.5,4.8)]  
pos |> List.iter (fun (pz,py) ->
  let z, y = int pz, int py
  m.[z,y] <- (pz,py) :: m.[z,y]
)

大概可以这样实现:

let pos = [(1.3,4.3); (5.6,5.4); (1.5,4.8)]
Matrix.generic.init 6 6 (fun z y ->
  pos |> List.fold (fun state (pz,py) ->
    let iz, iy = int pz, int py
    if iz = z && iy = y then (pz,py) :: state else state
  ) []
)

但我想它会慢得多,因为它循环遍历整个矩阵乘以列表而不是前一个列表迭代......

PS:代码可能有误,因为我在这台电脑上没有 F# 来检查它。

【问题讨论】:

    标签: algorithm f# functional-programming


    【解决方案1】:

    你可以这样做:

    [1.3, 4.3; 5.6, 5.4; 1.5, 4.8]
    |> Seq.groupBy (fun (pz, py) -> int pz, int py)
    |> Seq.map (fun ((pz, py), ps) -> pz, py, ps)
    |> Matrix.Generic.initSparse 6 6
    

    但在你的问题中你说:

    您如何使以下代码以相同的速度运行?

    在后来的评论中你说:

    好吧,我尽量避免可变性,以便将来代码易于并行化

    恐怕这是希望战胜现实的胜利。函数式代码通常具有较差的绝对性能,并且在并行化时扩展性很差。鉴于此代码正在执行大量分配,您根本不可能从并行性中看到任何性能提升。

    【讨论】:

    • 感谢您的评论。您能否指点我一本书或文章,在那里我可以学习如何编写易于并行化的代码,最好是在 F# 中?我怎么知道代码中有多少分配?
    • @Oldrich:我正在写一本关于它的书,所以我还不能推荐一本书!如果你能忍受的话,关于 Cilk 的研究论文就是一座金矿。在 F# 中,您还必须努力避免分配。我从经验中知道这一点。在这种情况下,序列元素、元组和稀疏矩阵元素都是堆分配的。我最近讲过这个:skillsmatter.com/podcast/agile-testing/…
    【解决方案2】:

    这取决于“功能性”的定义。我会说“功能”函数意味着它总是为相同的参数返回相同的结果,并且它不会修改任何全局状态(或参数的值,如果它们是可变的)。我认为这是对 F# 的合理定义,但这也意味着在本地使用突变不会“功能失调”。

    在我看来,下面的函数是“函数式的”,因为它创建并返回一个新矩阵而不是修改现有的矩阵,但当然,函数的实现使用突变。

    let performStep m =
      let res = Matrix.Generic.create 6 6 [] 
      let pos = [(1.3,4.3); (5.6,5.4); (1.5,4.8)]   
      for pz, py in pos do
        let z, y = int pz, int py 
        res.[z,y] <- (pz,py) :: m.[z,y] 
      res
    

    无突变版本: 现在,如果您想让实现完全正常运行,那么我将首先创建一个矩阵,该矩阵在您想要将新列表元素添加到矩阵元素的位置包含 Some(pz, py),在所有其他位置包含 None地方。我想这可以通过初始化一个稀疏矩阵来完成。像这样的:

    let sp = pos |> List.map (fun (pz, py) -> int pz, int py, (pz, py)) 
    let elementsToAdd = Matrix.Generic.initSparse 6 6 sp
    

    那么你应该能够将原始矩阵m 与新创建的elementsToAdd 结合起来。这当然可以使用init 来完成(但是,使用map2 之类的东西可能会更好):

    let res = Matrix.init 6 6 (fun i j -> 
      match elementsToAdd.[i, j], m.[i, j] with
      | Some(n), res -> n::res
      | _, res -> res )
    

    F# 库函数中仍然很可能隐藏了一些突变(例如initinitSparse),但至少它显示了一种使用更原始操作来实现操作的方法。

    编辑:这仅在您需要向每个矩阵单元添加最多单个元素时才有效。如果你想添加多个元素,你必须先将它们分组(例如使用Seq.groupBy

    【讨论】:

    • initSparse 似乎要求添加的类型支持数字运算:initSparse 4 4 [1,1,1] 对我来说很好,但initSparse 4 4 [1,1,"test"] 不行。不过,我没有遇到堆栈溢出...
    • 另外,我认为您的 initSparse 示例在这种情况下无论如何都不会正常工作,因为映射序列中有多个点具有坐标 (1,4)。
    • @kvb:好点子 - 我想我必须在调用 initSparse 之前添加一些像 Seq.groupBy 这样的想法。
    • initSparse 的问题在我安装 F# 时可能有些奇怪(因为即使像 float 这样的原始类型也会失败)。
    【解决方案3】:

    为什么要在功能上做到这一点? Matrix 类型被设计为可以变异,所以你现在的做法对我来说看起来不错。

    如果你真的想在功能上做到这一点,我会这样做:

    let pos = [(1.3,4.3); (5.6,5.4); (1.5,4.8)]  
    let addValue m k v = 
      if Map.containsKey k m then
        Map.add k (v::m.[k]) m
      else
        Map.add k [v] m
    let map = 
      pos 
      |> List.map (fun (x,y) -> (int x, int y),(x,y)) 
      |> List.fold (fun m (p,q) -> addValue m p q) Map.empty
    let m = Matrix.Generic.init 6 6 (fun x y -> if (Map.containsKey (x,y) map) then map.[x,y] else [])
    

    这会遍历列表一次,创建一个从索引到点列表的不可变映射。然后,我们初始化矩阵中的每个条目,对每个条目进行一次映射查找。这应该花费总时间O(M + N log N),其中MN 分别是矩阵和列表中的条目数。我相信您使用突变的原始解决方案需要O(M+N) 时间,而您修改后的解决方案需要O(M*N) 时间。

    【讨论】:

    • 好吧,我尽量避免可变性,以便代码在将来易于并行化并避免错误。我还想知道函数式程序员将如何解决它 :) 因为我经常遇到类似的问题,我必须进行变异。你写的代码乍一看很复杂。
    • @Oldrich - 如果你真的想避免可变性,那么 Matrix 是错误的数据类型;您可以创建一个基于列表列表之类的不可变模拟。但是,在某些情况下,可变解决方案确实是最干净的(这是 F# 支持突变的原因之一)。我认为您的原始解决方案非常简洁易懂,因此我不会更改它。
    • 抱歉,我是 F# 新手,不太了解您在 List.fold 操作中使用 addValue 函数的部分。看起来您将值 q 与键 p 相关联,但稍后您将两个坐标用作 Map.containsKey 中的键。我忽略了什么?
    • @kahoon - addValue 获取从键到值列表、键和值的映射,并返回添加了该键和值的新映射。在 fold 函数中,p 是一对用作键的整数,q 是一对放入值列表的浮点数;稍后我们使用由xy 坐标组成的显式对来访问地图,这可能是产生混淆的地方。如果便于理解,您可以在任何地方将p 替换为(x,y)
    • @kvb - 谢谢,现在我明白了。
    猜你喜欢
    • 2013-11-22
    • 1970-01-01
    • 2015-02-09
    • 2012-07-17
    • 2019-07-04
    • 2020-02-16
    • 1970-01-01
    • 2016-11-15
    • 1970-01-01
    相关资源
    最近更新 更多