【发布时间】: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 表示Node 或Cons 单元格的开始(取决于解析器的状态),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,我更新了我的问题,使我对“递归函数”的定义明确。谢谢!
-
全部解析器组合器......看在上帝的份上,我要退出 IT 并开一辆食品卡车。会更容易。
-
我的意思是正常的形式,因为该术语没有更多的redexes,包括内部抽象。也就是说,该术语是“强标准化”,定义为on this article。我发现这是对整体性的唯一合理定义,并且实际上包含在相应维基百科文章的定义中。有些术语可以证明是终止的,但不是强规范化的——读了一点之后,我猜那些解析器适合那个类别。 :(
标签: parsing haskell recursion functional-programming