如何使用类型系统编码和实施合法的FSM状态转换?

时间:2015-08-17 21:02:48

标签: haskell purescript

假设我的类型Thing带有州属性A | B | C
法律状态转换为A->B, A->C, C->A

我可以写:

transitionToA :: Thing -> Maybe Thing

如果Nothing处于无法转换为Thing的状态,则会返回A

但是我想定义我的类型,并且转换函数的转换只能在适当的类型上调用。

一个选项是创建单独的类型AThing BThing CThing,但在复杂的情况下似乎无法维护。

另一种方法是将每个状态编码为它自己的类型:

data A = A Thing
data B = B Thing
data C = C Thing

transitionCToA :: C Thing -> A Thing

这对我来说似乎更干净。但是我发现A,B,C是函数,除了转换函数之外,所有的函数都可以被映射。

使用类型类,我可以创建像:

这样的东西
class ToA t where  
    toA :: t -> A Thing

这似乎更清洁了。

还有其他首选方法适用于Haskell和PureScript吗?

4 个答案:

答案 0 :(得分:10)

这是一种使用(可能是幻影)类型参数来跟踪Thing所处状态的相当简单的方法:

{-# LANGUAGE DataKinds, KindSignatures #-}
-- note: not exporting the constructors of Thing
module Thing (Thing, transAB, transAC, transCA) where

data State = A | B | C
data Thing (s :: State) = {- elided; can even be a data family instead -}

transAB :: Thing A -> Thing B
transAC :: Thing A -> Thing C
transCA :: Thing C -> Thing A

transAB = {- elided -}
transAC = {- elided -}
transCA = {- elided -}

答案 1 :(得分:5)

您可以使用类型类(在PureScript中可用)以及John建议的幻像类型,但使用类型类作为路径类型的最终编码:

data A -- States at the type level
data B
data C

class Path p where
  ab :: p A B -- One-step paths
  ac :: p A C
  ca :: p C A
  trans :: forall a b c. p c b -> p b a -> p c a -- Joining paths
  refl :: forall a. p a a

现在您可以创建一种有效路径:

type ValidPath a b = forall p. (Path p) => p a b

roundTrip :: ValidPath A A
roundTrip = trans ca ac

路径只能使用您提供的一步路径构建。

您可以编写实例来使用您的路径,但重要的是,任何实例都必须遵守类型级别的有效转换。

例如,这是一个计算路径长度的解释:

newtype Length = Length Int

instance pathLength :: Path Length where
  ab = Length 1
  ac = Length 1
  ca = Length 1
  trans (Length n) (Length m) = Length (n + m)
  refl = Length 0

答案 2 :(得分:2)

由于您的目标是阻止开发人员执行非法转换,您可能需要查看幻像类型。幻像类型允许您在不利用类型系统的更高级功能的情况下为类型安全转换建模;因此,它们可以移植到多种语言。

以下是上述问题的PureScript编码:

foreign import data A :: *
foreign import data B :: *
foreign import data C :: *

data Thing a = Thing

transitionToA :: Thing C -> Thing A

当您具有两个不同状态无法转换到相同状态的属性时,幻像类型可以很好地模拟有效状态转换(除非所有状态都可以转换到该状态)。您可以使用类型类(class CanTransitionToA a where trans :: Thing a -> Thing A)来解决此限制,但此时,您应该调查其他方法。

答案 3 :(得分:2)

如果你想存储转换列表以便以后处理它,你可以这样做:

{-# LANGUAGE DataKinds, GADTs, KindSignatures, PolyKinds #-}

data State = A | B | C
data Edge (a :: State) (b :: State) where
    EdgeAB :: Edge A B
    EdgeAC :: Edge A C
    EdgeCA :: Edge C A

data Domino (f :: k -> k -> *) (a :: k) (b :: k)  where
    I :: Domino f a a
    (:>>:) :: f a b -> Domino f b c -> Domino f a c

infixr :>>:

example :: Domino Edge A B
example = EdgeAC :>>: EdgeCA :>>: EdgeAB :>>: I

您可以通过为Domino编写连接函数将其转换为Path的实例:

{-# LANGUAGE FlexibleInstances #-}
instance Path (Domino Edge) where
    ab = EdgeAB :>>: I
    ac = EdgeAC :>>: I
    ca = EdgeCA :>>: I

    refl = I
    trans I es' = es'
    trans (e :>>: es) es' = e :>>: (es `trans` es')

事实上,这让我想知道Hackage是否已经有一个定义“索引幺半群”的包:

class IMonoid (m :: k -> k -> *) where
    imempty :: m a a
    imappend :: m a b -> m b c -> m a c

instance IMonoid (Domino e) where
    imempty = I
    imappend I es' = es'
    imappend (e :>>: es) es' = e :>>: (es `imappend` es')