隐式定义和尾递归

时间:2014-08-16 22:53:53

标签: haskell functional-programming logic-programming

函数式语言中列表反转函数最直接的定义可能是(使用类似Haskell的伪代码)

rev [] = []
rev (x:xs) = (rev xs) ++ [x]

然而,每个初学的函数程序员都被告知这个实现效率低下,应该编写

rev' [] acc = acc
rev' (x:xs) acc = rev' xs (x:acc)
rev l = rev' l []

高效版本的一个坏处是程序员被迫引入一个辅助功能和参数,其含义不是很清楚。在我看来,如果一种语言允许隐式定义大致如下所示,则可能会避免这种情况:

rev [] = []
(rev (x:xs)) ++ m = (rev xs) ++ (x:m)

这些方程完全确定了rev的行为,因此可以说它们构成了它的隐含定义。它们没有引入辅助函数rev'的缺点。然而,有一种自然的方法来评估有效的功能。例如,这是一个看似合理的减少序列:

rev [1,2,3]
matches second line with x=1, xs=[2,3], m=[]
reduces to (rev [2,3]) ++ [1]
matches second line with x=2, xs=[3], m=[1]
reduces to (rev [3]) ++ [2,1]
matches second line with x=3, xs=[], m=[2,1]
reduces to (rev []) ++ [3,2,1]
reduces ultimately to [3,2,1]

我对这种事物的应用范围有多少没有多大意义,但它至少在这个例子中看起来确实很好用,而且在我看来它至少可以用于某些相似的事情。否则为了效率而必须引入辅助功能的情况。任何人都可以指出任何讨论此类内容或支持此类内容的语言的论文吗?这对我来说有点像逻辑编程,但我对逻辑编程的经验很少。

3 个答案:

答案 0 :(得分:2)

术语重写编程语言允许像这样编写规则。术语重写语言将一组重写规则与应用它们的策略相结合。让我们尝试按照你在Pure中的建议实现反向,这是一个相当简单易用的术语重写系统。

我们的第一次尝试将尝试按如下方式撤销列表:

rev [] = [];
(rev (x:xs)) + m = (rev xs) + (x:m)

我们会尝试一些示例查询,反转空列表[],单个列表[1]以及包含4个元素[1,2,3,4]的列表。我们希望输出分别为[][1][4,3,2,1]

> rev [];
[]
> rev [1];
rev [1]
> rev [1,2,3,4];
rev [1,2,3,4]

我们的第一条规则奏效了,但第二条规则从未适用过。 Pure有一个内置规则,用于将列表连接在一起,可能类似于:

xs     + [] = xs; // Pure's prelude doesn't actually even include this.
[]     + ys = ys;
(x:xs) + ys = x:(xs + ys);

但是它的重写策略并没有探索如果每个步骤都被颠倒会发生什么。为此,对于每个术语xs,都需要考虑将xs + []一词重写为xs以外的其他内容!相反,我们会告诉重写系统,当用rev重写时,考虑反向列表并附加一个空列表是有用的。

rev [] = [];
(rev (x:xs)) + m = (rev xs) + (x:m);
rev (x:xs) = (rev (x:xs)) + [];

即使是单个项目列表也会打击堆栈。事实证明,我们的第三条规则会一直应用,直到堆栈溢出,而第二条规则不会停止它。

> rev [1];
<stdin>, line 2: unhandled exception 'stack_fault' while evaluating 'rev [1]'

我们需要更多地控制评估策略。通过引入新符号rev2,我们可以阻止第三条规则进行匹配。这些规则与以前相同,除了rev2的规则不需要被其他程序看到。

rev [] = [];
rev (x:xs) = (rev2 (x:xs)) + [] with
    (rev2 (x:xs)) + m = (rev xs) + (x:m);
end;

这项工作正常,但我们并不评估。

> rev [];
[]
> rev [1];
[]+[1]
> rev [1,2,3,4];
[]+[4]+[3]+[2]+[1]

更糟糕的是,+是关联的,所以这仍然有令人讨厌的n^2运行时间。这是因为,每次在rev内拨打rev2时,我们每次都会引入一个新的[],并且仅在[]之前。 m始终为[]。我们需要在rev2中引用rev2,以便规则可以直接应用于自己的输出。当我们这样做时,rev2将需要自己的规则来处理空列表,我们开始以一种不愉快的方式重复自己。

rev [] = [];
rev (x:xs) = (rev2 (x:xs)) + [] with
    rev2 [] = [];
    (rev2 (x:xs)) + m = (rev2 xs) + (x:m);
end;

我们现在得到的几乎正是我们想要的:

> rev [];
[]
> rev [1];
[]+[1]
> rev [1,2,3,4];
[]+[4,3,2,1]

我们可以通过只为[]的空列表设置一个规则来清除空列表和额外rev2串联的规则的重复。

rev xs = (rev2 xs) + [] with
    (rev2 []    ) + m = m;
    (rev2 (x:xs)) + m = (rev2 xs) + (x:m);
end;

这非常有效:

> rev [];
[]
> rev [1];
[1]
> rev [1,2,3,4];
[4,3,2,1]

现在,我们可以更进一步,稍微清理一下我们的代码。由于涉及rev2的所有内容都具有模式(rev2 a) + b,并且只有符号很重要,因此我们可以使用更简单的表单rev2 a b替换该表单的所有内容。

rev xs = rev2 xs [] with
    rev2 []     m = m;
    rev2 (x:xs) m = rev2 xs (x:m);
end;

这与您首先尝试避免的Haskell定义完全相同

rev xs = rev' xs [] where
    rev' []     m = m
    rev' (x:xs) m = rev' xs (x:m)

答案 1 :(得分:1)

Prolog中的reverse函数确实有两个参数,其中一个是累加器。列表上的逻辑程序将始终显示:append/3将某些内容附加到列表的末尾,第三个“参数”是结果列表。

但是,Prolog中的高效反向谓词有三个参数。见here:

revappend([], Ys, Ys).
revappend([X|Xs], Ys, Zs) :- revappend(Xs, [X|Ys], Zs).
reverse(Xs,Ys) :- revappend(Xs,[],Ys).

非常类似于Haskell中的相同问题 - 天真的Prolog版本实际上会调用append/3,这很糟糕 - 它对应于Haskell的++

在我看来,您的提议只允许使用可选参数的语法。所以函数实际上被定义为二进制函数,但是你希望能够将它作为一元函数调用,第二个参数实例化为默认值(空列表)。在我看来,它与Python非常相似(例如,foo(x,y="bar")的功能主管允许您拨打foo("moo")ybar

但事实证明,Haskell程序员有时并不介意增加间接层。只需使用where关键字,即可获得较少的顶级函数。甚至还有一种新兴的约定,其中从属递归函数被称为go。或者,正如AndrewC所写,你也可以使用折叠。

答案 2 :(得分:1)

功能逻辑编程语言允许您使用在其他函数的应用上进行模式匹配的等式来定义函数。介绍性文章如下:

Sergio Antoy和Michael Hanus。功能逻辑编程。 ACM通讯,第53卷,第4期(2010年4月),第74-85页。

以下是该文章的相关摘录:

  

例如,从定义last的规则可以明显看出,只有当实际参数的表格与缩小zs++[e]的结果相匹配时,此规则才适用。因此,我们可以将该规则重新制定为:

  last (zs++[e]) = e

  请注意,纯函数式语言(如Haskell)不允许使用此规则,因为它不是基于构造函数的;相反,它包含一个功能模式,即内部具有已定义函数的模式。

本文使用Curry编程语言。