1

函数式语言中列表反转函数最直接的定义可能是(使用类似 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]

我对这种东西的应用范围不太了解,但至少在这个例子中它似乎工作得很好,在我看来,它至少可以在一些类似的情况下工作否则为了提高效率就不得不引入辅助功能。任何人都可以向我指出任何讨论此类内容或支持此类内容的语言的论文吗?对我来说,这有点像逻辑编程,但我对逻辑编程的经验很少。

4

3 回答 3

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程序的其余部分不需要看到 for 的规则。

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每次调用 inside rev2,我们每次都引入一个新的[],并且只在[]. m总是[]。我们需要引用rev2inrev2以便规则可以直接应用于它自己的输出。当我们这样做时,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)
于 2014-08-18T22:11:47.640 回答
1

Prolog 中的reverse函数确实会有两个参数,其中一个是累加器。列表上的逻辑程序总是会表现出这样的:在列表append/3的末尾附加一些东西,第三个“参数”是结果列表。

但是,Prolog 中的有效反向谓词将具有三个参数。见这里:

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"),并且y会是bar.

但事实证明,Haskell 程序员有时并不介意增加的间接层。只需使用where关键字,这样您就可以使用更少的顶级函数。甚至还有一种新出现的约定,即调用从属递归函数go。或者,正如 AndrewC 所写,您也可以使用折叠来代替。

于 2014-08-18T19:54:55.043 回答
1

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

塞尔吉奥·安托伊和迈克尔·哈努斯。功能逻辑编程。ACM 通讯,第 53 卷,第 4 期(2010 年 4 月),第 74-85 页。

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

例如,从定义last该规则的规则中可以明显看出,该规则仅在实际参数的形式与 narrowing 的结果匹配时才适用zs++[e]。因此,我们可以将该规则重新表述为:

last (zs++[e]) = e

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

本文使用Curry编程语言。

于 2014-08-18T20:57:39.487 回答