我想要构建匹配字符串的NFA。我有:
data State = State Char State | Split State State | Final
注意,State
可以是无限递归的,因为我想构建像下面这样的NFA:
-- NFA for regex `1*`
match1 = State '1' loop; loop = Split match1 Final
当对字符串执行这样的NFA时,我必须跟踪已经访问过的状态,以打破循环。我用Set
来实现这一点,而CCD_2又需要State
来实现Ord
。然而,派生的Ord
实例的compare
当然也有同样的问题,即它一直在递归。
我该如何解决这个问题?我看过StableName
,但makeStableName
使用IO
monad,因此在实现compare
(AFAIK)时无法使用。
我现在唯一能想到的就是向State
添加某种id以进行跟踪:
data State = State Int Char State | Split Int State State | Final
但这感觉相当不合法,尤其是考虑到客户端代码必须确保其唯一性。
TL;DR:不要将State
添加到Set
,而是使用StableName State
实现instance Hashable State
,并添加到CCD16
正如评论中所指出的,如果不用某种唯一的id标记每个状态,这个问题就不容易解决。
我的第一次尝试是在State
和Split
ctor中添加一个Int
,但这与NFA的构建方式不一致。由于这不是由客户端代码直接完成的,而是由构建器(解析器使用该构建器)完成的,后者必须自动生成唯一的id。
使用状态monad似乎是可能的,但问题是,实现会不断为已经是"monad"的状态生成新的唯一id;id'ed";由于NFA中的循环。
最终将我推向正确方向的是在一条(现已删除)评论中提到Data.Reify。
Data.Reify依赖于StableName
s和HashMap
来检测周期,所以我想我可以通过在StableName
的帮助下为State
实现Hashable
来做同样的事情。
以下是我的结局。
前言:
import qualified Control.Monad.State.Lazy as M
import Data.Hashable
import qualified Data.HashMap.Lazy as Map (empty, insert, lookup)
import Data.HashMap.Lazy (HashMap)
import System.IO.Unsafe (unsafePerformIO)
import System.Mem.StableName (eqStableName, makeStableName)
相关类型再次出现(因为我现在已经更改了它们):
data State = State {accepts :: Match, next :: State}
| Split {left :: State, right :: State}
| Final
data Match = AnyChar
| LiteralChar Char
| CharRange (Char, Char)
deriving (Eq, Ord, Show)
Hashable
的实施(需要Eq
);
instance Eq State where
x == y = unsafePerformIO $
M.liftM2 eqStableName (makeStableName x) (makeStableName y)
instance Hashable State where
hashWithSalt salt state = unsafePerformIO $
hashWithSalt salt <$> makeStableName state
我认为这种实现有三个可能的问题:
- 相同的
State
在评估后可能具有不同的StableName
。这是已知的StableName
行为,在NFA执行期间没有问题,因为在最坏的情况下,当计算出在不消耗输入的情况下可到达的下一个可能状态时,Split
将被跟踪两次 - 通过相同的令牌,CCD_ 31可以针对相同的CCD_ 33返回CCD_。到目前为止,我还不知道其中的含义♂️
- 我不知道
unsafePerformIO
在这种情况下有多安全。但我看到其他代码也做了类似的事情,Data.Reify文档说它应该是安全的
NFA执行的实现目前在存在循环的情况下工作,instance Show State
的实现也是如此。