Haskell状态monad使用代码的气味?

上帝我讨厌“代码味道”这个词,但我想不出任何更准确的东西。

我在我的业余时间学习有关编译器施工,语言设计,和函数式编程设计高级语言和编译器空白(编译程序被写入在Haskell)。

在编译器的代码生成阶段,我必须在遍历语法树时维护“状态”数据。 例如,编译流程控制语句,当我需要生成标签唯一的名称跳到(从则传递中,更新计数器生成的标签,和返回,计数器的旧值必须永远不会被再次使用)。 另一个例子是,当我遇到的行字符串字面量语法树,它们需要被永久转换成堆变量(在空白,字符串最佳存储在堆上)。 我目前正在将整个代码生成模块封装在monad状态中来处理这个问题。

有人告诉我,写一个编译器非常适合于功能模式有问题,但我发现我设计这在几乎相同的方式我会在C(设计它你真的可以用任何语言编写C - 甚至Haskell w /状态monads)。

我想学习如何在Haskell中思考(而不是在函数范式中) - 而不是在Haskell语法中使用C语言。 我是否应该真正尝试消除/最小化状态单子的使用,还是它是一种合法的功能“设计模式”?


我会说,一般来说,状态不是一种代码味道,只要它保持小而且控制良好。

这意味着使用单元如状态,ST或定制单元,或者只是将数据结构中包含的状态数据传递给几个地方,这不是一件坏事。 (实际上,monads只是帮助做到这一点!)然而,有状态遍布各地(是的,这意味着你,IO monad!)是一种难闻的气味。

一个相当明显的例子就是当我的团队正在为我们参加2009年ICFP编程大赛(代码可在git://git.cynic.net/haskell/icfp-contest-2009)上进行参与时。 我们结束了几个不同的模块化部分:

  • VM:运行模拟程序的虚拟机
  • 控制器:几组不同的例程读取模拟器的输出并生成新的控制输入
  • 解决方案:根据控制器的输出生成解决方案文件
  • 可视化器:几组不同的例程,可以读取输入和输出端口,并生成某种形式的可视化或记录随着模拟进展而发生的事情
  • 它们中的每一个都有自己的状态,它们都通过虚拟机的输入和输出值以各种方式进行交互。 我们有几个不同的控制器和可视化器,每个控制器和可视化器都有其不同的状态。

    这里的关键是,任何特定状态的内部都被限制在它们自己的特定模块中,并且每个模块甚至不知道其他模块的状态是否存在。 任何特定的有状态代码和数据集通常只有几十行,并且在该状态下有少量数据项。

    所有这些都被粘在一起,只有一行十几行的小函数,它们无法访问任何状态的内部结构,并且仅仅通过仿真循环来调用正确的顺序,并且通过了非常有限的每个模块的外部信息量(当然还有模块的前一个状态)。

    当状态以这种有限的方式使用时,类型系统阻止你无意中修改它,这很容易处理。 这是Haskell的美丽之一,它可以让你做到这一点。

    一个答案说,“不要使用单子。” 从我的角度来看,这完全是倒退。 Monads是一种控制结构,除其他功能外,它可以帮助您最大限度地减少触摸状态的代码量。 例如,如果以monadic解析器为例,解析器的状态(即正在解析的文本,已经获得的文本多少,已累积的警告等)必须贯穿解析器中使用的每个组合器。 然而,实际上直接操纵状态的只有少数组合器; 其他任何东西都使用这些少数功能之一。 这使您可以清楚地看到一个地方的所有少量代码,这些代码可以改变状态,并且更容易推断如何改变它,并且更容易处理。


    我在Haskell中编写了多个编译器,并且状态monad是许多编译器问题的合理解决方案。 但是你想保持抽象---不要让你明白你正在使用monad。

    这里有一个来自格拉斯哥Haskell编译器(我没有写;我只是围绕几条边)的例子,我们在那里构建控制流图。 以下是制作图表的基本方法:

    empyGraph    :: Graph
    mkLabel      :: Label -> Graph
    mkAssignment :: Assignment -> Graph  -- modify a register or memory
    mkTransfer   :: ControlTransfer -> Graph   -- any control transfer
    (<*>)        :: Graph -> Graph -> Graph
    

    但正如您发现的那样,保持独特标签的供应充其量也是单调乏味的,所以我们也提供这些功能:

    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事物是一种抽象类型,翻译者只是以纯粹的功能方式快速构建图表,而没有意识到任何单点事件正在发生。 然后,当最终构建图时,为了将其转换为代数数据类型,我们可以生成代码,我们给它提供唯一标签,运行状态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 Reader中的文章)?

    使用AG可以将属性添加到语法树中。 这些属性在合成和继承属性中分开。

    合成属性是您从语法树中生成(或综合)的东西,可能是生成的代码或所有注释,或其他任何您感兴趣的内容。

    继承的属性被输入到您的语法树中,这可能是环境或代码生成期间使用的标签列表。

    在乌得勒支大学,我们使用属性语法系统(UUAGC)编写编译器。 这是一个预处理器,它可以从提供的.ag文件生成haskell代码( .hs文件)。


    尽管如果你还在学习Haskell,那么也许现在不是开始学习另一层抽象的时候了。

    在这种情况下,您可以手动编写语法为您生成的代码类型,例如:

    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
    
    链接地址: http://www.djcxy.com/p/7421.html

    上一篇: Use of Haskell state monad a code smell?

    下一篇: Does functional programming replace GoF design patterns?