使用Haskell状态monad的代码味道?
上帝我讨厌“代码味道”这个词,但我想不出更准确的东西。
在空闲时间,我正在devise一个高级语言和编译器,以便学习编译器构造,语言devise和函数式编程(编译器正在用Haskell编写)。
在编译器的代码生成阶段,我必须在遍历语法树时维护“状态”数据。 例如,编译stream程控制语句时,我需要为要跳转的标签生成唯一的名称(从传入,更新和返回的计数器生成的标签,并且计数器的旧值不能再次使用)。 另一个例子是当我在语法树中遇到内联string文字时,他们需要永久转换成堆variables(在空白string中最好存储在堆中)。 我目前正在包装整个代码生成模块在状态monad来处理这个。
我被告知编写一个编译器是一个非常适合于function范例的问题,但是我发现我的devise方式与C中的devise方式非常相似(你可以用任何语言编写C语言 – 甚至Haskell w /状态monads)。
我想学习如何在Haskell中思考(而不是在函数范式中) – 而不是在Haskell语法中使用C语言。 我应该真的试图消除/最小化使用状态monad,还是它是一个合法的function“devise模式”?
我会说,一般来说,国家不是一种代码味道,只要保持小而良好的控制。
这意味着使用诸如State,ST或自定义单元之类的monad,或者只是将包含状态数据的数据结构传递给几个地方,这不是一件坏事。 (实际上,monads只是在这方面做的协助!)然而,有遍布各处的状态(是的,这意味着你,IO monad!)是一种难闻的气味。
一个相当明显的例子就是当我的团队正在为我们参加2009年的ICFP编程竞赛 (代码可以在git://git.cynic.net/haskell/icfp-contest-2009)上进行工作时。 我们结束了几个不同的模块化部分:
- 虚拟机:运行模拟程序的虚拟机
- 控制器:几组不同的例程读取模拟器的输出并生成新的控制input
- 解决scheme:根据控制器的输出生成解决scheme文件
- 可视化器:几个不同的程序集,读取input和输出端口,并生成一些可视化或logging随着模拟进展发生的事情
它们中的每一个都有自己的状态,并且通过虚拟机的input和输出值以各种方式进行交互。 我们有几个不同的控制器和可视化器,每一个都有它自己的不同的状态。
这里的关键是任何特定国家的内部都限于自己的特定模块,每个模块甚至不知道其他模块的状态。 任何特定的有状态代码和数据集通常只有几十行,在该状态下有less量的数据项。
所有这一切都被粘在了一个十几行的小函数中,这些小函数没有访问任何状态的内部,只是按照正确的顺序调用正确的顺序,并且通过了一个非常有限的每个模块的外部信息量(当然还有模块的前一个状态)。
当状态以这种有限的方式使用时,types系统正在防止你无意中修改它,这是很容易处理的。 这是Haskell的美丽之一,它可以让你做到这一点。
一个答案是:“不要使用单子”。 从我的angular度来看,这完全是倒退。 单子是一种控制结构,除其他外,可以帮助您最大限度地减less触摸状态的代码量。 例如,如果以monadicparsing器为例,parsing器的状态(即正在parsing的文本,已经到达的文本,已经累积的警告等等)必须通过parsing器中使用的每个组合器。 然而,实际上直接操纵国家的只有less数几个联合者, 其他任何东西都使用这几个函数之一。 这可以让你在一个地方清楚地看到所有可以改变状态的less量代码,并且更容易地推断如何改变它,使得它更容易处理。
我已经在Haskell中编写了多个编译器,并且状态monad是许多编译器问题的合理解决scheme。 但是你想保持抽象—不要让你明白你正在使用monad。
这里有一个来自格拉斯哥Haskell编译器(我没有写,我只是围绕几个边缘)的例子,在那里我们build立控制stream图。 以下是制作图表的基本方法:
empyGraph :: Graph mkLabel :: Label -> Graph mkAssignment :: Assignment -> Graph -- modify a register or memory mkTransfer :: ControlTransfer -> Graph -- any control transfer (<*>) :: Graph -> Graph -> Graph
但是正如你发现的那样,保持独特标签的供应充其量也是单调乏味的,所以我们也提供这些function:
withFreshLabel :: (Label -> Graph) -> Graph mkIfThenElse :: (Label -> Label -> Graph) -- branch condition -> Graph -- code in the 'then' branch -> Graph -- code in the 'else' branch -> Graph -- resulting if-then-else construct
整个Graph
东西是一个抽象的types,翻译者只是快乐地构造纯粹function性的graphics,而没有意识到一切正在发生。 然后,当最终构build图时,为了将其转换为代数数据types,我们可以生成代码,我们给它提供唯一的标签,运行状态monad,并提取数据结构。
国家单体隐藏在下面; 虽然它没有暴露给客户, Graph
的定义是这样的:
type Graph = RealGraph -> [Label] -> (RealGraph, [Label])
或者更精确一点
type Graph = RealGraph -> State [Label] RealGraph -- a Graph is a monadic function from a successor RealGraph to a new RealGraph
随着国家monad隐藏在一层抽象的背后,它一点都不臭!
你看过属性语法 (AG)吗? (更多关于维基百科和Monad阅读器的文章 )?
使用AG可以将属性添加到语法树中。 这些属性在合成和inheritance属性中分开。
合成属性是从语法树中生成(或综合)的东西,这可能是生成的代码或所有注释,或者其他您感兴趣的东西。
inheritance的属性被input到你的语法树,这可能是环境,或者是在代码生成过程中使用的一系列标签。
在乌得勒支大学,我们使用属性语法系统 ( UUAGC )编写编译器。 这是一个从提供的.ag
文件生成haskell代码( .hs
文件)的预处理器。
虽然,如果你还在学习Haskell,那么也许现在不是开始学习另一层抽象的时候了。
在这种情况下,您可以手动编写语法为您生成的代码types,例如:
data AbstractSyntax = Literal Int | Block AbstractSyntax | Comment String AbstractSyntax compile :: AbstractSyntax -> [Label] -> (Code, Comments) compile (Literal x) _ = (generateCode x, []) compile (Block ast) (l:ls) = let (code', comments) = compile ast ls in (labelCode l code', comments) compile (Comment s ast) ls = let (code, comments') = compile ast ls in (code, s : comments') generateCode :: Int -> Code labelCode :: Label -> Code -> Code
我不认为使用State Monad是用来模拟状态的代码味道。
如果你需要通过你的函数对状态进行线程化,你可以明确地做到这一点,把状态作为一个参数并在每个函数中返回。 Monad提供了一个很好的抽象:它为你传递状态,并提供了很多有用的function来组合需要状态的函数。 在这种情况下,使用State Monad(或Applicatives)不是一种代码异味。
然而,如果你使用State Monad来模拟一个必要的编程风格,而一个function性的解决scheme就足够了,那么你只是使事情变得复杂。
一般来说,你应该尽量避免状态,但这并不总是实际的。 Applicative
使有效代码看起来更好,更实用,特别是树遍历代码可以从这种风格中受益。 对于名称生成的问题,现在有一个相当不错的包: 价值供应 。
那么,不要使用单子。 函数式编程的function是函数纯度和重用。 有一篇我曾经写过的教授的文章,他是帮助build立Haskell的人之一。
这篇论文被称为“ 为什么函数式编程很重要 ”,我build议你通读它。 这是一个很好的阅读。
让我们小心这里的术语。 国家本身并不坏, function语言有状态。 什么是“代码味道”是当你发现自己想分配variables值并改变它们。
当然,Haskell状态monad正是出于这个原因 – 就像I / O一样,它可以让你在受限制的环境中执行不安全和不起作用的事情。
所以,是的,这可能是一种代码味道。