【问题标题】:How to write a parser for a mutually recursive ADT without recursion and side effects?如何为没有递归和副作用的相互递归 ADT 编写解析器?
【发布时间】:2015-07-06 21:37:39
【问题描述】:

警告:收到冗长而复杂的问题。

有些人认为total functional programming 是一个有价值的想法,因此寻找实现它的技术也是如此。请注意,我怎样才能为相互递归的 ADT 编写解析器而没有递归和副作用?在这里,我将任何不是strongly normalizing 的术语定义为“递归”。

我尝试过的:

请注意以下相互递归的 ADT:

data Tree = Node Int [Tree]
tree = Node 10 [Node 20 [], Node 30 [], Node 40 []]

tree可以序列化为:

tree_serial = [0,10,0,0,20,1,0,0,30,1,0,0,40,1,1] :: [Int]

为了简单起见,这里使用整数,0 表示NodeCons 单元格的开始(取决于解析器的状态),1 表示Nil,其余表示数据。我们可以很容易地使用副作用为它编写一个解析器:

var string = [0,10,0,0,20,1,0,0,30,1,0,0,40,1,1];

function parse(string){
    function getInt(){
        return string.shift();
    };
    function parseTree(){
        var chr = getInt();
        if (chr === 0)
            return ["Node",getInt(),parseList()];
    };
    function parseList(){
        var chr = getInt();
        if (chr === 0)
            return ["Cons",parseTree(),parseList()];
        if (chr === 1)
            return "Nil";
    };
    return parseTree();
};

console.log(JSON.stringify(parse(string)));

这里,getInt 是有效的:它从字符串中获取下一个 int。我们可以使用 Parsec 或类似工具轻松优雅地将其转换为 Haskell - 但为了更好地理解,我跳过了这些并定义了一个精简的解析器类型:

data Parser res = GetInt (Int -> Parser res) | Return res
runParser (GetInt fn) (c:cs) = runParser (fn c) cs
runParser (Return res) c     = res

这类似于一元解析器,除了更明确:

main = do
    let parsePair = (GetInt (\a -> (GetInt (\b -> Return (a,b)))))
    print $ runParser parsePair [1,2,3,4,5] 

使用它,我们可以定义没有副作用的解析器:

data Tree = Node Int [Tree] deriving Show

parseTree = treeParser Return where
    treeParser = (\ cont -> 
        GetInt (\ _ ->  
            GetInt (\ tag -> 
                listParser (\ listParsingResult -> 
                    (cont (Node tag listParsingResult)))))) 
    listParser = (\ cont -> 
        GetInt (\ a -> 
            if a == 0 
                then treeParser (\x -> listParser (\y -> cont (x : y)))
                else cont []))

main = do
    let treeData = [0,10,0,0,20,1,0,0,30,1,0,0,40,1,1]
    print $ runParser parseTree treeData

这会按预期输出Node 10 [Node 20 [],Node 30 [],Node 40 []]。请注意,这仍然使用递归,我不得不使用cont 在两个递归函数之间传递控制。现在,我知道有两种摆脱递归的策略:

1. Use folds.

2. Use church numbers for bounded recursion.

在这里使用折叠显然不可行,因为没有可折叠的结构(我们正在构建它!)。如果我们解析列表而不是树,那么使用教堂编号将是完美的,因为它们的工作方式与有界递归的 Y 组合器完全一样——而且,知道列表的长度,我们可以写 toChurch listLength listParser init。但是,这种情况的问题在于,存在相互递归,使用哪个教堂编号并不明显。我们有许多层的列表和长度不可预测的树。事实上,如果我们使用足够大的教会编号,它无需递归即可工作,但代价是增加了工作量。这是一个实际有用的程序的最后一个示例,我无法在没有递归的情况下“正确”复制。可以做到吗?

为了完整起见,这里有一个 JavaScript 程序,它不递归地解析该树,而是使用虚构的教堂编号:

function runParser(f){return function(str){
    var a = f(str[0]);
    return a(str.slice(1));
}};
function Const(a){return function(b){return a}};
function toChurch(n){return (function(f){return (function(a){ 
    for (var i=0; i<n; ++i) 
        a  =  f(a); 
    return a; 
}) }) };
function parser(get){
    return toChurch(50)(function(rec){
        return function (res){
            return get(function(a){
                return [get(function(b){
                    return toChurch(50)(function(recl){
                        return function(res){
                            return get(function(a){
                                return [
                                    rec(function(a){
                                        return recl(function(b){
                                            return res(["Cons",a,b])
                                        })
                                    }),
                                    res("Nil")][a];
                            });
                        };
                    })(0)(function(x){return res(["Node",b,x])});
                })][a];
            });
        };
    })(0)(Const);
};
var string = [0,200,0,0,300,0,0,400,1,0,0,500,1,0,0,500,1,1,0,0,600,0,0,700,1,0,0,800,1,0,0,900,1,1,1];
console.log(JSON.stringify(parser(runParser)(string)));

请注意50 函数内的parser 常量:它是完全任意的界限。我不确定那些“完全适合”特定可解析值的选择是否有“正确”选择。

【问题讨论】:

  • 您想在没有递归或没有显式递归的情况下执行此操作吗?如果您使用折叠,那么它仍然使用递归,从技术上讲,如果您使用的是列表,那么您使用的是递归类型(在 Haskell 中)。如果您知道要从什么大小开始使用向量类型,则可以使用它,但尝试取消递归递归数据结构并不是 IMO 非常有价值的练习。
  • @bheklilr,我更新了我的问题,使我对“递归函数”的定义明确。谢谢!
  • "Total" 并不意味着你不能有递归甚至无限的东西链。在总函数中解析无限字符串是可以的——你只需要 corecursion。所有总函数必须是结构递归的(您可以找到一些技术here)或生产性的(参见例如this)。有一个 paper 关于所有解析器组合器。
  • 全部解析器组合器......看在上帝的份上,我要退出 IT 并开一辆食品卡车。会更容易。
  • 我的意思是正常的形式,因为该术语没有更多的redexes,包括内部抽象。也就是说,该术语是“强标准化”,定义为on this article。我发现这是对整体性的唯一合理定义,并且实际上包含在相应维基百科文章的定义中。有些术语可以证明是终止的,但不是强规范化的——读了一点之后,我猜那些解析器适合那个类别。 :(

标签: parsing haskell recursion functional-programming


【解决方案1】:

tl;dr:Church 对您的输入列表进行编码,并使用它来驱动您的递归。

正确的列表编码需要RankNTypes,看起来有点像这样:

{-# LANGUAGE RankNTypes #-}

data List a = List { runList :: forall r. (a -> r -> r) -> r -> r }
instance Show a => Show (List a) where
    showsPrec n (List xs) = showsPrec n (xs (:) [])

nilVal :: List a
nilVal = List $ \cons nil -> nil

consVal :: a -> List a -> List a
consVal a (List as) = List $ \cons nil -> cons a (as cons nil)

-- handy for pattern-matching
uncons :: List a -> Maybe (a, List a)
uncons (List xs) = xs cons nil where
    cons x Nothing = Just (x, nilVal)
    cons x (Just (x', xs)) = Just (x, consVal x' xs)
    nil = Nothing

现在我们只需要编写解析器。我对解析理论真的很糟糕,所以我把一些可怕的东西放在一起。可能对那个领域有一两点了解的人可以在这里给你一些更有原则的建议。我来解析一下语法:

tree -> 0 N list
list -> 0 tree list | 1

我的解析器状态将跟踪我们当前正在解析的“漏洞”。对于非终端,我们实际上需要一堆孔。因此,端子孔具有以下形式之一:

* N list
0 * list
* tree list
*

我们将折叠最后两个。请注意,这些漏洞之前都没有有趣的信息,因此我们不需要在THole 中存储任何内容。非终端孔具有以下形式之一:

0 N *
0 * list
0 tree *

在这种情况下,树形成规则中的空洞前面有一个我们稍后需要的数字,而列表形成规则中的第二种空洞前面有一棵我们需要保留的树,所以NTHole 将需要构造函数中的那些。因此:

data Tree = Node Int [Tree]
    deriving (Eq, Ord, Read, Show)

data THole
    = TreeT0
    | TreeT1
    | ListT
    deriving (Eq, Ord, Read, Show)

data NTHole
    = TreeNT Int
    | ListNT0
    | ListNT1 Tree
    deriving (Eq, Ord, Read, Show)

我们当前的解析器状态将是我们当前所处的终端漏洞,随着规则的减少,我们需要填充的非终端漏洞堆栈。

type RawState = (THole, List NTHole)
initRawState = (TreeT0, nilVal)

...好吧,除了我们还有两个感兴趣的状态:完成列表和错误。

type State = Maybe (Either RawState Tree)
initState = Just (Left initRawState)

现在我们可以编写一个阶跃函数来处理一个好的状态。同样,您可能需要一个解析器生成器工具来为您创建其中一个,但是这种语言足够小,我是手动完成的。

stepRaw :: Int -> RawState -> State
stepRaw 0 (TreeT0, xs) = Just (Left (TreeT1, xs))
stepRaw n (TreeT1, xs) = Just (Left (ListT , consVal (TreeNT n) xs))
stepRaw 0 (ListT , xs) = Just (Left (TreeT0, consVal ListNT0    xs))
stepRaw 1 (ListT , xs) = fst (runList xs cons nil) [] where
    cons v (f, xs) = flip (,) (consVal v xs) $ case v of
        ListNT1 t -> \acc -> f (t:acc)
        TreeNT  n -> \acc -> let t = Node n acc in case uncons xs of
            Nothing -> Just (Right t)
            Just (ListNT0, xs) -> Just (Left (ListT, consVal (ListNT1 t) xs))
            _ -> Nothing
        _ -> \acc -> Nothing
    nil = (\acc -> Nothing, nilVal)
stepRaw _ _ = Nothing

step :: Int -> State -> State
step n v = v >>= either (stepRaw n) (const Nothing)

事实证明,这个解析器实际上是向后运行的,这是不幸的,但不是根本的限制。我更容易朝这个方向思考。根据需要,这里没有递归。我们可以在 ghci 中的示例 List Int 上试用它。

*Main> let x = foldr consVal nilVal [1,1,40,0,0,1,30,0,0,1,20,0,0,10,0]
*Main> runList x step initState
Just (Right (Node 10 [Node 20 [],Node 30 [],Node 40 []]))

我使用foldr 构建x,而foldr 是递归的,所以你可能会为此尖叫。但是我们可以很容易地定义 x 而没有 foldr;内置列表语法比consValnilVal 的长链更方便读写。

【讨论】:

  • 你太棒了!我真的不知道该怎么感谢你了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-02
  • 2014-06-01
相关资源
最近更新 更多