我已经阅读了维基百科上关于过程式编程和函数式编程的文章,但我还是有点困惑。有人能把它归结为核心吗?


当前回答

进一步阐述康拉德的评论:

求值的顺序不是 定义良好的

一些函数式语言有所谓的惰性求值。这意味着直到需要该值时才执行函数。在此之前,传递的是函数本身。

过程式语言是步骤1、步骤2、步骤3……如果在第二步你说加2 + 2,它马上就会做。在惰性求值中,你会说2 + 2,但如果结果从未被使用,它就永远不会做加法。

其他回答

过程式编程将语句序列和条件构造划分为单独的块,称为过程,这些块通过参数化(非函数式)值。

函数式编程与此类似,只是函数是一类值,因此它们可以作为参数传递给其他函数,并作为函数调用的结果返回。

注意,在这个解释中,函数式编程是过程式编程的泛化。然而,少数人将“函数式编程”解释为没有副作用,这与除Haskell之外的所有主要函数式语言都完全不同,但无关紧要。

我从来没有在其他地方看到过这样的定义,但我认为这很好地总结了这里给出的差异:

函数式编程主要关注表达式

过程式编程主要关注语句

表达式有值。函数式程序是一个表达式,其值是由计算机执行的一系列指令。

语句没有值,而是修改一些概念机器的状态。

在纯函数式语言中,没有语句,也就是说没有办法操纵状态(它们可能仍然有一个名为“语句”的语法结构,但除非它操纵状态,否则我不会在这种意义上称其为语句)。在纯程序语言中,没有表达式,一切都是操纵机器状态的指令。

Haskell是纯函数式语言的一个例子,因为没有办法操纵状态。机器代码是纯过程语言的一个例子,因为程序中的所有内容都是操作机器寄存器和内存状态的语句。

令人困惑的部分是,绝大多数编程语言同时包含表达式和语句,允许您混合使用范式。语言可以根据它们鼓励使用语句和表达式的程度被分类为更函数化或更过程化。

For example, C would be more functional than COBOL because a function call is an expression, whereas calling a sub program in COBOL is a statement (that manipulates the state of shared variables and doesn't return a value). Python would be more functional than C because it allows you to express conditional logic as an expression using short circuit evaluation (test && path1 || path2 as opposed to if statements). Scheme would be more functional than Python because everything in scheme is an expression.

你仍然可以在一种鼓励过程范式的语言中以函数式风格编写,反之亦然。只是在语言不鼓励的范式下写作更困难和/或更尴尬。

基本上这两种风格,就像阴阳。一个是有组织的,而另一个是混乱的。在某些情况下,函数式编程是显而易见的选择,而在其他情况下,过程式编程是更好的选择。这就是为什么至少有两种语言最近推出了新版本,包含了这两种编程风格。(Perl 6和d2)

#程序:#

例程的输出并不总是与输入直接相关。 每件事都有特定的顺序。 例程的执行可能有副作用。 倾向于强调以线性方式实现解决方案。

##Perl 6 ##

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;
}

2 # # # # D

int factorial( int n ){

  int result = 1;

  for( ; n > 0 ; n-- ){
    result *= n;
  }

  return result;
}

#功能:#

通常递归。 对于给定的输入总是返回相同的输出。 计算的顺序通常是不确定的。 必须是无状态的。即任何手术都不能有副作用。 很适合并行执行 倾向于强调分而治之的方法。 可具有惰性求值的特性。

哈斯克尔# # # # (摘自维基百科);

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

##Perl 6 ##

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 }

2 # # # # D

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,而不是在签名后返回UInt。

(你可以用2开始范围,因为乘法“运算符”在不带任何参数的情况下调用时将返回1)

这里没有一个答案显示了惯用的函数式编程。递归阶乘的答案很适合在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等函数/概念完成的,最后三个你可以查阅以进一步理解。

齿顶高

In order to be used in the wild, the compiler will normally have to work out how to convert the functional version into the procedural version internally, as function call overhead is too high. Recursive cases such as the factorial shown will use tricks such as tail call to remove O(n) memory usage. The fact that there are no side effects allows functional compilers to implement the && ret optimization even when the .reduce is done last. Using Lodash in JS, obviously does not allow for any optimization, so it is a hit to performance (Which isn't usually a concern with web development). Languages like Rust will optimize internally (And have functions such as try_fold to assist && ret optimization).

我在这里没有看到真正强调的一点是,现代函数语言(如Haskell)实际上更多地关注流控制的第一类函数,而不是显式递归。您不需要像上面那样在Haskell中递归地定义阶乘。我想是这样的

fac n = foldr (*) 1 [1..n]

是一个完美的惯用结构,在精神上更接近于使用循环,而不是使用显式递归。