我知道选择编程方式取决于用户需求,但为什么建议学习函数式编程语言?
以下是区别:
命令式:
... 等等 ...
声明式, 其中函数式是其子类别:
... 等等 ...
总结: 在命令式语言中,您告诉计算机如何按顺序更改位、字节和单词的内存。在函数式语言中,我们告诉计算机是什么事物、动作等。例如,我们说0的阶乘为1,每个其他自然数的阶乘是该数与其前一个数的阶乘的乘积。我们不会说:要计算n的阶乘,请预留一个存储区域并将1存储在其中,然后将该存储区域中的数字与2到n的数字相乘,并将结果存储在同一位置,最终,存储区域将包含阶乘。
定义: 命令式语言使用一系列语句来确定如何达到特定目标。这些语句被称为在依次执行时改变程序状态。
例子: Java是一种命令式语言。例如,可以创建一个程序来添加一系列数字:
int total = 0;
int number1 = 5;
int number2 = 10;
int number3 = 15;
total = number1 + number2 + number3;
每个语句都会改变程序的状态,从为每个变量分配值到最终将这些值相加。使用五个语句的序列,程序明确告诉如何将数字5、10和15相加。
函数式编程: 函数式编程范式是专门为支持纯函数式方法而创建的。函数式编程是一种声明式编程形式。
纯函数的优点: 实现函数式转换为纯函数的主要原因是纯函数是可组合的:即自包含且无状态。这些特性带来了许多好处,包括以下几点: 增加可读性和可维护性。这是因为每个函数都旨在根据其参数完成特定任务。该函数不依赖于任何外部状态。
更容易进行反复开发。因为代码更容易重构,所以设计更改通常更容易实现。例如,假设您编写了一个复杂的转换,然后意识到某些代码在转换中重复了几次。如果通过纯方法进行重构,则可以随意调用纯方法,而不必担心副作用。
更容易进行测试和调试。因为纯函数更容易被孤立地测试,所以您可以编写测试代码,使用典型值、有效边缘情况和无效边缘情况调用纯函数。
针对面向对象编程(OOP)或命令式语言:
当你需要对事物进行固定的一组操作并且你的代码在演变中主要是添加新的事物时,面向对象的语言非常适用。这可以通过添加实现现有方法的新类来实现,而现有的类则保持不变。
当你需要对一组固定的事物进行操作并且你的代码在演变中主要是添加新的操作时,函数式语言非常适用。这可以通过添加用于现有数据类型计算的新函数来实现,而现有的函数则保持不变。
缺点:
选择编程方式取决于用户需求,所以只有当用户没有选择适当的方式时才会存在问题。
当演变方向错误时,你会遇到问题:
大多数现代编程语言在不同程度上都是命令式和函数式的,但为了更好地理解函数式编程,最好以纯函数式语言Haskell为例,对比一下不太函数式的语言如Java/C#的命令式代码。我认为通过示例来说明总是更容易的,因此以下是一个示例。
函数式编程:计算n的阶乘,即n!,即n x (n-1) x (n-2) x ...x 2 X 1
-- | Haskell comment goes like
-- | below 2 lines is code to calculate factorial and 3rd is it's execution
factorial 0 = 1
factorial n = n * factorial (n - 1)
factorial 3
-- | for brevity let's call factorial as f; And x => y shows order execution left to right
-- | above executes as := f(3) as 3 x f(2) => f(2) as 2 x f(1) => f(1) as 1 x f(0) => f(0) as 1
-- | 3 x (2 x (1 x (1)) = 6
请注意,Haskel允许函数重载到参数值的级别。现在以下是按照增加不可变性的顺序排列的命令式代码示例:
//somewhat functional way
function factorial(n) {
if(n < 1) {
return 1;
}
return n * factorial(n-1);
}
factorial(3);
//somewhat more imperative way
function imperativeFactor(n) {
int f = 1;
for(int i = 1; i <= n; i++) {
f = f * i;
}
return f;
}
这篇文章可以作为一个很好的参考,以了解命令式代码更加关注“如何”部分、机器状态(例如for循环中的i)、执行顺序和流程控制。
后面的例子可以大致看作是Java/C#语言代码,而前面部分则是与Haskell相比语言本身的限制——通过值(零)重载函数,因此不能说它是纯函数式编程语言。另一方面,可以说它在某种程度上支持函数式编程。
声明:以上代码均未经测试/执行,但希望足以传达概念。同时,欢迎任何纠错评论 :)
return n * factorial(n-1);
吧? - jinaween * (n-1)
进行了更正。 - old-monk函数式编程 是一种声明式编程,它描述计算的逻辑和执行顺序完全不强调。
问题:我想把这个生物从马变成长颈鹿。
每个步骤可以以任意顺序运行以产生相同的结果。
命令式编程 是一种过程化的编程方式。状态和顺序很重要。
问题:我想停车。
每个步骤必须按顺序完成以达到所需的结果。在车库门关闭时将车开进车库会导致破损的车库门。
(x - 1) * 3
与(x * 3) - 1
不同。另一个例子是,fillBasket(emptyBasket(b), items)
与emptyBasket(fillBasket(b, items))
有不同的效果。 - Mulan//The IMPERATIVE way
int a = ...
int b = ...
int c = 0; //1. there is mutable data
c = a+b; //2. statements (our +, our =) are used to update existing data (variable c)
一段命令式程序 = 一系列改变现有数据的语句。
关注于“什么” = 我们可变的数据(可修改的值,也称为变量)。
要链接命令式语句 = 使用过程(和/或面向对象编程)。
//The FUNCTIONAL way
const int a = ... //data is always immutable
const int b = ... //data is always immutable
//1. declare pure functions; we use statements to create "new" data (the result of our +), but nothing is ever "changed"
int add(x, y)
{
return x+y; //only depends on inputs; only impact on caller: via "return" value
}
//2. usage = call functions to get new data
const int c = add(a,b); //c can only be assigned (=) once (const)
add
。这些基本区别具有深远的影响。
严肃的软件有很多数据和很多代码。
因此,同样的数据(变量)在代码的多个部分中被使用。
A. 在命令式程序中,这种(共享)数据的可变性会引起问题
作为优点:数据真正地在原地修改,需要复制的需求较少。(一些性能收益)
B. 另一方面,函数式代码使用不可变数据,没有这样的问题。数据是只读的,因此没有竞争条件。代码可以轻松并行化。结果可以被缓存。更容易理解。
作为缺点:数据经常被复制以获得“修改”。
另请参阅:https://en.wikipedia.org/wiki/Referential_transparency
更新:在命令式代码中,状态突变可能随时发生。因此,您的逻辑与突变状态交织在一起,这使得该状态实际上影响了您的...逻辑,因此代码变得非常难以理解。远程、非本地状态可能会以意想不到的方式影响函数。
在函数式方法中,只有局部状态才重要,只在函数内部。在外部,您可以安全地重用函数,知道它将始终以相同的方式运行,无需外部上下文即可确定其功能。组合这样的函数成为一个非常强大的工具,只要函数是纯的,就可以获得复杂但可靠的代码。
textInput =“”
,一个单一的变量。“abcd”
因此,我们刚刚更新了我们的文本和textInput更新,规则2不适用,规则3表示如果textInput已更新(刚刚发生),那么重新呈现模板,然后我们回到规则2,这表明如果模板被呈现,则计算小部件,好的让我们计算小部件。我还相信,最终,功能代码将被编译器翻译成汇编或机器代码,这是命令式/过程式的。但是,除非您编写汇编语言,作为使用高级/人类可读语言编写代码的人,函数式编程是表达所列场景的更合适方式。
关于函数式程序和命令式程序有很多不同的观点。
我认为,函数式程序最容易描述为“惰性评估”导向。语言通过递归方法而非程序计数器迭代指令来设计实现。
在函数式语言中,函数的求值从返回语句开始回溯,直到最终得出一个值。这对于语言语法产生了深远的影响。
命令式:将计算机运送
下面,我用邮局类比试图说明这一点。命令式语言会将计算机发送到不同的算法中,然后将计算机带回结果。
函数式:运输食谱
函数式语言将发送食谱,在需要结果时,计算机将开始处理食谱。这样,您可以确保不浪费太多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 不是一种函数式语言,所以必须手动解包值。正如你所想象的那样,在你将计算机发送到调用函数后,立即开始了相当多的工作。
整个编程逻辑是递归的,但实际上,随着计算机从算法到算法的移动,算法是按顺序进行的,借助于一堆“便笺”栈。
任务:计算第三个斐波那契数。步骤:
在便笺上写下以下内容:
字段 | 值 |
---|---|
指令 | The Fibonaccis |
参数 | 3 |
这就是基本上的内容。现在,该便笺代表了fib(3)
的计算结果。
我们已经将参数3附加到名为The Fibonaccis
的配方中。除非有人需要标量值,否则计算机不必执行任何计算。
我一直在设计一种名为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);
HALF的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
我知道这个问题已经很老了,其他人已经解释得很好了,但我想举一个例子来简单说明同样的问题。
问题:写出1的乘法表。
解决方案:-
通过命令式风格:=>
1*1=1
1*2=2
1*3=3
.
.
.
1*n=n
通过函数式编程风格: =>
1
2
3
.
.
.
n
在命令式风格中,我们会更明确地编写指示,以简化指令。
而在函数式风格中,那些已经不言自明的事情将被忽略。