Functional programming 用表查找替换函数

Functional programming 用表查找替换函数,functional-programming,lookup-tables,imperative-programming,Functional Programming,Lookup Tables,Imperative Programming,我一直在看,我想更好地理解他说的话: 每一个不切实际的程序员都要经历这个学习阶段 函数可以替换为表查找 现在,我是一名C#程序员,从未上过大学,所以也许在这一过程中,我错过了其他人都能理解的东西 布赖恩所说的: 函数可以替换为表查找 是否有这样做的实际例子,它是否适用于所有功能?他给出了sin函数的例子,我可以理解它,但是我如何从更一般的角度理解它呢?Brian刚刚证明函数也是数据。函数通常只是一个集合到另一个集合的映射:y=f(x)是集合{x}到集合{y}:f:x->y的映射。这些表也是映射:

我一直在看,我想更好地理解他说的话:


每一个不切实际的程序员都要经历这个学习阶段 函数可以替换为表查找

现在,我是一名C#程序员,从未上过大学,所以也许在这一过程中,我错过了其他人都能理解的东西

布赖恩所说的:

函数可以替换为表查找


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

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

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


但这并不意味着所有函数都可以或应该很容易地用表替换。这只意味着理论上你可以对每个函数都这样做。因此,结论是函数是数据,因为表是数据(当然在编程环境中)。

在函数编程环境中,有引用透明的概念。引用透明的函数可以替换为任何给定参数(或参数集)的值,而不改变程序的行为

例如,考虑函数<强> f,需要1个参数,<强> n<强>。F是参考透明的,因此F(n)可以替换为Fn处计算的值。这对程序没有影响

在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背景,因此如果这个示例在语法上不太正确,您必须原谅我)

这里很明显,当使用参数2调用函数时,apply不能有4以外的值,因为它只是返回参数的平方。函数的值仅取决于它的参数,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的缓存值,并用该值替换函数调用即可。很明显,这很可能会使程序的速度大大提高。

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_U]
”重写器将迭代这个过程,经过指数级的重写之后,最终生成
fib[n]
的值。这就像我们在JavaScript中使用的递归函数调用形式一样

function fib(n) {
  var result = fibTable[n] || ( fib(n-1) + fib(n-2) );
  return result;
}
注意,在进行递归调用之前,它首先检查表中显式存储的两个值。Mathematica evaluator自动执行此检查,因为规则的表示顺序很重要——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;
}
这比p运行得快得多
DownValues[fib]
fibTable