【发布时间】:2018-05-29 08:10:58
【问题描述】:
我可以定义一个玩具状态机(带有微不足道的输入)如下:
--------------------------------------------
-- module State where
data State = A | B Int
--------------------------------------------
-- module A where
-- import State
transitionA :: State
transitionA = B 10
--------------------------------------------
-- module B where
-- import State
transitionB :: Int -> State
transitionB i
| i < 0 = A
| otherwise = B (i-1)
--------------------------------------------
-- module StateMachine where
-- import State
-- import A
-- import B
transition :: State -> State
transition A = transitionA
transition (B i) = transitionB i
如果我现在决定添加一个新状态,我必须:
- 修改 State 模块以添加新状态,比如说
data State = A | B Int | C Double Double
在模块 C 中添加新的转换函数 transitionC
在最后一个模块中导入 C,并将 C 大小写添加到模式匹配中
我想进行一些设置,这样我只需执行第 2 步(编写一个新的转换函数),其他一切都会自动处理。
例如,可以尝试使用存在类型来执行以下操作:
--------------------------------------------
{-# LANGUAGE ExistentialQuantification #-}
-- module State where
class State s where
transition :: s -> AState
data AState = forall s. State s => AState s
instance State AState where
transition (AState s) = transition s
-------------------------------------
-- module A where
-- import State
-- import B
data A = A
instance State A where
transition _ = AState (B 10)
-------------------------------------
-- module B where
-- import State
-- import A
data B = B Int
instance State B where
transition (B i)
| i < 0 = AState ( A )
| otherwise = AState ( B (i-1) )
这很方便:要添加一个新的状态,我们只需要做一件事,就是在一个新的模块中写一个数据类型及其相关的转换函数,其他什么都不需要改变。不幸的是,这种方法不起作用,因为它会创建循环依赖,例如在这种情况下,A 需要引用 B,B 需要引用 A。
我也尝试过使用可扩展的求和类型(多态变体),但同样的问题会出现,除非我们提前在单独的模块中声明所有可能的状态,以便后续模块可以引用它们。也就是说,它可以消除第 3 步,但不能消除第 1 步。
这种问题可以使用(Conor McBride 的版本)索引单子来解决吗?似乎我们可以使用某种我们事先不知道返回状态的索引状态 monad,我从他对 What is indexed monad? 的回答中收集到,这是 MonadIx 实现的。
【问题讨论】:
-
你打算如何绕过循环依赖?如果 A 不知道 B(或 B 不知道 A),你怎么能引用它?