我正在用 Haskell 创建一个简单的解析库,它使用 Template Haskell 将解析器规范编译为优化的代码。
但是,我试图弄清楚哪种代码对优化最有效,所以让我们考虑一个简单的解析器编写的解析器片段。
解析器可以看作是一个函数String -> (a, String)
,其中a
是我们在解析过程中想要识别的任何内容,而生成的字符串是我们未匹配的输入的任何部分。当解析器按顺序运行时,下一个解析器应该继续处理这个字符串。最后,如果最终结果是 shape ,我们可以说成功解析了一个完整的字符串("", _)
。
在此示例中,我们将使用解析任意数量的“a”字符的解析器。 (错误处理,即如果看到除“a”以外的任何字符时报告错误,已被排除在代码片段之外,以保持一切简洁,以使问题清晰)。
直截了当(天真的?)实施
如果我们用解析器编写递归解析器的代码,一个简单的手动实现可能是:
parser_raw :: String -> (String, String)
parser_raw str =
case str of
[] -> ([], [])
('a' : rest) ->
let
(res', rest') = parser_raw rest
in
(('a' : res'), rest')
这匹配任意数量的'a'
'。解析器递归直到字符串为空。
查看创建的 GHC Core,我们看到以下输出:
Rec {
-- RHS size: {terms: 36, types: 45, coercions: 0, joins: 0/1}
$wparser_raw
= \ w_s7BO ->
case w_s7BO of {
[] -> (# [], [] #);
: ds_d736 rest_a5S6 ->
case ds_d736 of { C# ds1_d737 ->
case ds1_d737 of {
__DEFAULT -> case lvl6_r7Fw of wild2_00 { };
'a'# ->
let {
ds3_s7vv
= case $wparser_raw rest_a5S6 of { (# ww1_s7C3, ww2_s7C4 #) ->
(ww1_s7C3, ww2_s7C4)
} } in
(# : lvl2_r7Fs
(case ds3_s7vv of { (res'_a65m, rest'_a65n) -> res'_a65m }),
case ds3_s7vv of { (res'_a65m, rest'_a65n) -> rest'_a65n } #)
}
}
}
end Rec }
在我未经训练的眼里,这似乎不是尾递归,因为我们首先进行递归调用,然后仍然需要将输出组合成一个元组。我还觉得奇怪/有趣的是let
,源代码中的 仍然存在于编译代码中,这使得计算看起来确实需要在多个(即非尾递归)步骤中完成。
我想到了另外两种编写此代码的方法。(也许还有更多?)
续传风格
parser_cps :: String -> (String, String)
parser_cps str = parser_cps' str id
where
parser_cps' :: String -> ((String, String) -> (String, String)) -> (String, String)
parser_cps' str fun =
case str of
[] -> fun ([], [])
('a' : rest) ->
parser_cps' rest ((\(tl, final) -> (('a' : tl), final) ) . fun)
这导致:
Rec {
-- RHS size: {terms: 28, types: 29, coercions: 0, joins: 0/0}
parser_cps_parser_cps'
= \ str_a5S8 fun_a5S9 ->
case str_a5S8 of {
[] -> fun_a5S9 lvl9_r7FM;
: ds_d72V rest_a5Sa ->
case ds_d72V of { C# ds1_d72W ->
case ds1_d72W of {
__DEFAULT -> lvl8_r7FL;
'a'# ->
parser_cps_parser_cps'
rest_a5Sa
(\ x_i72P ->
case fun_a5S9 x_i72P of { (tl_a5Sb, final_a5Sc) ->
(: lvl2_r7FF tl_a5Sb, final_a5Sc)
})
}
}
}
end Rec }
-- RHS size: {terms: 4, types: 4, coercions: 0, joins: 0/0}
parser_cps = \ str_a5S6 -> parser_cps_parser_cps' str_a5S6 id
从我们执行尾调用的意义上说,这看起来是尾递归的。然而,结果似乎确实是一个巨大的重击。在到达递归的基础之前,我们是否使用了大量额外的堆空间?
国家单子
-- Using `mtl`'s
-- import Control.Monad.State.Strict
parser_state :: String -> (String, String)
parser_state = runState parser_state'
where
parser_state' :: State String String
parser_state' = do
str <- get
case str of
[] -> return []
('a' : rest) -> do
put rest
res <- parser_state'
return ('a' : res)
这导致:
Rec {
-- RHS size: {terms: 26, types: 36, coercions: 0, joins: 0/0}
$wparser_state'
= \ w_s7Ca ->
case w_s7Ca of {
[] -> (# [], [] #);
: ds_d72p rest_a5Vb ->
case ds_d72p of { C# ds1_d72q ->
case ds1_d72q of {
__DEFAULT -> case lvl11_r7FO of wild2_00 { };
'a'# ->
case $wparser_state' rest_a5Vb of { (# ww1_s7Cl, ww2_s7Cm #) ->
(# : lvl2_r7FF ww1_s7Cl, ww2_s7Cm #)
}
}
}
}
end Rec }
-- RHS size: {terms: 8, types: 14, coercions: 6, joins: 0/0}
parser_state1
= \ w_s7Ca ->
case $wparser_state' w_s7Ca of { (# ww1_s7Cl, ww2_s7Cm #) ->
(ww1_s7Cl, ww2_s7Cm) `cast` <Co:6>
}
-- RHS size: {terms: 1, types: 0, coercions: 7, joins: 0/0}
parser_state = parser_state1 `cast` <Co:7>
在这里,我不确定如何解释核心:-code 的 Thelet
消失parser_raw
了。相反,递归调用立即在case
. 之后我们仍然需要将结果放入一个元组中,但是这个“无缺点”是否足以取悦递归之神?
所以,总结一下:这是编写简单解析函数的三种技术。我想知道其中哪一个是最节省内存的,以及如何通过查看 GHC 核心输出来获得一些直觉来识别尾递归。