关于什么是函数式程序和什么是命令式程序似乎有很多意见。
我认为函数式程序最容易被描述为面向“惰性求值”。该语言设计采用递归方法,而不是让程序计数器遍历指令。
在函数式语言中,函数的求值将从 return 语句 开始并回溯,直到最终达到一个值。这对语言语法有深远的影响。
当务之急:随身携带计算机
下面,我尝试使用邮局的类比来说明这一点。命令式语言会将计算机发送到不同的算法,然后让计算机返回结果。
功能:到处运送食谱
函数式语言将发送食谱,当您需要结果时,计算机将开始处理食谱。
这样,您可以确保不会将过多的 CPU 周期浪费在从未用于计算结果的工作上。
当您在函数式语言中调用函数时,返回值是由配方构成的配方,而配方又由配方构成。这些配方实际上就是所谓的闭包。
// helper function, to illustrate the point
function unwrap(val) {
while (typeof val === "function") val = val();
return val;
}
function inc(val) {
return function() { unwrap(val) + 1 };
}
function dec(val) {
return function() { unwrap(val) - 1 };
}
function add(val1, val2) {
return function() { unwrap(val1) + unwrap(val2) }
}
// lets "calculate" something
let thirteen = inc(inc(inc(10)))
let twentyFive = dec(add(thirteen, thirteen))
// MAGIC! The computer still has not calculated anything.
// 'thirteen' is simply a recipe that will provide us with the value 13
// lets compose a new function
let doubler = function(val) {
return add(val, val);
}
// more modern syntax, but it's the same:
let alternativeDoubler = (val) => add(val, val)
// another function
let doublerMinusOne = (val) => dec(add(val, val));
// Will this be calculating anything?
let twentyFive = doubler(thirteen)
// no, nothing has been calculated. If we need the value, we have to unwrap it:
console.log(unwrap(thirteen)); // 26
unwrap 函数将对所有函数求值,使其具有标量值。
语言设计后果
命令式语言中的一些不错的功能在函数式语言中是不可能的。例如value++ 表达式,它在函数式语言中很难计算。函数式语言对语法必须如何进行限制,因为它们的评估方式。
另一方面,命令式语言可以借鉴函数式语言的好主意并成为混合体。
函数式语言很难使用 一元运算符(例如 ++)来增加值。造成这种困难的原因并不明显,除非您了解函数式语言是“反向”评估的。
实现一元运算符必须像这样实现:
let value = 10;
function increment_operator(value) {
return function() {
unwrap(value) + 1;
}
}
value++ // would "under the hood" become value = increment_operator(value)
注意我上面使用的unwrap函数,是因为javascript不是函数式语言,所以在需要的时候我们必须手动解包。
现在很明显,应用增量一千次会导致我们用 10000 个闭包来包装这个值,这是毫无价值的。
更明显的方法是直接就地更改值 - 但是瞧:您已经引入了可修改值 aka 可变值这使得语言势在必行 - 或者实际上是一种混合语言。
在后台,当提供输入时,它归结为两种不同的方法来产生输出。
下面,我将尝试用以下项目来描绘一个城市:
- 计算机
- 你的家
- 斐波那契
命令式语言
任务:计算第三个斐波那契数。
步骤:
-
将计算机放入一个盒子并用便签标记:
| Field |
Value |
| Mail Address |
The Fibonaccis |
| Return Address |
Your Home |
| Parameters |
3 |
| Return Value |
undefined |
然后发送计算机。
-
斐波那契将在收到盒子后照常做:
-
参数是否
-
是:更换便签,然后将电脑送回邮局:
| Field |
Value |
| Mail Address |
The Fibonaccis |
| Return Address |
Your Home |
| Parameters |
3 |
| Return Value |
0 or 1 (returning the parameter)
|
并返回给发件人。
-
否则:
-
在旧的上面放一个新的便签:
| Field |
Value |
| Mail Address |
The Fibonaccis |
| Return Address |
Otherwise, step 2, c/oThe Fibonaccis
|
| Parameters |
2 (passing parameter-1)
|
| Return Value |
undefined |
然后发送。
-
取下退回的便签。将一个新的便笺放在最初的上面,然后再次发送计算机:
| Field |
Value |
| Mail Address |
The Fibonaccis |
| Return Address |
Otherwise, done, c/o The Fibonaccis
|
| Parameters |
2 (passing parameter-2)
|
| Return Value |
undefined |
-
现在,我们应该有来自请求者的初始便笺,以及两个使用过的便笺,每个都填充了它们的返回值字段。我们将返回值汇总并放在最后的便笺的返回值字段中。
| Field |
Value |
| Mail Address |
The Fibonaccis |
| Return Address |
Your Home |
| Parameters |
3 |
| Return Value |
2 (returnValue1 + returnValue2)
|
并返回给发件人。
您可以想象,在您将计算机发送到您调用的函数之后,会立即开始大量工作。
整个编程逻辑是递归的,但实际上,当计算机在一堆便笺的帮助下从一个算法移动到另一个算法时,算法是按顺序发生的。
函数式语言
任务:计算第三个斐波那契数。步骤:
-
在便签上写下以下内容:
| Field |
Value |
| Instructions |
The Fibonaccis |
| Parameters |
3 |
基本上就是这样。那个便签现在代表fib(3)的计算结果。
我们已将参数 3 附加到名为 The Fibonaccis 的配方中。计算机不必执行任何计算,除非有人需要标量值。
函数式 Javascript 示例
我一直致力于设计一种名为 Charm 的编程语言,这就是斐波那契在该语言中的外观。
fib: (n) => if (
n < 2 // test
n // when true
fib(n-1) + fib(n-2) // when false
)
print(fib(4));
此代码可以编译成命令式和函数式“字节码”。
命令式 javascript 版本是:
let fib = (n) =>
n < 2 ?
n :
fib(n-1) + fib(n-2);
半功能 javascript 版本将是:
let fib = (n) => () =>
n < 2 ?
n :
fib(n-1) + fib(n-2);
纯函数式 javascript 版本会涉及更多,因为 javascript 没有等效的函数。
let unwrap = ($) =>
typeof $ !== "function" ? $ : unwrap($());
let $if = ($test, $whenTrue, $whenFalse) => () =>
unwrap($test) ? $whenTrue : $whenFalse;
let $lessThen = (a, b) => () =>
unwrap(a) < unwrap(b);
let $add = ($value, $amount) => () =>
unwrap($value) + unwrap($amount);
let $sub = ($value, $amount) => () =>
unwrap($value) - unwrap($amount);
let $fib = ($n) => () =>
$if(
$lessThen($n, 2),
$n,
$add( $fib( $sub($n, 1) ), $fib( $sub($n, 2) ) )
);
我会手动将其“编译”成 javascript 代码:
"use strict";
// Library of functions:
/**
* Function that resolves the output of a function.
*/
let $$ = (val) => {
while (typeof val === "function") {
val = val();
}
return val;
}
/**
* Functional if
*
* The $ suffix is a convention I use to show that it is "functional"
* style, and I need to use $$() to "unwrap" the value when I need it.
*/
let if$ = (test, whenTrue, otherwise) => () =>
$$(test) ? whenTrue : otherwise;
/**
* Functional lt (less then)
*/
let lt$ = (leftSide, rightSide) => () =>
$$(leftSide) < $$(rightSide)
/**
* Functional add (+)
*/
let add$ = (leftSide, rightSide) => () =>
$$(leftSide) + $$(rightSide)
// My hand compiled Charm script:
/**
* Functional fib compiled
*/
let fib$ = (n) => if$( // fib: (n) => if(
lt$(n, 2), // n < 2
() => n, // n
() => add$(fib$(n-2), fib$(n-1)) // fib(n-1) + fib(n-2)
) // )
// This takes a microsecond or so, because nothing is calculated
console.log(fib$(30));
// When you need the value, just unwrap it with $$( fib$(30) )
console.log( $$( fib$(5) ))
// The only problem that makes this not truly functional, is that
console.log(fib$(5) === fib$(5)) // is false, while it should be true
// but that should be solveable
https://jsfiddle.net/819Lgwtz/42/