我正在Haskell中实现类似于Spreadsheet引擎的东西。
有ETables
,其中包含以AST形式包含表达式的单元格行(例如BinOp + 2 2
),其中可以包含对ETables
其他单元格的引用。
主要功能应将这些ETables
转换为VTables
,其中包含单元格中完全解析的值(例如,单元格BinOp + 2 2
应解析为IntValue 4
)。当单元格没有外部引用时,这非常容易,因为您可以从单元格的表达式AST(例如eval (BinOpExpr op l r) = IntValue $ (eval l) op (eval r)
,从减去拆箱和类型检查)开始一直构建值到表({{1 }})
然而,当外部引用被抛入混合时,我无法想到处理这种情况的“自然”方式。我是否正确地假设我不能只在被引用的单元格上调用evalTable = (map . map) eval rows
并使用它的值,因为Haskell不够智能来缓存结果并在单独评估该单元格时重新使用它?
我想到的最好的事情是使用逐渐填充的eval
,因此缓存是显式的(每个eval调用在返回之前使用返回值更新状态)。这应该有效,但感觉“程序性”。是否有更多惯用的方法可供我使用?
答案 0 :(得分:4)
默认情况下,Haskell没有记忆,因为这通常会占用太多内存,所以你不能仅仅依靠eval
做正确的事情。但是,惰性求值的本质意味着数据结构在某种意义上是被记忆的:大型惰性结构中的每个thunk只被评估一次。这意味着您可以通过在内部定义一个大型惰性数据结构来记忆函数,并通过访问结构来替换递归调用 - 结构的每个部分最多只会被评估一次。
我认为对电子表格进行建模的最优雅方法是一个大的,懒惰的有向图,其中单元格为节点,参考为边缘。然后你需要以递归的方式定义VTable
图形,这样所有的递归都会通过图形本身,这将按照我上面描述的方式记忆结果。
有一些方便的方法来建模图表。一种选择是使用带有整数的显式映射作为节点标识符 - IntMap
甚至可以使用某种类型的数组。另一种选择是使用现有的图库;这将为您节省一些工作并确保您有一个很好的图形抽象,但需要先付出一些努力才能理解。我是fgl,"功能图库"的忠实粉丝,但它确实需要一些前期阅读和思考才能理解。性能不会有很大差异,因为它也是以IntMap
的形式实现的。
稍微嘟嘟我自己的号角,我已经写了几篇博文来扩展这个答案:一个关于memoization with lazy structures(有图片!)和一个关于functional graph library。把这两个想法放在一起应该可以得到你想要的东西,我相信。