Как я могу кодировать и применять законные переходы состояния FSM с системой типов?

Предположим, что у меня есть тип 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 являются функторами, где все функции Things могут отображаться, кроме функций перехода.

С классами я мог бы создать somthing как:

class ToA t where  
    toA :: t -> A Thing

Что кажется еще более чистым.

Существуют ли другие предпочтительные подходы, которые будут работать в Haskell и PureScript?

Ответ 1

Здесь довольно простой способ использования параметра типа (потенциально phantom) для отслеживания состояния a 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 -}

Ответ 2

Вы можете использовать класс типа (доступен в PureScript) вместе с phantom типами, как предложил Джон, но используя класс типа в качестве окончательной кодировки типа путей:

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

Ответ 3

Поскольку ваша цель - запретить разработчикам выполнять незаконные переходы, вы можете захотеть просмотреть типы phantom. phantom типы позволяют моделировать безопасные типы без использования более сложных функций системы типов; поэтому они переносимы на многие языки.

Здесь PureScript-кодирование вашей проблемы:

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

data Thing a = Thing

transitionToA :: Thing C -> Thing A

Phantom хорошо работают, чтобы моделировать допустимые переходы состояния, когда у вас есть свойство, чтобы два разных состояния не могли перейти в одно и то же состояние (если только все состояния не могут перейти в это состояние). Вы можете обойти это ограничение, используя классы типов (class CanTransitionToA a where trans :: Thing a -> Thing A), но на этом этапе вам следует исследовать другие подходы.

Ответ 4

Если вы хотите сохранить список переходов, чтобы вы могли его обработать позже, вы можете сделать что-то вроде этого:

{-# 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

Вы можете превратить это в экземпляр Path, написав функцию конкатенации для Domino:

{-# 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')