Haskell 根本不提供突变是一个普遍的神话。实际上,它提供了一种非常特殊的变异:一个值只能变异一次,从未评估到评估。利用这种特殊突变的艺术被称为打结。我们将从一个类似于 C++ 的数据结构开始:
data Vector -- held abstract
data Point = Point
{ position :: Vector
, v, w :: Double
, neighbors :: [Point]
}
现在,我们要做的是构建一个Array Point
包含neighbors
指向同一数组中其他元素的指针。以下代码的主要特点Array
是它是spine-lazy(它不会过早地强制它的元素)并且具有快速的随机访问;如果您愿意,可以用这些属性替换您最喜欢的备用数据结构。
找邻居功能的接口有很多选择。为了具体起见并使我自己的工作变得简单,我假设你有一个函数,它接受一个Vector
和一个列表Vectors
并给出邻居的索引。
findNeighbors :: Vector -> [Vector] -> [Int]
findNeighbors = undefined
让我们也为computeV
和设置一些类型computeW
。对于 nonce,我们将要求computeV
遵守您所说的非正式合同,即它可以查看 any 的position
andneighbors
字段Point
,但不能查看v
orw
字段。(类似地,除了它可以得到的任何领域computeW
之外,可能会查看任何东西。)实际上可以在类型级别强制执行此操作而无需太多体操,但现在让我们跳过它。w
Point
computeV, computeW :: Point -> Double
(computeV, computeW) = undefined
现在我们准备好构建我们的(标记的)内存图。
buildGraph :: [Vector] -> Array Int Point
buildGraph vs = answer where
answer = listArray (0, length vs-1) [point pos | pos <- vs]
point pos = this where
this = Point
{ position = pos
, v = computeV this
, w = computeW this
, neighbors = map (answer!) (findNeighbors pos vs)
}
就是这样,真的。现在你可以写你的
newPositions :: Point -> [Vector]
newPositions = undefined
wherenewPositions
可以完全自由地检查Point
它的任何字段,并将所有功能放在一起:
update :: [Vector] -> [Vector]
update = newPositions <=< elems . buildGraph
编辑:...在开头解释“特殊类型的突变”评论:在评估期间,您可以期望当您要求w
a 的字段时Point
,事情会按以下顺序发生:computeW
将强制该v
字段;然后computeV
将强制neighbors
场;然后该neighbors
字段将从未评估变为已评估;然后该v
字段将从未评估变为已评估;然后该w
字段将从未评估变为已评估。最后三个步骤看起来与 C++ 算法的三个变异步骤非常相似!
双重编辑:我决定我想看到这个东西运行,所以我用虚拟实现实例化了上面所有抽象的东西。我还想看到它只评估一次,因为我什至不确定我做对了!所以我打了几个trace
电话。这是一个完整的文件:
import Control.Monad
import Data.Array
import Debug.Trace
announce s (Vector pos) = trace $ "computing " ++ s ++ " for position " ++ show pos
data Vector = Vector Double deriving Show
data Point = Point
{ position :: Vector
, v, w :: Double
, neighbors :: [Point]
}
findNeighbors :: Vector -> [Vector] -> [Int]
findNeighbors (Vector n) vs = [i | (i, Vector n') <- zip [0..] vs, abs (n - n') < 1]
computeV, computeW :: Point -> Double
computeV (Point pos _ _ neighbors) = sum [n | Point { position = Vector n } <- neighbors]
computeW (Point pos v _ neighbors) = sum [v | Point { v = v } <- neighbors]
buildGraph :: [Vector] -> Array Int Point
buildGraph vs = answer where
answer = listArray (0, length vs-1) [point pos | pos <- vs]
point pos = this where { this = Point
{ position = announce "position" pos $ pos
, v = announce "v" pos $ computeV this
, w = announce "w" pos $ computeW this
, neighbors = announce "neighbors" pos $ map (answer!) (findNeighbors pos vs)
} }
newPositions :: Point -> [Vector]
newPositions (Point { position = Vector n, v = v, w = w }) = [Vector (n*v), Vector w]
update :: [Vector] -> [Vector]
update = newPositions <=< elems . buildGraph
并在 ghci 中运行:
*Main> length . show . update . map Vector $ [0, 0.25, 0.75, 1.25, 35]
computing position for position 0.0
computing v for position 0.0
computing neighbors for position 0.0
computing position for position 0.25
computing position for position 0.75
computing w for position 0.0
computing v for position 0.25
computing neighbors for position 0.25
computing v for position 0.75
computing neighbors for position 0.75
computing position for position 1.25
computing w for position 0.25
computing w for position 0.75
computing v for position 1.25
computing neighbors for position 1.25
computing w for position 1.25
computing position for position 35.0
computing v for position 35.0
computing neighbors for position 35.0
computing w for position 35.0
123
如您所见,每个位置的每个字段最多计算一次。