如何构建一个观察此状态机中每个转换的函数/类型?

chi*_*ro2 6 haskell finite-automata

我的问题的要点是我有一个确定性状态自动机,它根据一个移动列表进行转换,我希望这个转换序列充当另一个函数的"计算上下文".这个其他函数会在每次转换时观察状态机,并用它做一些计算,模糊地让人联想到模型视图模式.通常,这个其他功能可能只是读取机器所处的当前状态,并将其打印到屏幕上.

我的状态机实现:

data FA n s = FA { initSt1 :: n, endSt1 :: [n], trans1 :: n -> s -> n }

-- | Checks if sequence of transitions arrives at terminal nodes
evalFA :: Eq n => FA n s -> [s] -> Bool
evalFA fa@(FA _ sfs _ ) = (`elem` sfs) . (runFA fa)

-- | Outputs final state reached by sequence of transitons
runFA :: FA n s -> [s] -> n
runFA (FA s0 sfs trans) = foldl trans s0
Run Code Online (Sandbox Code Playgroud)

例如:

type State = String
data Trans = A | B | C | D | E

fa :: FA State Trans
fa = FA ("S1") ["S4","S5"] t1

-- | Note non-matched transitions automatically goes to s0
t1 :: State -> Trans -> State
t1 "S1" E = "S1"
t1 "S1" A = "S2"
t1 "S2" B = "S3"
t1 "S2" C = "S4"
t1 "S3" D = "S5"
t1 _  _   = "S1"

runFA fa [A,B]   -- | S3
Run Code Online (Sandbox Code Playgroud)

Gab*_*lez 9

我将把这个答案分成两部分.第一部分将回答您的原始问题,第二部分将回答您在评论中提出的非确定性FSA问题.

管道

您可以使用pipes在计算之间交错效果.首先,我将从稍微修改过的代码版本开始:

data FA n s = FA { initSt1 :: n, endSt1 :: [n], trans1 :: n -> s -> n }

data State = S1 | S2 | S3 | S4 | S5 deriving (Eq, Show)
data Trans = A | B | C | D | E deriving (Read)

fa :: FA State Trans
fa = FA (S1) [S4,S5] t1

-- | Note non-matched transitions automatically goes to s0
t1 :: State -> Trans -> State
t1 S1 E = S1
t1 S1 A = S2
t1 S2 B = S3
t1 S2 C = S4
t1 S3 D = S5
t1 _  _ = S1
Run Code Online (Sandbox Code Playgroud)

唯一的区别是,我使用的是枚举,而不是StringState.

接下来,我将实现您的转换Pipe:

runFA :: (Monad m, Proxy p) => FA n s -> () -> Pipe (StateP n p) s n m r
runFA (FA _ _ trans) () = forever $ do
    s <- request ()
    n <- get
    put (trans n s)
    respond n
Run Code Online (Sandbox Code Playgroud)

让我们仔细看看Pipe:

() -> Pipe (StateP n p) s n m r
                   ^    ^ ^
                   |    | |
 'n' is the state -+    | |
                        | |
          's's come in -+ +- 'n's go out
Run Code Online (Sandbox Code Playgroud)

所以你可以认为这是一个有效的scanl.它s使用s 接收s 流request并输出ns 流respond.如果我们想要,它实际上可以直接交错效果,但我会将效果外包给其他处理阶段.

当我们将其表述为a时Pipe,我们可以选择输入和输出流.例如,我们可以将输入连接到不纯的stdin并将输出连接到不纯的stdout:

import Control.Proxy
import Control.Proxy.Trans.State

main = runProxy $ execStateK (initSt1 fa) $
    stdinS >-> takeWhileD (/= "quit") >-> mapD read >-> runFA fa >-> printD
Run Code Online (Sandbox Code Playgroud)

这是一个处理管道,你可以读到:

  • Pipe以初始状态执行以下操作initSt
  • 从标准输入流中传输值
  • 保持流式传输,直到其中一个值为止 "quit"
  • 应用于read所有值以将它们转换为Transes
  • 通过我们的扫描有限状态自动机运行它们
  • 打印State自动机发出的s

我们来试试吧:

>>> main
A
S1
B
S2
C
S3
A
S1
quit
S2
>>>
Run Code Online (Sandbox Code Playgroud)

注意它是如何返回State我们的自动机所在的最后一个.然后你可以fmap测试这个计算,看看它是否在终端节点中结束:

>>> fmap (`elem` [S1, S2]) main
A
S1
B
S2
C
S3
A
S1
quit
True
Run Code Online (Sandbox Code Playgroud)

或者,我们也可以将自动机连接到纯输入和输出:

import Control.Proxy.Trans.Writer
import Data.Functor.Identity

main = print $ runIdentity $ runProxy $ runWriterK $ execStateK (initSt1 fa) $
    fromListS [A, C, E, A] >-> runFA fa >-> liftP . toListD
Run Code Online (Sandbox Code Playgroud)

那条管道说:

  • 在纯计算(即`runIdentity)中运行它并打印纯结果
  • 使用Writer记录所有我们参观了美国
  • 用于State跟踪我们当前的状态
  • 纯粹提供预定义转换列表
  • 通过我们的FSA运行这些转换
  • 将输出记录到Writer,liftP用于指定我们的目标Writer

我们也试试这个:

>>> main
(S2,[S1,S2,S4,S1])
Run Code Online (Sandbox Code Playgroud)

这将输出最终状态和访问状态列表.

ListT

现在,您提出了第二个问题,即您如何进行有效的非确定性计算.丹尼尔实际上是错误的:您也可以使用非确定性交错效果pipes!诀窍是使用ProduceT,这是pipes实现ListT.

首先,我将展示如何使用ProduceT:

fsa :: (Proxy p) => State -> Trans -> ProduceT p IO State
fsa state trans = do
    lift $ putStrLn $ "At State: " ++ show state
    state' <- eachS $ case (state, trans) of
        (S1, A) -> [S2, S3]
        (S2, B) -> [S4, S5]
        (S3, B) -> [S5, S2]
        (S4, C) -> [S2, S3]
        (S5, C) -> [S3, S4]
        (_ , _) -> [S1]
    return state'
Run Code Online (Sandbox Code Playgroud)

上面的代码说:

  • 打印当前状态
  • 非确定性地绑定许多可能的转换
  • 返回新选择的状态

为了避免手动状态传球,我将包装fsaStateT:

import qualified Control.Monad.Trans.State as S

fsa2 :: (Proxy p) => Trans -> S.StateT State (ProduceT p IO) State
fsa2 trans = do
    s <- S.get
    s' <- lift $ fsa s trans
    S.put s'
    return s'
Run Code Online (Sandbox Code Playgroud)

现在我可以通过使用轻松地在多个转换上运行生成器mapM.当我完成后,我将其编译为Producer使用runRespondT:

use1 :: (Proxy p) => () -> Producer p State IO ()
use1 () = runRespondT $ (`S.execStateT` S1) $ do
    mapM_ fsa2 [A, B, C]  -- Run the generator using four transitions
Run Code Online (Sandbox Code Playgroud)

这会产生一个管道,其效果是打印它正在遍历的状态,并输出它遇到的最终状态流.我将输出连接到打印阶段,以便我们可以同时观察:

>>> runProxy $ use1 >-> printD
At State: S1
At State: S2
At State: S4
S2
S3
At State: S5
S3
S4
At State: S3
At State: S5
S3
S4
At State: S2
S1
Run Code Online (Sandbox Code Playgroud)

我们可以观察自动机的路径以及它如何回溯.它打印出每个步骤之后的当前位置,然后在它们到达时立即发出所有7个最终状态.

对不起,如果这篇文章有点粗糙,但这是我能赶时间做的最好的.

  • 我提出了一个关于SO的特殊案例,我们可以不止一次地赞成@GabrielGonzalez的答案! (2认同)