微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如何在 Haskell 中实现状态图?

如何解决如何在 Haskell 中实现状态图?

在阅读了《实用 UML 状态图 in C/C++” by Miro Samek,我渴望有时间尝试一下。 最近,我开始自学 Haskell 和函数式 编程。

我关于 Haskell 的书只有几章,让我震惊的是 状态图可能很困难,甚至与 Haskell 格格不入。毕竟付出了很大的努力 致力于创建无状态程序,或者至少保持所有不纯 部分代码纯代码部分分开。

当我在网上搜索“Haskell”结合“statechart”时,我 几乎一无所获!这激起了我的好奇心。毕竟,Haskell 是一个 相当古老的通用编程语言。怎么会这样 似乎几乎没有与状态图相关的活动?

Haskell sub-Reddit 中提出了几种可能的解释 线程“haskellers thoughts on statecharts”,我希望我的短 摘录不会扭曲任何作者的观点:

  1. /.../ Haskell 社区在 UI 方面总体上不是很活跃 发展 /.../

  2. /.../ Statecharts 的缺点是所有的灵活性 他们提供 make 模型检查(因此也是基于模型的 测试)更难。 /…/

  3. /…/ 状态机是状态和转换的集合 规则。但是对于实现状态是多余的,您可以使用 单独的转换规则,它们整齐地表示为(一个相互 函数的递归族。 /.../ 但是 Haskell 不受此限制 限制,因此显式实现状态机通常是 多余的。

    Hense(原文如此!),作为一个 Haskeller,我真的不需要明确的状态 大部分时间是机器。一等公民的功能和 TCO 使 它们充其量是一个实现细节,最坏的情况是不需要的。 /…/

  4. 一个。 /.../ 使用类似的搜索词可能更幸运 “转换系统”、“演员模型”或“状态转换器” Haskell 生态系统。 /…/

    B. /.../ 函数式反应式编程 (FRP) 和箭头是 在 Haskell 中实现信号流。 /…/

    c. /.../ State monad 转换器可以模拟它。如果你有 中间步骤中的外部信号,您可以将其嵌入 延续单子。 /…/

我允许自己解释,并稍微评论一下:

  1. 我发现 H. 程序员不使用状态图,因为 他们不会做那种有点难以置信的编程。我可以 错了,但我认为应用领域要广泛得多 不仅仅是用户界面。
  2. 这个论点有点超出我的想象。作者可能重点 在某种类型的测试,某种工具上。我的意思是,不会 预先定义好的状态集,使测试更容易,如果 什么?
  3. 这可能是最有趣的论点,即显式声明 在 Haskell 中是多余的,因此需要代码可见 状态机。我的反对也许不公平,但不是国家 机器抽象(或者更确切地说,它在代码中的可见化身) 发明是为了让事情更清楚更容易理解? 它解决的问题——使未指定的、事实上的状态 隐藏在“普通”程序中的机器,根据 大量组织不良的变量的值,更明显 — 这不是通过摆脱状态机来否定吗?
  4. 状态图真的就这么简单吗? 简单地称为完全不同的东西? ……或者是这些 在 Haskell 的世界里,更好 解决同样的问题, 有效地使状态图真正变得多余?

一旦我读完关于 Haskell 和 FP 的书,这一切会变得尴尬吗?

解决方法

您可以使用状态图来实现,但您不需要这样做,因为 Haskell 的工作级别高于其他语言,并将状态图设计包含在代码中。这是你如何做到的。 (注意:我从一个 monad 转换器版本简化了这个,它也处理异常,如果我犯了任何错误,很抱歉)

首先,你可以像这样在 Haskell 中定义一个状态机:

newtype AutoS i o = AutoS {runAutoS :: (o,i -> AutoS i o)}

换句话说,状态机由其最新输出 o 和从输入 i 到下一个状态的函数组成。我称它为“自动”,因为它们是自动机,这是状态机的数学术语。

现在,你可以只取一UML状态图,并将其直接转化为AutoS值的集合。但还有更好的方法。

newtype Auto i o a = Auto {runAuto :: (a -> AutoS i o) -> AutoS i o}

这是 continuation monad 的变体。对于传统的 monad,每个步骤的结果都用作下一个步骤的参数,但在延续中,每个步骤 gets the next step as a function argument 并将其结果传递给该函数。这听起来像是一种奇怪的做事方式,但这意味着您可以从 monad 内部访问“其余的计算”,这让您可以做一些聪明的事情。但在解释之前,这里是实例。这是值得花一点的时间打坐对特定的单子实例。在所有这些函数中,k 是延续;表示其余计算的参数。

instance (Functor m) => Functor (Auto i o) where
   fmap f (Auto act) = Auto $ \k -> act (k . f)

instance (Monad m) => Applicative (Auto i o ) where
   pure v = Auto $ \k -> k v
   f <*> v = Auto $ \k -> runAuto f $ \g -> runAuto v (k . g)

instance (Monad m) => Monad (Auto i o) where
   return = pure
   v >>= f = Auto $ \k -> runAuto v $ \x -> runAuto (f x) k

所以现在我们可以在 Auto 中编写操作。但是他们能做什么呢?这是 yield 函数,它是 Auto 中唯一的原语:

yield :: o -> Auto i o i
yield v = Auto $ \k -> AutoS (v,k)

这就是神奇之处。像以前一样,k 是延续(即此步骤之后的所有内容)。请记住,步骤的结果被传递给该功能(即yield的结果)。通过将其与产生的值包装在 AutoS 中,我们将该值作为状态机的输出传递,并为状态机调用者提供下一个状态转换。

因此,现在我们可以编写一元代码,而不是将程序编写为具有许多命名状态的状态机。当我们的 monadic 代码想要与世界其他地方交换信息时,它使用 yield 发送一个值并获得一个新值作为回报。

有一个 old joke 是关于一位数学家被要求在笼子里捕捉狮子。数学家进入笼子,关上门,并宣布(通过几何倒置)他现在在笼子外面,其他所有东西,包括狮子在内,都在笼子里。这个延续 monad 有点像这样;你的一元代码就像数学家;它将一个值传递给 yield 并返回一个结果。但是从世界其他地方的角度来看,屈服值状态机(笼子),下一个状态转换返回成为yield 的结果。

此处您唯一需要的是一种运行 Auto 操作的方法:

startMachine :: Auto i o Void -> AutoS i o
startMachine act = runAuto act $ error "Can't happen: Auto terminated."

这假设您的状态机没有结束状态。您从 void package 获得 Void。如果你需要一个结束状态,那么它必须有一个独立的类型,该类型必须与其他内容一起流浪汉身边。在原始的 Cont 延续 monad 中,该类型是 r

这样做的一大好处是状态机的逻辑就像普通程序一样表达为一系列步骤。如果没有单子的方式你已经是状态和转换的列表,这使得下面的逻辑很辛苦。这就像阅读一个程序,其中每一行都以 GOTO 结尾。当代码难以理解时,您必须使用设计符号来解释它。状态图有点像状态机的流程图。通过使用 monad,您可以直接在状态图级别进行编程,使其变得无关紧要,就像结构化编程使流程图无关紧要一样。

创建 monad 转换器版本 AutoT 留给学生作为练习。提示:newtype AutoS i o m = runAutoS :: m (o,i -> AutoS i o m)},然后查看 monad 转换器包中的 ContT

实际上,您可以通过将 Auto 替换为 Cont 来实现此目的。我没有,因为我也需要例外,而 ContT 不做例外。添加诸如适当的异常处理之类的内容也留给学生作为练习。提示:newtype AutoS i o e m = AutoS {runAutoS :: m (o,i -> AutoS i o e m,e -> AutoS i o e m) }

修改:您提到的容易性测试状态图的执行

是的,对于足够小的“测试”值。如果您使用由枚举索引的状态以传统语言实现状态图,那么测试您的状态转换是否与状态图匹配确实是微不足道的。但是,您仍然需要验证设计中的状态图是否正确。这与验证 Auto 中的 monadic 代码是否正确是完全相同的问题,因为它们都在同一抽象级别描述解决方案。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。