为什么函数式编程语言支持自动记忆而不是命令式语言?

时间:2016-09-07 07:39:56

标签: java functional-programming lisp theory imperative-programming

这是我在互联网上随机找到的关于动态编程的一些讲座中读到的一个问题。 (我毕业了,我已经知道了动态编程的基础)

在解释为什么需要记忆的部分,即

// psuedo code 
int F[100000] = {0};
int fibonacci(int x){
    if(x <= 1) return x;
    if(F[x]>0) return F[x];
    return F[x] = fibonacci(x-1) + fibonacci(x-2);
}

如果没有使用memoization,那么许多子问题将被重新计算很多次,这使得复杂性非常高。

然后在一个页面上,这些笔记有一个没有答案的问题,这正是我想问的问题。在这里,我使用的是准确的措辞及其显示的示例:

  

自动记忆:许多函数式编程语言(例如Lisp)都内置了对memoization的支持。

     

为什么不用命令式语言(例如Java)?

该说明提供的LISP示例(声称它有效):

(defun F (n)
    (if
        (<= n 1)
        n
        (+ (F (- n 1)) (F (- n 2)))))

它提供的Java示例(声称它是指数级的)

static int F(int n) {
  if (n <= 1) return n;
  else return F(n-1) + F(n-2);
}

在阅读本文之前,我甚至不知道在某些编程语言中内置了对memoization的支持。

说明中的声明是真的吗?如果是,那么为什么命令式语言不支持它?

3 个答案:

答案 0 :(得分:5)

关于&#34; LISP&#34;非常模糊,他们甚至没有提到他们所说的 LISP方言或实施。我熟悉的LISP方言都没有自动记忆,但是LISP可以很容易地编写一个包装函数,将任何现有函数转换为记忆函数。

全自动,无条件的记忆将是一种非常危险的做法,并会导致内存不足错误。在命令式语言中,它会更糟糕,因为返回值通常是可变的,因此不可重复使用。命令式语言通常不支持尾递归优化,进一步降低了memoization的适用性。

答案 1 :(得分:3)

对memoization的支持只不过是拥有一流的功能。

如果你想为一个特定的情况记住Java版本,你可以明确地写它:创建一个哈希表,检查现有的值等。不幸的是,你不能轻易地概括它以便记住任何函数。具有一流功能的语言使编写功能和记忆几乎正交的问题。

基本情况很简单,但您必须考虑递归调用。 在像OCaml这样的静态类型函数式语言中,一个被记忆的函数不能只是递归地调用它自己,因为它会调用非memoized版本。但是,对现有函数的唯一更改是接受一个函数作为参数,例如以self命名,只要函数想要递归,就应该调用它。然后,通用记忆设施提供适当的功能。 this answer中提供了完整的示例。

Lisp版本有两个功能,使现有功能的记忆更加简单。

  1. 您可以像其他任何值一样操作函数
  2. 您可以在运行时重新定义功能
  3. 例如,在Common Lisp中,您定义F

    (defun F (n)
      (if (<= n 1)
          n
          (+ (F (- n 1))
             (F (- n 2)))))
    

    然后,您看到需要记住该函数,因此您加载了一个库:

    (ql:quickload :memoize) 
    

    ...并且你会记住F

    (org.tfeb.hax.memoize:memoize-function 'F)
    

    工具接受参数以指定应缓存哪个输入以及使用哪个测试函数。然后,函数F被一个新函数替换,它引入了使用内部哈希表的必要代码。对FF的递归调用现在调用包装函数,而不是原始函数(您甚至不重新编译F)。唯一可能的问题是原始F是否受到尾调优化的影响。您应该声明它notinline或使用DEF-MEMOIZED-FUNCTION

答案 2 :(得分:1)

虽然我不确定任何广泛使用的Lisps是否支持自动 memoization,但我认为有两个原因可以解释为什么memoization在函数式语言中更常见,另外一个用于Lisp家族语言

首先,人们用函数式语言编写函数:计算的结果仅取决于它们的参数,并且不会对环境产生副作用。任何不符合要求的东西都不适合记忆。而且,命令式语言只是那些不满足或不满足这些要求的语言,因为它们本身并不是必要的!

当然,即使只是像(大多数)Lisps这样的功能友好的语言,你必须要小心:你可能不应该记住以下内容,例如:

(defvar *p* 1)

(defun foo (n)
  (if (<= n 0)
      *p*
    (+ (foo (1- n)) (foo (- n *p*)))))

其次,函数式语言通常希望讨论不可变数据结构。这意味着两件事:

  1. 记住返回大型数据结构的函数实际上是安全的
  2. 构建非常大的数据结构的函数通常需要消耗大量的垃圾,因为它们不能改变临时结构。
  3. (2)稍有争议:收到的智慧是,GC现在非常好,它不是问题,复制非常便宜,编译器可以做魔术等等。那么,编写这些函数的人会知道这只是部分正确:GC 好,复制 便宜(但指针追逐大型结构来复制它们通常是对缓存非常不友好),但实际上并不够(编译器几乎从不做他们声称要做的魔术)。所以你要么通过无偿地使用非功能性代码作弊,要么你会记忆。如果您记住该函数,那么您只需构建一次所有临时结构,并且一切都变得便宜(除了在内存中,但在memoization中适当的弱点可以处理)。

    第三:如果你的语言不支持简单的金属语言抽象,那么实现memoization就会非常痛苦。或者换句话说:你需要Lisp风格的宏。

    要记住一个功能,至少需要做两件事:

    1. 您需要控制哪些参数是记忆的关键 - 并非所有函数都只有一个参数,并且并非所有具有多个参数的函数都应该在第一个上记忆;
    2. 你需要在函数内部进行干预,以禁用任何自我尾调用优化,这将彻底破坏memoization。
    3. 虽然这样做很残忍,因为它很容易,但我会通过嘲笑Python来证明这一点。

      您可能认为装饰器是您在Python中记忆函数所需的。事实上,你可以使用装饰器编写备忘录工具(我已经写了很多)。而这些甚至是各种各样的工作,尽管他们大多是偶然的。

      首先,装饰者不能轻易了解它正在装饰的功能。所以你最终要么根据函数的所有参数的元组进行memoize,要么在装饰器中指定要记忆的参数,或者同样粗糙的东西。

      其次,装饰器将它正在装饰的函数作为一个参数获取:它不会在它内部徘徊。这实际上没问题,因为Python作为其“1956年以后发明的概念”政策的一部分,当然不会假设在f的定义内词汇调用f(并且没有干预绑定事实上是自我呼唤。但也许有一天它会发生,你的所有记忆现在都会破裂。

      总结:要强有力地记忆函数,你需要Lisp风格的宏。可能只有那些具有Lisps的命令式语言。