haskell - Haskell 中用于 DSL 的智能链操作符
问题描述
我有一个State
sum 类型,我需要评估这些状态列表中序列的有效性:
data State = Running | Idle | Stopped | Heating | Broken | Paused | Starting | Stopping
seq = [Stopped, Starting, Running, Paused, Running, Idle, Stopping, Stopped]
对于 的每个元素seq
,我需要评估它是否与它之前的一个或多个项目兼容。通过检查和 (可选)的i
值,这很容易做到。seq
seq !! (i-1)
seq !! (i-2)
但是,出于可读性和维护目的,我正在尝试基于链接运算符构建一个 mini-DSL,以便对于 中的每个元素seq
,我可以根据其前面的序列评估它是否有效。
按照下面的玩具示例:
-- current/naive implementation
-- (real-world follows the same model but is much more complex)
isValid :: [State] -> Int -> Bool
isValid seq i =
ite (curr == Running) (prev1 `elem` [Starting, Idle, Paused])
$ ite (curr == Idle) (prev2 `elem` [Paused] && prev1 `elem` [Running])
$ ite (curr == Broken) (prev3 `elem` [Running] && prev2 `elem` [Heating] && prev1 `elem` [Heating])
$ False
where curr = seq !! i
prev1 = seq !! (i-1)
prev2 = seq !! (i-2)
-- "ideal" implementation using a (|>) chaining operator
-- (with some syntactic liberties)
isValid :: [State] -> Int -> Bool
isValid seq i =
Running => [Starting, Idle, Paused]
$ Idle => [Paused] |> [Running]
$ Broken => [Running] |> [Heating] |> [Heating]
where (=>) curr prev = -- ...
(|>) prev1 prev2 = -- ...
我最苦苦挣扎的部分是,如何确保(|>)
了解它是否必须查找i-1
或i-2
取决于i-3
它被链接的次数,以便:
[Starting] -- seq !! (i-1) == Starting
[Starting] |> [Running] -- seq !! (i-2) == Starting && seq !! (i-1) == Running
[Starting] |> [Running] |> [Idle] -- seq !! (i-3) == Starting && seq !! (i-2) == Running && seq !! (i-1) == Idle
我并不特别喜欢遵循与上述“理想”版本中完全相同的语法糖,但任何可以接近它的想法或方法都会受到欢迎。
解决方案
您的 DSL 是一种基于上下文的谓词语言(它们说某事是真还是假)(它们在输入的不同部分进行评估)。
特别是,上下文由输入和其中的索引组成:
type Context = ([State], Int)
type Predicate = Context -> Bool
然后我们可以自上而下地研究语言,isValid
从根检查定义。首先,它是一个谓词序列。正如您已经注意到的那样,每一行都是一个逻辑暗示,如果任何暗示被破坏,您希望谓词失败。所以我们从谓词的连接开始:
infixr 4 &&.
(&&.) :: Predicate -> Predicate -> Predicate
p &&. q = \c -> p c && q c
蕴涵可以类似地定义,对应的布尔运算与运算相Ord
吻合(<=)
。
(=>.) :: Predicate -> Predicate -> Predicate
p =>. q = \c -> p c <= q c
在您建议的语法中,的左侧(=>)
实际上不是 a Predicate
,而是 a State
。您可以将其分解为(=>.)
我们刚刚定义的谓词的二元运算,以及将状态视为谓词的操作。当您编写 时Running => ...
,您试图说“如果当前状态是Running
,那么……”。所以一个状态s
对应于谓词“当前状态是s
”:
current :: State -> Predicate
current s = \(seq, i) ->
-- Naive version: (seq !! i) == s
index i seq == Just s
-- Total indexing function
index :: Int -> [a] -> Maybe a
index i seq = listToMaybe (drop i seq)
我们还想谈谈当前状态之前的状态。一种方法是转换谓词以在指向先前状态的修改后的上下文中对其进行评估:
prev :: Predicate -> Predicate
prev p = \(seq, i) -> p (seq, i-1)
在右侧,您还有状态列表,这意味着如果当前状态是其中任何一个,则匹配的谓词:
currentIn :: [State] -> Predicate
currentIn ss = \(seq, i) ->
case index i seq of
Nothing -> False
Just s -> s `elem` ss
使用所有这些基本构建块,我们可以构建更接近您正在寻找的语法的更高级别的组合器。
(|>)
在列表中查找当前状态(第一个参数),并将其第二个参数及时移回:
infixr 9 |>
(|>) :: [State] -> Predicate -> Predicate
ss |> q = currentIn ss &&. prev q
-- End delimiter/identity element for `(|>)`
true :: Predicate
true = \_ -> True
(=>+)
是一个状态在其左侧的暗示,但也将第二个参数转移到开始直接查看前一个状态(避免保留语法=>
)
infixr 8 =>+
(=>+) :: State -> Predicate -> Predicate
s =>+ q = current s &&. prev q
您的示例的相关操作是(&&.)
, (|>)
, (=>+)
,我们可以注意运算符优先级以避免括号。
isValid :: Predicate
isValid =
Running =>+ [Starting, Idle, Paused] |> true
&&. Idle =>+ [Paused] |> [Running] |> true
&&. Broken =>+ [Running] |> [Heating] |> [Heating] |> true
最后,我们需要从一个序列中生成所有相关的上下文,以验证整个序列:
allContexts :: [State] -> [Context]
allContexts seq = [(seq, i) | i <- [0 .. length seq - 1]]
validate :: [State] -> Bool
validate seq = all isValid (allContexts seq)
这应该足以让事情正常进行,但人们可能会抱怨的一个大问题是所有这些列表查找都很昂贵。将上下文的表示更改为更适合 DSL 操作的东西会更有效率。值得注意的是,我们希望能够查看当前状态以及之前的状态。因此,更好的表示应该使那些更直接可用:
type Context = [State] -- A reversed prefix of the whole sequence, so the current state is at the head, and other states precede it.
-- Example:
-- - Old context: ([a,b,c,d,e], 1) ("the element at position 1 in the list [a,b,c,d,e]")
-- - New context: [b,a]
allContexts :: [State] -> [Context]
allContexts seq = init (tails (reverse seq)) -- non-empty prefixes, reversed
您必须更新所有组合子,但 的定义isValid
应保持不变。(读者练习。)
推荐阅读
- pyspark - 将 parquet 文件模式导出为 JSON 或 CSV
- angular - [Angular]程序化管道创建?
- python - 从 Python 2 更改为 Python 3 后使用 sip.isdeleted() 时出错
- typo3 - TYPO3 是否可以跳过 9.5 更新
- html - 如何防止下拉列表向上?
- xml - 架构无效,无法从此架构源 XSD 生成 XML 数据
- android - 我们可以清除 SQLite 吗?如果是,那么告诉我怎么做?
- python - Win32Com python创建全局实例并在flask函数中使用
- html - LXML xpath 没有检测到“脏”的 html 文件,但是在缩进和清理它之后,它成功了
- javascript - 如何在 React Native 中从 JSON const 中提取数据