大多数主流语言,包括面向对象编程(OOP)语言,如C#,Visual Basic,C ++和Java,主要用于支持命令式(程序性)编程,而Haskell / gofer类似语言则纯粹是功能性的。任何人都可以详细说明这两种编程方式之间的区别吗?
我知道这取决于用户要求选择编程方式,但为什么建议学习函数式编程语言?
答案 0 :(得分:195)
区别在于:
势在必行:
......依此类推......
声明,其中functional是子类别:
......等等......
总结:在命令式语言中,您告诉计算机如何更改其内存中的位,字节和单词以及顺序。在功能性方面,我们告诉计算机什么是事物,行为等。例如,我们说阶乘为0是1,而每个其他自然数的阶乘是该数字与其前身的阶乘的乘积。我们不说:要计算n的阶乘,保留一个存储区并在那里存储1,然后将该存储区中的数字乘以数字2到n,并将结果存储在同一个地方,最后,内存区域将包含阶乘。
答案 1 :(得分:129)
<强>定义:强> 命令式语言使用一系列语句来确定如何达到某个目标。据说这些语句会改变程序的状态,因为每个程序依次执行。
<强>示例:强> Java是一种命令式语言。例如,可以创建一个程序来添加一系列数字:
int total = 0;
int number1 = 5;
int number2 = 10;
int number3 = 15;
total = number1 + number2 + number3;
每个语句都会更改程序的状态,从为每个变量赋值到最终添加这些值。使用五个语句的序列,程序被明确告知如何将数字5,10和15加在一起。
功能语言: 功能编程范例是明确创建的,以支持解决问题的纯函数方法。函数式编程是一种声明式编程。
纯函数的优点: 将函数转换实现为纯函数的主要原因是纯函数是可组合的:即自包含和无状态。这些特征带来许多好处,包括以下内容: 提高可读性和可维护性。这是因为每个函数都是为了在给定参数的情况下完成特定任务而设计的。该功能不依赖于任何外部状态。
更容易重复开发。由于代码更容易重构,因此设计更改通常更容易实现。例如,假设您编写了一个复杂的转换,然后意识到某些代码在转换中会重复多次。如果您通过纯方法重构,您可以随意调用纯方法而不必担心副作用。
更容易测试和调试。因为可以更容易地单独测试纯函数,所以可以编写使用典型值,有效边缘情况和无效边缘情况调用纯函数的测试代码。
对于OOP人或 命令式语言:
当你对事物进行固定的操作时,面向对象的语言是很好的,随着代码的发展,你主要添加新东西。这可以通过添加实现现有方法的新类来实现,并且现有的类是独立的。
当你有一套固定的东西时,功能语言很好,随着代码的发展,你主要在现有的东西上添加新的操作。这可以通过添加使用现有数据类型计算的新函数来实现,并且现有函数不受影响。
缺点:
这取决于用户选择编程方式的要求,因此只有当用户没有选择正确的方式时才会有害。
当进化走向错误时,你就会遇到问题:
答案 2 :(得分:9)
大多数现代语言在不同程度上都具有命令式和函数式的功能,但为了更好地理解函数式编程,最好以像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;
}
此read可以很好地理解命令式代码如何更多地关注零件,机器状态(即for循环),执行顺序和流程控制。
后面的示例可以大致看作是Java / c#lang代码,而第一部分可以看作是语言本身的局限性,与Haskell会将函数按值(零)重载形成对比,因此可以说这不是纯粹的函数式语言,另一方面,您可以说它支持功能编。在某种程度上。
公开:以上代码均未经过测试/执行,但希望能够足以传达这一概念;对于任何此类更正的评论,我也将不胜感激:)
答案 3 :(得分:8)
函数式编程是一种声明式编程,描述了计算逻辑,并且完全取消了执行顺序。
问题:我想将此生物从马匹变成长颈鹿。
每个项目都可以以任何顺序运行以产生相同的结果。
命令式编程是程序性的。状态和秩序很重要。
问题:我想停车。
必须完成每个步骤才能获得所需的结果。在车库门关闭时将其拉入车库会导致车库门损坏。
答案 4 :(得分:4)
函数式编程是“使用函数编程”,其中函数具有一些预期的数学属性,包括引用透明性。从这些属性中,进一步的属性流动,特别是通过可替代性实现的熟悉的推理步骤导致数学证明(即证明结果的置信度)。
因此,功能程序仅仅是一种表达。
通过在命令式程序中注意表达式不再具有透明性(因此不是使用函数和值构建,并且本身不能成为函数的一部分),您可以轻松地看到两种样式之间的对比。最明显的两个地方是: 突变(例如变量) 其他副作用 非本地控制流程(例如例外)
在这个由函数和值组成的程序 - 表达式框架上,构建了语言,概念,“功能模式”,组合器以及各种类型系统和评估算法的完整实践范例。
通过最极端的定义,几乎任何语言 - 甚至C或Java - 都可以被称为功能性的,但通常人们会为具有特定相关抽象的语言保留术语(例如闭包,不可变值和模式匹配等语法辅助) 。 就函数式编程的使用而言,它涉及使用functins并构建代码而没有任何副作用。 用来写证明
答案 5 :(得分:3)
•命令式语言:
高效执行
复杂语义
复杂语法
并发是程序员设计的
复杂的测试,没有参照透明性,有副作用
•功能语言:
简单语义
简单语法
执行效率较低
程序可以自动并发
简单的测试,具有参照透明性,没有副作用
答案 6 :(得分:1)
从2005年到2013年,Web开发中一直沿用了一种编程式的编程风格。
通过命令式编程,我们逐步编写了列出应用程序应该执行的操作的代码。
函数式编程风格通过巧妙地组合函数来产生抽象。
答案中提到了声明式编程,因此我将说声明式编程列出了我们要遵循的一些规则。然后,我们向应用程序提供所谓的某些初始状态,然后让这些规则定义应用程序的行为。
现在,这些快速的描述可能没有多大意义,因此让我们通过类推来逐步了解命令式和声明式编程之间的区别。
想象一下,我们不是在构建软件,而是在烤馅饼。也许我们是个糟糕的面包师,不知道该怎么烤美味的馅饼。
所以我们的老板给了我们一个方向清单,我们称之为食谱。
食谱将告诉我们如何做馅饼。一种配方以命令式的方式编写,如下所示:
说明性食谱将执行以下操作:
1杯面粉,1个鸡蛋,1杯糖-初始状态
规则
因此,命令式方法的特征在于逐步方法。您从第一步开始,转到第二步,依此类推。
您最终会得到一些最终产品。因此,制作这块馅饼时,我们将这些配料混合在一起,放入锅中和烤箱中,即可得到最终产品。
在声明性世界中,它与众不同。在声明性食谱中,我们将我们的食谱分为两个独立的部分,首先是列出食谱初始状态的部分,例如变量。因此,这里的变量是成分的数量及其类型。
我们采用初始状态或初始成分,并对它们应用一些规则。
因此,我们采用初始状态,并一遍又一遍地通过这些规则,直到可以立即食用大黄草莓派或其他任何东西为止。
因此,在声明式方法中,我们必须知道如何正确构造这些规则。
因此,我们可能要检查成分或状态的规则,如果混合使用,请将其放入锅中。
在初始状态下,这并不匹配,因为我们尚未混合配料。
因此规则2说,如果没有混合,则将它们混合在碗中。好的,这条规则适用。
现在我们有一碗混合食材作为我们的状态。
现在,我们将新状态再次应用于规则。
因此,规则1指出,如果将配料混合在一起放在锅中,是的,现在规则1确实适用,那就去做吧。
现在,我们有了一个新的状态,可以将配料混合并放在锅中。规则1不再相关,规则2不适用。
第3条规则说,如果将配料放在锅中,则将它们放入烤箱中,这很重要,那就是适用于此新状态的规则。
最后我们得到了美味的热苹果派或其他任何东西。
现在,如果您像我一样,您可能会想,为什么我们仍不进行命令式编程。这是有道理的。
是的,对于简单的流程,是的,但是大多数Web应用程序具有更复杂的流程,这些功能无法通过命令式编程设计正确捕获。
在声明式方法中,我们可能具有一些初始成分或初始状态,例如textInput=“”
(单个变量)。
也许文本输入开始时是一个空字符串。
我们采用此初始状态,并将其应用于您的应用程序中定义的一组规则。
如果用户输入文本,请更新文本输入。好吧,现在这不适用。
如果呈现了模板,请计算小部件。
好吧,这都不适用,因此程序只会等待事件发生。
因此,用户有时会更新文本输入,然后我们可以应用规则编号1。
我们可能会将其更新为“abcd”
所以我们只更新了text和textInput更新,规则2不适用,规则3说如果文本输入是刚刚发生的更新,则重新渲染模板,然后回到规则2,即是否呈现模板,计算小部件,好的,让我们计算小部件。
总的来说,作为程序员,我们希望争取更具声明性的编程设计。
命令式似乎更清晰明显,但是声明式方法可以很好地用于大型应用程序。
答案 7 :(得分:0)
我认为有可能以命令式方式表达函数式编程:
-对对象和if... else
/ switch
语句进行大量状态检查
-一些超时/等待机制来解决异步问题
这种方法存在很大的问题:
-重复规则/程序
-有状态性会带来副作用/错误的机会
解决诸如问题之类的功能编程,处理对象之类的功能/方法以及拥抱无状态是天生的。
用法示例:前端应用程序(例如Android,iOS或Web应用程序的逻辑,包括与后端通信
答案 8 :(得分:0)
//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)
命令式程序=更改现有数据的语句序列。
关注WHAT =我们的变异数据(可修改的值,即变量)。
要链接命令式语句=使用过程(和/或oop)。
//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;
}
//2. usage = call functions to get new data
const int c = add(a,b); //c can only be assigned (=) once (const)
功能程序=“解释”如何获取新数据的功能列表。
专注于HOW =我们的功能add
。
要链接功能“语句” =使用功能组合。
这些基本区别具有深远的意义。
严重的软件具有大量的数据和大量的代码。
因此,在代码的多个部分中使用了相同的数据(变量)。
A。在命令式程序中,此(共享)数据的可变性会导致问题
一个优点是:数据已就地修改,无需复制。
B。另一方面,功能代码使用不存在此类问题的不可变数据。数据是只读的,因此没有竞争条件。代码可以很容易地并行化。结果可以被缓存。
一个缺点是:为了获得“修改”,大量复制了数据。
答案 9 :(得分:0)
对于什么是函数式程序和什么是命令式程序,似乎有很多意见。
我认为函数式程序最容易被描述为面向“惰性评估”。不同于让程序计数器遍历指令,该语言设计采用递归方法。
在函数式语言中,函数的计算将从 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 个闭包来包装值,这是毫无价值的。
更明显的方法是实际直接更改值就地 - 但是瞧:您引入了可修改值又名可变值这使得语言势在必行 - 或者实际上是一种混合。
在幕后,当提供输入时,它归结为两种不同的方法来产生输出。
下面,我将尝试用以下项目制作一个城市的插图:
任务:计算第三个斐波那契数。 步骤:
将计算机放入一个盒子中,并用便签标记:
字段 | 价值 |
---|---|
邮件地址 | The Fibonaccis |
退货地址 | Your Home |
参数 | 3 |
返回值 | undefined |
然后送走计算机。
斐波那契数列在收到盒子后会像往常一样做:
参数是<2吗?
是:更改便签,然后将计算机送回邮局:
字段 | 价值 |
---|---|
邮件地址 | The Fibonaccis |
退货地址 | Your Home |
参数 | 3 |
返回值 | 0 或 1 (返回参数) |
并返回给发件人。
否则:
在旧的上面放一个新的便签:
字段 | 价值 |
---|---|
邮件地址 | The Fibonaccis |
退货地址 | Otherwise, step 2, c/o The Fibonaccis |
参数 | 2 (传递参数-1) |
返回值 | undefined |
并发送。
取下返回的便签。将新的便签放在最初的纸条上,然后再次发送给计算机:
字段 | 价值 |
---|---|
邮件地址 | The Fibonaccis |
退货地址 | Otherwise, done, c/o The Fibonaccis |
参数 | 2 (传递参数 2) |
返回值 | undefined |
现在,我们应该有来自请求者的初始便签,以及两个使用过的便签,每个都填充了它们的返回值字段。我们总结返回值并将其放入最终便签的“返回值”字段中。
字段 | 价值 |
---|---|
邮件地址 | The Fibonaccis |
退货地址 | Your Home |
参数 | 3 |
返回值 | 2 (returnValue1 + returnValue2) |
并返回给发件人。
您可以想象,在您将计算机发送到您调用的函数之后,很多工作立即开始。
整个编程逻辑是递归的,但实际上,随着计算机在一堆便利贴的帮助下从一个算法移动到另一个算法,算法会按顺序发生。
任务:计算第三个斐波那契数。步骤:
在便签上写下以下内容:
字段 | 价值 |
---|---|
说明 | 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);
半功能 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
答案 10 :(得分:-2)
我知道这个问题已经陈旧了,其他人已经解释得很好了,我想举一个例子,用简单的术语解释一下。
问题:写1表。
解决方案: -
以命令式风格:=&gt;
1*1=1
1*2=2
1*3=3
.
.
.
1*n=n
按功能样式:=&gt;
1
2
3
.
.
.
n
在命令式样式中的说明我们更明确地编写指令,并且可以以更简化的方式调用它们。
在功能风格中,不言自明的事物将被忽略。