大多数主流语言,包括 C#、Visual Basic、C++ 和 Java 等面向对象编程 (OOP) 语言,旨在主要支持命令式(过程)编程,而类似 Haskell/gofer 的语言则是纯粹的函数式。谁能详细说明这两种编程方式有什么区别?
我知道选择编程方式取决于用户需求,但为什么建议学习函数式编程语言?
这是区别:
至关重要的:
开始
打开你的鞋子尺寸 9 1/2。
在你的口袋里腾出空间来存放一个数组[7]的钥匙。
把钥匙放在房间里,把钥匙放在口袋里。
进入车库。
打开车库。
进入汽车。
...等等等等...
将牛奶放入冰箱。
停止。
声明性的,其中功能性是一个子类别:
牛奶是一种健康饮料,除非您在消化乳糖方面有问题。
通常,人们将牛奶储存在冰箱中。
冰箱是一个让里面的东西保持凉爽的盒子。
商店是出售物品的地方。
我们所说的“卖”是指用东西换钱。
还有,用钱换东西,叫做“买”。
...等等等等...
确保冰箱里有牛奶(当我们需要的时候——对于懒惰的函数式语言)。
简介:在命令式语言中,您告诉计算机如何更改其内存中的位、字节和字以及以什么顺序。在函数式中,我们告诉计算机事物、动作等是什么。例如,我们说 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 ie n 的阶乘!即 nx (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 可以作为一个很好的参考,以了解命令式代码如何更多地关注部分、机器状态(i 在 for 循环中)、执行顺序、流控制。
后面的示例可以粗略地视为 java/C# 语言代码,第一部分是语言本身的限制,而 Haskell 则按值(零)重载函数,因此可以说它不是纯粹的函数式语言,另一方面手你可以说它支持功能编。在某种程度上。
披露:上述代码均未经过测试/执行,但希望足以传达概念;我也将不胜感激任何此类更正的评论:)
return n * factorial(n-1);
吗?
n * (n-1)
更正它
函数式编程是声明式编程的一种形式,它描述了计算的逻辑并且完全不强调执行顺序。
问题:我想把这个生物从马变成长颈鹿。
拉长脖子
拉长腿
涂抹斑点
给这个生物一个黑色的舌头
去除马尾
每个项目都可以按任何顺序运行以产生相同的结果。
命令式编程是程序性的。状态和秩序很重要。
问题:我想停车。
注意车库门的初始状态 将车停在车道上 如果车库门关闭,打开车库门,记住新的状态;否则继续 把车开进车库 关上车库门
必须完成每个步骤才能达到预期的结果。在车库门关闭时拉入车库会导致车库门损坏。
(x - 1) * 3
与 (x * 3) - 1
不同。作为另一个示例,fillBasket(emptyBasket(b), items)
与 emptyBasket(fillBasket(b, items))
相比具有不同的效果。
函数式编程是“用函数编程”,其中函数具有一些预期的数学属性,包括引用透明性。从这些性质,进一步的性质流动,特别是通过导致数学证明的可替代性启用的熟悉的推理步骤(即证明对结果的信心)。
因此,函数式程序只是一个表达式。
通过注意命令式程序中表达式不再具有引用透明性(因此不是用函数和值构建的,并且本身不能成为函数的一部分)的地方,您可以很容易地看到两种风格之间的对比。最明显的两个地方是: 突变(例如变量) 其他副作用 非本地控制流(例如异常)
在这个由函数和值组成的程序即表达式框架上,构建了语言、概念、“功能模式”、组合子以及各种类型系统和评估算法的完整实用范式。
根据最极端的定义,几乎任何语言——即使是 C 或 Java——都可以称为函数式,但通常人们将这个术语保留给具有特定相关抽象的语言(例如闭包、不可变值和语法辅助,如模式匹配)。就函数式编程的使用而言,它涉及使用 functins 并构建没有任何副作用的代码。用来写证明
• 命令式语言:
高效执行
复杂的语义
复杂的语法
并发是程序员设计的
复杂的测试,没有参考透明度,有副作用
有状态
• 函数式语言:
简单语义
简单的语法
执行效率较低
程序可以自动并发
测试简单,具有引用透明性,无副作用
没有状态
命令式编程风格从 2005 年一直到 2013 年在 Web 开发中得到实践。
通过命令式编程,我们编写的代码一步一步准确地列出了我们的应用程序应该做什么。
函数式编程风格通过巧妙地组合函数的方式产生抽象。
答案中提到了声明式编程,对此我会说声明式编程列出了我们要遵循的一些规则。然后,我们向应用程序提供我们所说的一些初始状态,并让这些规则定义应用程序的行为方式。
现在,这些快速描述可能没有多大意义,所以让我们通过一个类比来了解命令式编程和声明式编程之间的区别。
想象一下,我们不是在构建软件,而是以烤馅饼为生。也许我们是糟糕的面包师,不知道如何以我们应该的方式烤出美味的馅饼。
所以我们的老板给了我们一份方向清单,我们称之为食谱。
食谱会告诉我们如何做馅饼。一个食谱是用命令式编写的,如下所示:
混合 1 杯面粉 加 1 个鸡蛋 加 1 杯糖 将混合物倒入平底锅中 将平底锅放入烤箱 30 分钟,华氏 350 度。
声明式配方将执行以下操作:
1杯面粉,1个鸡蛋,1杯糖-初始状态
规则
如果一切都混合了,放在锅里。如果一切都没有混合,放在碗里。如果一切都在锅里,放在烤箱里。
因此,命令式方法的特点是逐步方法。您从第一步开始,然后转到第二步,依此类推。
你最终会得到一些最终产品。所以做这个馅饼,我们把这些原料混合在一起,把它放在平底锅和烤箱里,你就得到了你的最终产品。
在声明式世界中,情况有所不同。在声明式配方中,我们会将配方分成两个独立的部分,从列出配方初始状态的部分开始,例如变量。所以我们这里的变量是我们成分的数量和它们的类型。
我们采用初始状态或初始成分并对它们应用一些规则。
因此,我们采用初始状态并一遍又一遍地通过这些规则,直到我们准备好吃大黄草莓派或其他任何东西。
因此,在声明式方法中,我们必须知道如何正确构建这些规则。
因此,我们可能要检查我们的成分或状态的规则,如果混合,请将它们放入平底锅中。
与我们的初始状态不匹配,因为我们还没有混合我们的成分。
所以规则 2 说,如果它们不混合,则将它们混合在一个碗中。好的,是的,这条规则适用。
现在我们有一碗混合配料作为我们的状态。
现在我们再次将这个新状态应用到我们的规则中。
所以规则 1 说,如果成分混合,请将它们放在平底锅中,好吧,是的,现在规则 1 确实适用,让我们这样做。
现在我们有了这个新的状态,其中的成分混合在一个平底锅里。规则 1 不再相关,规则 2 不适用。
规则 3 说如果原料在锅里,把它们放在烤箱里,这条规则适用于这个新状态,让我们去做吧。
最后我们得到了一个美味的热苹果派或其他什么。
现在,如果你和我一样,你可能会想,为什么我们还不做命令式编程。这是有道理的。
好吧,对于简单的流程是可以的,但是大多数 Web 应用程序都有更复杂的流程,无法通过命令式编程设计正确捕获。
在声明式方法中,我们可能有一些初始成分或初始状态,例如 textInput=“”
,单个变量。
也许文本输入以空字符串开始。
我们采用此初始状态并将其应用于您的应用程序中定义的一组规则。
如果用户输入文本,则更新文本输入。嗯,现在不适用。如果呈现模板,则计算小部件。如果 textInput 已更新,则重新渲染模板。
好吧,这些都不适用,所以程序只会等待事件发生。
因此,在某个时候,用户更新了文本输入,然后我们可能会应用第 1 条规则。
我们可能会将其更新为 “abcd”
所以我们刚刚更新了我们的 text 和 textInput 更新,第 2 条规则不适用,第 3 条规则表示是否更新了文本输入,这刚刚发生,然后重新渲染模板,然后我们回到规则 2,即如果模板被渲染,计算小部件,好吧,让我们计算小部件。
一般来说,作为程序员,我们希望争取更多的声明式编程设计。
命令式似乎更加清晰和明显,但声明式方法非常适合大型应用程序。
//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. 另一方面,功能代码使用不可变数据,不存在此类问题。数据是只读的,因此没有竞争条件。代码可以很容易地并行化。结果可以被缓存。更容易理解。
作为一个缺点:为了获得“修改”,数据被复制了很多。
另请参阅:https://en.wikipedia.org/wiki/Referential_transparency
我认为可以用命令式的方式来表达函数式编程:
使用大量对象的状态检查和 if...else/switch 语句
一些超时/等待机制来处理异步
这种方法存在巨大的问题:
重复规则/程序
有状态会留下副作用/错误的机会
函数式编程,将函数/方法视为对象并拥抱无状态,是为了解决我认为的这些问题而诞生的。
使用示例:Android、iOS 等前端应用程序或网络应用程序的逻辑,包括。与后端的通信。
使用命令式/过程代码模拟函数式编程时的其他挑战:
比赛条件
复杂的组合和事件顺序。例如,用户尝试在银行应用程序中汇款。步骤 1) 并行执行以下所有操作,只有在一切正常时才继续 a) 检查用户是否仍然良好(欺诈、AML) b) 检查用户是否有足够的余额 c) 检查收件人是否有效且良好(欺诈、 AML) 等 步骤 2) 执行转账操作 步骤 3) 显示用户余额和/或某种跟踪的更新。以 RxJava 为例,代码简洁明了。没有它,我可以想象会有很多代码,混乱和容易出错的代码
我还相信,最终,功能代码将被编译器转换为命令式/程序化的汇编或机器代码。但是,除非您编写汇编,因为人类使用高级/人类可读语言编写代码,否则函数式编程是列出的场景更合适的表达方式
关于什么是函数式程序和什么是命令式程序,似乎有很多意见。
我认为函数式程序最容易被描述为面向“惰性评估”。该语言设计采用递归方法,而不是让程序计数器遍历指令。
在函数式语言中,函数的计算将从 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 个闭包来包装这个值,这是毫无价值的。
更明显的方法是实际直接更改值 - 但是瞧:您已经引入了可修改的值,即可变值,这使得语言变得势在必行 - 或者实际上是混合体。
在引擎盖下,当提供输入时,它归结为两种不同的方法来产生输出。
下面,我将尝试用以下项目来描绘一个城市:
你家的电脑 斐波那契
命令式语言
任务:计算第三个斐波那契数。脚步:
将计算机放入一个盒子并用便笺标记它: 字段值邮件地址斐波那契返回地址您的家庭参数 3 返回值未定义并发送计算机。斐波那契在收到盒子后会像往常一样做:参数是否 < 2?是:更改便笺,并将计算机退回邮局:字段值邮件地址斐波那契返回地址您的家庭参数3返回值0或1(返回参数)并返回给发件人。否则:在旧的上面贴一张新的便签:字段值邮件地址斐波那契返回地址否则,步骤 2,c/o斐波那契参数 2(传递参数-1)返回值未定义并发送。取下退回的便签。将新的便签放在最初的便签上,然后再次发送计算机: 字段值 邮件地址 斐波那契返回地址 否则,完成,c/o 斐波那契参数 2(传递参数 2) 返回值未定义 现在,我们应该有来自请求者的初始便笺和两个使用过的便笺,每个都填充了它们的返回值字段。我们将返回值汇总并放在最终便笺的 Return Value 字段中。字段值邮件地址斐波那契返回地址您的家庭参数3 返回值2 (returnValue1 + returnValue2) 并返回给发件人。
正如您可以想象的那样,在您将计算机发送到您调用的功能后,会立即开始大量工作。
整个编程逻辑是递归的,但实际上,当计算机在一堆便签的帮助下从一个算法移动到另一个算法时,算法是按顺序发生的。
函数式语言
任务:计算第三个斐波那契数。脚步:
在便利贴上写下以下内容: 字段值说明 斐波那契参数 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);
HALF 功能 javascript 版本将是:
let fib = (n) => () =>
n < 2 ?
n :
fib(n-1) + fib(n-2);
PURE 函数式 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/
我知道这个问题比较老,其他人已经很好地解释了,我想举一个简单的例子来解释相同的问题。
问题:写 1 的表。
解决方案: -
按命令式:=>
1*1=1
1*2=2
1*3=3
.
.
.
1*n=n
按功能风格:=>
1
2
3
.
.
.
n
命令式的解释我们更明确地编写指令,并且可以以更简化的方式调用。
在功能风格中,不言自明的东西将被忽略。
不定期副业成功案例分享