Answers:
函数式语言(理想情况下)允许您编写数学函数,即采用n个参数并返回值的函数。如果执行程序,则根据需要对该功能进行逻辑评估。1个
另一方面,过程语言执行一系列顺序步骤。(有一种将顺序逻辑转换为功能逻辑的方法,称为连续传递样式。)
结果,一个纯功能的程序对于输入总是产生相同的值,并且评估的顺序没有明确定义。这意味着很难用纯功能语言来建模诸如用户输入或随机值之类的不确定值。
1正如此答案中的所有其他内容一样,这是一个概括。该属性在需要计算结果时而不是在调用结果的位置按顺序进行计算时被称为“惰性”。并非所有的函数语言实际上都是普遍懒惰的,懒惰也不限于函数式编程。相反,此处给出的描述提供了一个“思想框架”,以考虑不同的编程风格,这些风格不是截然相反的类别,而是流动的想法。
基本上这两种风格,就像阴和阳。一个是有组织的,而另一个则是混乱的。在某些情况下,函数式编程是显而易见的选择,而在其他情况下,过程式编程是更好的选择。这就是为什么最近至少有两种新版本推出了包含两种编程风格的语言。(Perl 6和D 2)
sub factorial ( UInt:D $n is copy ) returns UInt {
# modify "outside" state
state $call-count++;
# in this case it is rather pointless as
# it can't even be accessed from outside
my $result = 1;
loop ( ; $n > 0 ; $n-- ){
$result *= $n;
}
return $result;
}
int factorial( int n ){
int result = 1;
for( ; n > 0 ; n-- ){
result *= n;
}
return result;
}
(从Wikipedia复制);
fac :: Integer -> Integer
fac 0 = 1
fac n | n > 0 = n * fac (n-1)
或一行:
fac n = if n > 0 then n * fac (n-1) else 1
proto sub factorial ( UInt:D $n ) returns UInt {*}
multi sub factorial ( 0 ) { 1 }
multi sub factorial ( $n ) { $n * samewith $n-1 } # { $n * factorial $n-1 }
pure int factorial( invariant int n ){
if( n <= 1 ){
return 1;
}else{
return n * factorial( n-1 );
}
}
实际上,阶乘是一个常见的示例,它说明了与在创建子例程中所使用的方法一样,在Perl 6中创建新运算符是多么容易。此功能已根植于Perl 6中,因此Rakudo实现中的大多数运算符都是通过这种方式定义的。它还允许您将自己的多个候选项添加到现有的运算符。
sub postfix:< ! > ( UInt:D $n --> UInt )
is tighter(&infix:<*>)
{ [*] 2 .. $n }
say 5!; # 120
此示例还显示了范围创建(2..$n
)和列表精简元运算符([ OPERATOR ] LIST
)与数字中缀乘法运算符的组合。(*
)
它也表明您可以放入--> UInt
签名,而不是放在签名returns UInt
之后。
(您可以从以开始范围开始,2
因为在1
没有任何参数的情况下调用乘号“ operator”将返回)
sub postfix:<!> ($n) { [*] 1..$n }
No operation can have side effects
您能详细说明一下吗?
sub foo( $a, $b ){ ($a,$b).pick }
←并不总是为相同的输入返回相同的输出,而以下内容会返回sub foo( $a, $b ){ $a + $b }
我从未在其他地方看到过这个定义,但是我认为这很好地总结了这里给出的区别:
函数式编程专注于表达式
程序编程侧重于语句
表达式具有值。功能程序是一种表达式,其值是计算机要执行的一系列指令。
语句没有值,而是修改某些概念性机器的状态。
在纯功能语言中,就没有语句可以操纵状态(它们可能仍具有名为“ statement”的句法构造),在这种意义上,就不会有任何语句,但是除非它操纵状态,否则我不会在这种意义上称其为语句)。在纯粹的过程语言中,不会有表达式,所有内容都会是一条操纵机器状态的指令。
Haskell将是纯功能语言的一个示例,因为无法操纵状态。机器代码将是纯粹过程语言的示例,因为程序中的所有内容都是一条语句,用于操纵机器的寄存器和内存的状态。
混乱的是,绝大多数的编程语言中包含两种表达式和语句,使您可以混合范式。根据语言鼓励使用陈述式还是表达式的程度,可以将它们归类为功能更强或程序性更强的语言。
例如,C比COBOL更具功能性,因为函数调用是一个表达式,而在COBOL中调用子程序则是一条语句(操纵共享变量的状态并且不返回值)。Python比C更具功能性,因为它允许您使用短路评估(测试&& path1 || path2而不是if语句)将条件逻辑表示为表达式。Scheme比Python更具功能性,因为Scheme中的所有内容都是表达式。
您仍然可以以一种能鼓励程序范式的语言来编写功能样式,反之亦然。用这种语言鼓励的范式编写起来更困难和/或更尴尬。
在计算机科学中,函数式编程是一种编程范例,将计算视为对数学函数的评估,并避免了状态数据和可变数据。与强调状态变化的过程编程风格相反,它强调函数的应用。
GetUserContext()
函数中,而会传递用户上下文。这是函数式编程吗?提前致谢。
我相信过程/功能/目标编程是关于如何解决问题的方法。
第一种样式将按步骤计划所有内容,并通过一次执行一个步骤(一个过程)来解决问题。另一方面,函数式编程将强调分而治之的方法,即将问题分为子问题,然后解决每个子问题(创建一个解决该子问题的函数)并将结果组合为为整个问题创造答案。最后,Objective编程将通过在计算机内部创建一个包含许多对象的微型世界来模仿现实世界,每个对象都具有(某种)独特的特征并与其他物体交互。从这些相互作用中将得出结果。
每种编程风格都有其自身的优点和缺点。因此,进行“纯粹编程”之类的事情(即纯粹的程序性-顺便说一句,没有人这样做,这很奇怪-或纯粹的功能性或纯粹的客观性)非常困难,即使不是不可能,除了一些基本问题之外旨在证明编程风格的优势(因此,我们称呼那些喜欢纯粹的人为“ weenie”:D)。
然后,从这些样式中,我们获得了针对每种样式进行优化的编程语言。例如,汇编就是关于程序的。好的,大多数早期语言都是程序性的,不仅像C,Pascal这样的Asm(还有Fortran,我听说)。然后,我们在目标学校都拥有着名的Java(实际上,Java和C#也在名为“面向金钱”的课程中,但这需要进一步讨论)。目标也是Smalltalk。在功能学校中,我们将拥有“几乎功能性”(有些人认为它们是不纯的)Lisp家族和ML家族,还有许多“纯粹功能性”的Haskell,Erlang等。顺便说一下,有许多通用语言,例如Perl,Python ,露比
num = 1
def function_to_add_one(num):
num += 1
return num
function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)
#Final Output: 2
num = 1
def procedure_to_add_one():
global num
num += 1
return num
procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()
#Final Output: 6
function_to_add_one
是一个功能
procedure_to_add_one
是一个过程
即使您运行该函数五次,每次也会返回2
如果您运行该过程五次,则在第五次运行结束时将给您6。
程序语言倾向于跟踪状态(使用变量),并倾向于按一系列步骤执行。纯粹的功能语言不会跟踪状态,使用不可变的值,并且倾向于以一系列依赖关系的形式执行。在许多情况下,调用堆栈的状态将保存与程序代码中状态变量中存储的信息等效的信息。
递归是功能样式编程的经典示例。
康拉德说:
结果,一个纯粹的功能程序总是为输入产生相同的值,并且评估顺序没有明确定义;这意味着很难用纯功能语言来建模诸如用户输入或随机值之类的不确定值。
在纯功能程序中,评估的顺序可能很难(尤其是懒惰)甚至不重要,但我认为说它的定义不明确,听起来好像您无法判断程序是否在运行上班!
也许更好的解释是功能程序中的控制流基于何时需要函数参数的值。关于这一点的好消息是,在编写良好的程序中,状态变得很明确:每个函数都将其输入作为参数列出,而不是任意改变全局状态。因此,在某种程度上,更容易一次就一个功能推断评估顺序。每个函数都可以忽略其余部分,而将精力集中在需要做的事情上。组合使用时,可以保证功能与单独运行时的功能相同[1]。
...用纯函数语言很难对诸如用户输入或随机值之类的不确定值进行建模。
在纯功能程序中,输入问题的解决方案是使用足够强大的抽象将命令性语言嵌入DSL。在命令式(或非纯函数式)语言中,这不是必需的,因为您可以“欺骗”并隐式传递状态,并且评估的顺序是明确的(无论您是否喜欢)。由于这种“欺骗”和对所有函数的所有参数的强制求值,使用命令式语言1)您失去了创建自己的控制流机制(没有宏)的能力,2)代码本质上不是线程安全和/或可并行化的默认,3)和实现类似undo(时间旅行)之类的工作需要认真的工作(当务之急,程序员必须存储一个方法来取回旧的值!),而纯函数式编程可以为您提供所有这些东西,而我可能还要已经忘记了“免费”。
我希望这听起来不像是狂热,我只是想补充一些观点。命令式编程,尤其是C#3.0之类的强大语言中的混合范例编程,仍然是完成任务的完全有效方法,没有灵丹妙药。
[1] ...可能在内存使用方面除外(参见Haskell中的foldl和foldl')。
@Creighton:
在Haskell中,有一个名为product的库函数:
prouduct list = foldr 1 (*) list
或者简单地:
product = foldr 1 (*)
所以“惯用”阶乘
fac n = foldr 1 (*) [1..n]
简直是
fac n = product [1..n]
要理解这种差异,需要了解程序和功能编程的“教父”范式是命令式编程。
基本上,过程编程只是一种构造命令式程序的方法,其中抽象的主要方法是“过程”。(或某些编程语言中的“功能”)。甚至面向对象的编程也只是构造命令式程序的另一种方法,其中状态被封装在对象中,成为具有“当前状态”的对象,此外该对象还具有一组函数,方法和其他东西,可以让您程序员操纵或更新状态。
现在,关于函数式编程,其要旨是确定要采用的值以及应如何传递这些值。(因此没有状态,也没有可变数据,因为它将函数作为第一类值并将它们作为参数传递给其他函数)。
PS:了解每种编程范例所用的内容,应阐明它们之间的差异。
PSS:归根结底,编程范例只是解决问题的不同方法。
PSS:这个法定答案有很好的解释。
这里没有答案显示惯用的函数式编程。递归阶乘答案非常适合表示FP中的递归,但是大多数代码不是递归的,因此我认为答案不能完全代表。
假设您有一个字符串数组,每个字符串代表一个整数,例如“ 5”或“ -200”。您想对照内部测试用例检查此字符串输入数组(使用整数比较)。两种解决方案如下所示
arr_equal(a : [Int], b : [Str]) -> Bool {
if(a.len != b.len) {
return false;
}
bool ret = true;
for( int i = 0; i < a.len /* Optimized with && ret*/; i++ ) {
int a_int = a[i];
int b_int = parseInt(b[i]);
ret &= a_int == b_int;
}
return ret;
}
eq = i, j => i == j # This is usually a built-in
toInt = i => parseInt(i) # Of course, parseInt === toInt here, but this is for visualization
arr_equal(a : [Int], b : [Str]) -> Bool =
zip(a, b.map(toInt)) # Combines into [Int, Int]
.map(eq)
.reduce(true, (i, j) => i && j) # Start with true, and continuously && it with each value
虽然纯功能语言通常是研究语言(因为现实世界喜欢自由副作用),但在适当的情况下,现实世界的过程语言将使用简单得多的功能语法。
通常,这可以通过外部库(如Lodash)或可用的内置内建语言(如Rust)来实现。函数式编程的繁重与类似功能/概念做了map
,filter
,reduce
,currying
,partial
,它的最后三个你可以看一下做进一步的了解。
为了在野外使用,编译器通常必须弄清楚如何在内部将功能版本转换为过程版本,因为函数调用开销太高。递归情况(例如所示的阶乘)将使用诸如尾调用之类的技巧来删除O(n)内存使用情况。没有副作用的事实允许函数式编译器&& ret
即使在.reduce
最后完成时也可以实现优化。在JS中使用Lodash显然不允许进行任何优化,因此会降低性能(通常与Web开发无关)。诸如Rust之类的语言将在内部进行优化(并具有诸如try_fold
辅助&& ret
优化之类的功能)。