【问题标题】:Replacing functions with Table Lookups用表查找替换函数
【发布时间】:2012-11-25 19:54:53
【问题描述】:

我一直在看this MSDN video with Brian Beckman,我想更好地理解他所说的话:

每个迫切的程序员都会经历这个学习阶段 函数可以用表查找代替

现在,我是一名从未上过大学的 C# 程序员,所以也许在某个地方我错过了其他人学会理解的东西。

布赖恩是什么意思:

函数可以用表查找替换

是否有这样做的实际示例,是否适用于所有功能?他给出了 sin 函数的例子,我可以理解,但我如何更一般地理解这个?

【问题讨论】:

  • 看看 java tableSwitch 和 lookupSwitch,clojure hashmaps 是如何成为其键的函数

标签: functional-programming lookup-tables imperative-programming


【解决方案1】:

Brian 刚刚表明函数也是数据。一般来说,函数只是一个集合到另一个集合的映射:y = f(x) 是集合 {x} 到集合 {y} 的映射:f:X->Y。这些表也是映射:[x1, x2, ..., xn] -> [y1, y2, ..., yn]

如果函数在有限集上运行(编程中就是这种情况),那么它可以替换为表示该映射的表。正如 Brian 提到的,每个命令式程序员都会经历这个阶段,即出于性能原因,可以用表查找替换函数。

但这并不意味着所有功能都可以或应该轻松地替换为表格。这仅意味着您理论上可以为每个功能做到这一点。所以结论是函数是数据,因为表是(当然在编程的上下文中)。

【讨论】:

  • 谢谢@mobyte - 这让事情变得更清楚了。与往常一样,所指的内容比我想象的要简单得多:)
  • 需要注意的是,只有有限的函数才能被表替换,因为表必然是有限的,而函数往往是无限的,即有一个无限的域。然而,计算量大的函数的一种常用技术是用一个不断增长的记住值表来支持函数,这样每个输入只需计算一次结果——这称为记忆。
  • @pelotom 可以在表格中构建有限插值函数,以很好地逼近原始受害函数。这方面的一个示例是协同定位 NURB(非均匀有理 BSplines)。我在适度广义的自动插值方案方面取得了一些成功,这些方案可以即时优化连续函数。
  • “每个命令式程序员都会经历这个学习函数可以用表查找代替的阶段”。这是这些学习程序员做的纯粹的脑力练习吗,还是你真的可以编写代码来执行这样的查找,从而看到性能的提高。如果是这样,在各种编程教科书或计算机科学书籍中肯定有大量示例,不是吗?
【解决方案2】:

在 Mathematica 中有一个可爱的技巧,它创建一个表作为评估函数调用作为重写规则的副作用。考虑经典的慢斐波那契

fib[1] = 1
fib[2] = 1
fib[n_] := fib[n-1] + fib[n-2]

前两行为输入 1 和 2 创建表条目。这与说的完全相同

fibTable = {};
fibTable[1] = 1;
fibTable[2] = 1;

在 JavaScript 中。 Mathematica 的第三行说“请安装一个重写规则,在用出现的实际参数替换模式变量 n_ 之后,用 fib[n-1] + fib[n-2] 替换任何出现的 fib[n_]。”重写器将迭代此过程,并最终在指数次数的重写后产生fib[n] 的值。这就像我们在 JavaScript 中使用

获得的递归函数调用形式
function fib(n) {
  var result = fibTable[n] || ( fib(n-1) + fib(n-2) );
  return result;
}

请注意,在进行递归调用之前,它首先检查表中我们显式存储的两个值。 Mathematica 求值器自动执行此检查,因为规则的呈现顺序很重要——Mathematica 首先检查更具体的规则,然后检查更一般的规则。这就是为什么 Mathematica 有两种赋值形式,=:=:前者用于特定规则,其右侧可以在定义规则时计算;后者适用于在应用规则时必须评估右侧的一般规则。

现在,在 Mathematica 中,如果我们说

fib[4]

它被重写为

fib[3] + fib[2]

然后去

fib[2] + fib[1] + 1

然后去

1 + 1 + 1

最后是 3,它在下一次重写时不会改变。你可以想象,如果我们说fib[35],我们会产生巨大的表达式,填满内存,融化CPU。但诀窍是将最终的重写规则替换为以下内容:

fib[n_] := fib[n] = fib[n-1] + fib[n-2]

这表示“请将每个出现的fib[n_] 替换为一个表达式,该表达式将为fib[n] 的值安装新的特定规则并生成该值。”这个运行得更快,因为它扩展了规则库——值表! -- 在运行时。

我们可以在 JavaScript 中做同样的事情

function fib(n) {
  var result = fibTable[n] || ( fib(n-1) + fib(n-2) );
  fibTable[n] = result;
  return result;
}

这比之前定义的 fib 运行得快很多。

这被称为“自动记忆化”[原文如此——不是“记忆化”而是“记忆化”,就像为自己创建备忘录一样]。

当然,在现实世界中,您必须管理所创建表的大小。要检查 Mathematica 中的表格,请执行

DownValues[fib]

要在 JavaScript 中检查它们,只需这样做

fibTable

在诸如 Node.JS 支持的 REPL 中。

【讨论】:

  • 顺便说一句,这是 Brian Beckman:“rebcabin”是“brianbec”的变位词,是我常用的 nom-de-internet。
  • 对我来说,您所说的似乎是缓存
【解决方案3】:

在函数式编程的上下文中,存在引用透明性的概念。对于任何给定的参数(或参数集),可以将引用透明的函数替换为其值,而不会改变程序的行为。

Referential Transparency

例如,考虑一个函数 F,它接受 1 个参数 n。 F 是引用透明的,因此 F(n) 可以替换为在 n 处评估的 F 的值。对程序没有影响。

在 C# 中,这看起来像:

public class Square
{
    public static int apply(int n)
    {
        return n * n;
    }

    public static void Main()
    {
        //Should print 4
        Console.WriteLine(Square.apply(2));
    }
}

(我对 C# 不是很熟悉,来自 Java 背景,所以如果这个示例在语法上不太正确,请您原谅我)。

这里很明显,函数 apply 在使用参数 2 调用时不能有任何其他值,因为它只是返回其参数的平方。 only 函数的值取决于它的参数,n;换句话说,参照透明性。

那么,我问你,Console.WriteLine(Square.apply(2))Console.WriteLine(4) 之间有什么区别。答案是,根本没有区别,因为所有意图都是目的。我们可以遍历整个程序,将Square.apply(n) 的所有实例替换为Square.apply(n) 返回的值,结果将完全相同。

那么,Brian Beckman 关于用表查找替换函数调用的声明是什么意思?他指的是引用透明函数的这种属性。如果Square.apply(2) 可以替换为4 而对程序行为没有影响,那么为什么不在第一次调用时缓存值,并将其放入由函数参数索引的表中。 Square.apply(n) 值的查找表看起来有点像这样:

              n: 0 1 2 3 4  5  ...
Square.apply(n): 0 1 4 9 16 25 ...

并且对于任何对Square.apply(n)的调用,我们可以简单地在表中找到n的缓存值,而不是调用函数,并将函数调用替换为该值。很明显,这很可能会大大提高程序的速度。

【讨论】:

    猜你喜欢
    • 2018-10-19
    • 2021-08-09
    • 1970-01-01
    • 2014-04-07
    • 1970-01-01
    • 2016-02-05
    • 2015-08-17
    • 1970-01-01
    • 2013-12-07
    相关资源
    最近更新 更多