首页 文章

程序编程和函数编程有什么区别? [1]

提问于
浏览
218

我已经阅读了程序编程函数式编程的维基百科文章,但我仍然有些困惑。有人可以把它归结为核心吗?

17 回答

  • 129

    功能语言(理想情况下)允许您编写数学函数 i.e。一个带有 n 个参数并返回值的函数。如果程序被执行,则此函数在逻辑上被评估为 needed.1

    另一方面,过程语言执行一系列顺序步骤。 (有一种方法可以将顺序逻辑转换为称为_1_.)的功能逻辑

    因此,纯函数式程序总是为输入生成相同的值,并且评估的顺序不是 well-defined;这意味着用户输入或随机值等不确定值很难用纯函数式语言进行建模。


    1 与此答案中的其他内容一样,这是一种概括。该属性在需要其结果时计算计算而不是在其被调用的位置处进行计算,称为“懒惰”。并非所有函数式语言实际上都是普遍的懒惰,懒惰也不限于函数式编程。相反,这里给出的描述提供了一个“心理框架”来思考不同的编程风格,这些风格不是截然不同的相反类别,而是流畅的想法。

  • 89

    基本上这两种风格,都喜欢阴阳。一个是有组织的,而另一个是混乱的。有些情况下功能编程是明显的选择,其他情况是程序编程是更好的选择。这就是为什么至少有两种语言最近推出了新版本,它包含了两种编程风格。 (Perl 6D 2)

    程序:

    • 例程的输出并不总是与输入直接相关。

    • 一切都按照特定的顺序完成。

    • 执行例程可能会产生副作用。

    • 倾向于强调以线性方式实施解决方案。

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

    D 2

    int factorial( int n ){
    
      int result = 1;
    
      for( ; n > 0 ; n-- ){
        result *= n;
      }
    
      return result;
    }
    

    功能:

    • 通常是递归的。

    • 始终为给定输入返回相同的输出。

    • 评估顺序通常是不确定的。

    • 必须是无国籍的。 i.e。没有手术会产生副作用。

    • 非常适合并行执行

    • 倾向于强调分而治之的方法。

    • 可能具有懒惰评估功能。

    哈斯克尔

    (从维基百科复制);

    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 }
    

    D 2

    pure int factorial( invariant int n ){
      if( n <= 1 ){
        return 1;
      }else{
        return n * factorial( n-1 );
      }
    }
    

    边注:

    Factorial 实际上是一个常见的例子,表明在 Perl 6 中创建新运算符是多么容易,就像创建子例程一样。这个特性在 Perl 6 中根深蒂固,Rakudo 实现中的大多数操作符都是以这种方式定义的。它还允许您将自己的多候选项添加到现有运算符。

    sub postfix:< ! > ( UInt:D $n --> UInt )
      is tighter(&infix:<*>)
      { [*] 2 .. $n }
    
    say 5!; # 120␤
    

    此示例还显示范围创建(2..$n)和列表缩减 meta-operator([ OPERATOR ] LIST)与数字中缀乘法运算符的组合。 (*)
    它还表明您可以将--> UInt放在签名中而不是returns UInt之后。

    (你可以通过2来启动范围,因为乘法“运算符”在没有任何参数的情况下调用时将返回1)

  • 60

    我从未在其他地方看到这个定义,但我认为这总结了这里给出的差异:

    功能编程侧重于表达式

    程序编程侧重于陈述

    表达式具有价值。功能程序是一个表达式,其值是计算机执行的一系列指令。

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

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

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

    令人困惑的部分是,绝大多数编程语言都包含表达式和语句,允许您混合范例。语言可以根据他们鼓励使用语句与表达的程度来分类为更多功能或更多程序。

    例如,C 将比 COBOL 更有用,因为函数调用是一个表达式,而在 COBOL 中调用子程序是一个语句(它操纵共享变量的状态而不返回值)。 Python 将比 C 更具功能性,因为它允许您使用短路评估将条件逻辑表达为表达式(test && path1 || path2 而不是 if 语句)。 Scheme 比 Python 更有用,因为 scheme 中的所有东西都是表达式。

    您仍然可以使用鼓励程序范例的语言编写函数式样,反之亦然。写一个不受语言鼓励的范式更难__更尴尬。

  • 44

    在计算机科学中,函数式编程是一种编程范式,它将计算视为数学函数的评估,并避免状态和可变数据。它强调功能的应用,与强调状态变化的程序编程风格形成对比。

  • 26

    我相信 procedural/functional/objective 编程是关于如何解决问题的。

    第一种风格将计划所有步骤,并通过一次实施一个步骤(一个过程)来解决问题。另一方面,函数式编程将强调 divide-and-conquer 方法,其中问题被分为 sub-problem,然后每个 sub-problem 被解决(创建解决该子问题的函数)并且结果被组合以创建整个问题的答案。最后,目标编程将模拟现实世界,在计算机内部创建一个具有许多对象的 mini-world,每个对象都具有(某种程度上)独特的特征,并与其他对象进行交互。从这些相互作用中,结果将会出现。

    每种编程风格都有自己的优点和缺点。因此,做一些诸如“纯编程”之类的事情(i.e.纯粹是程序性的 - 没有人这样做,顺便说一句,这有点奇怪 - 或纯粹的功能或纯粹的客观)是非常困难的,如果不是不可能的话,除了一些基本的问题专为展示编程风格的优势而设计(因此,我们称那些喜欢纯粹的人为“weenie”:D)。

    然后,从这些样式中,我们有编程语言,旨在针对每种样式进行优化。例如,汇编就是程序性的。好吧,大多数早期语言都是程序性的,不仅仅是 Asm,比如 C,Pascal,(和 Fortran,我听说过)。然后,我们在客观学校中拥有所有着名的 Java(实际上,Java 和 C#也在一个名为“money-oriented”的类中,但这是另一个讨论的主题)。 Smalltalk 也是客观的。在功能学校,我们会有“近乎功能性”(有些人认为它们是不纯的)Lisp 家族和 ML 家族以及许多“纯粹功能性”的 Haskell,Erlang 等。顺便说一下,有许多通用语言,如 Perl,Python ,Ruby。

  • 12

    扩展康拉德的评论:

    因此,纯函数式程序总是为输入生成相同的值,并且评估的顺序不是 well-defined;

    因此,功能代码通常更容易并行化。由于(通常)函数没有副作用,并且它们(通常)只是对它们的参数起作用,因此很多并发问题都会消失。

    当您需要能够证明您的代码是正确的时,也会使用函数式编程。这对于程序编程来说要困难得多(功能不容易,但更容易)。

    免责声明:我多年没有使用过函数式编程,直到最近才开始再次查看它,所以我在这里可能不完全正确。 :)

  • 11

    我在这里没有看到的一件事是,现代函数语言(如 Haskell)在流控制的第一类函数上比在显式递归中更多。您不需要在 Haskell 中递归地定义阶乘,如上所述。我觉得有点像

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

    是一个完美的惯用结构,在使用循环方面比使用显式递归更接近精神。

  • 8

    函数式编程与不使用全局变量的过程式编程相同。

  • 6

    过程语言倾向于跟踪状态(使用变量)并倾向于作为一系列步骤执行。纯函数式语言不跟踪状态,使用不可变值,并倾向于作为一系列依赖项执行。在许多情况下,调用堆栈的状态将保存与过程代码中的状态变量中存储的信息等效的信息。

    递归是功能样式编程的典型例子。

  • 6

    康拉德说:

    因此,纯函数式程序总是为输入生成相同的值,并且评估的顺序不是 well-defined;这意味着用户输入或随机值等不确定值很难用纯函数式语言进行建模。

    纯粹功能性程序中的评估顺序可能是 hard(er)推理(尤其是懒惰)甚至不重要但我认为说它没有明确定义会让你听不清楚你的程序是否会起作用一点都没有!

    或许更好的解释是功能程序中的控制流程基于何时需要函数参数的值。关于这一点的好事,在编写良好的程序中,状态变得明确:每个函数将其输入列为参数而不是任意改写(munging)全局状态。因此,在某种程度上,一次就一个函数更容易推理评估顺序。每个函数都可以忽略宇宙的其余部分,并专注于它需要做的事情。结合使用时,保证功能与隔离功能相同[34]。

    ...用户输入或随机值等不确定值很难用纯函数语言建模。

    纯功能程序中输入问题的解决方案是使用一个足够强大的抽象将命令式语言作为DSL嵌入。在命令式(或 non-pure 功能)语言中,这不是必需的,因为您可以“欺骗”并隐式传递状态,并且评估顺序是明确的(无论您是否喜欢)。由于这种“作弊”并强制评估每个函数的所有参数,在命令式语言 1)你失去了创建自己的控制流机制(没有宏)的能力,2)代码本身并不是线程安全 and/or 默认可并行化,3)并实现像撤消(时间旅行)这样的事情需要仔细的工作(命令式程序员必须存储一个用于获取旧 value(s 的食谱)!,而纯函数编程会购买所有这些东西 - 还有一些我可能已经忘记了-“免费”。

    我希望这听起来不像狂热,我只想添加一些观点。命令式编程,特别是 C#3.0 等强大语言中的混合范式编程仍然是完成任务和没有银弹的完全有效的方法。

    [41] ......除了可能尊重内存使用(参见 Haskell 中的 foldl 和 foldl')。

  • 6

    功能编程

    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

  • 5

    扩展康拉德的评论:

    并且评估顺序不是 well-defined

    一些函数式语言具有所谓的 Lazy Evaluation。这意味着在需要值之前不会执行函数。在此之前,函数本身就是传递的东西。

    程序语言是步骤 1 步骤 2 步骤 3 ...如果在步骤 2 中你说加 2 2,它就是正确的。在延迟评估中,您会说添加 2 2,但如果结果从未使用过,则它永远不会添加。

  • 4

    如果您有机会,我建议您获取 Lisp/Scheme 的副本,并在其中执行一些项目。最近成为带号的大多数想法在几十年前的 Lisp 中得到了表达:函数式编程,延续(作为闭包),垃圾收集,甚至是 XML。

    因此,这将是一个很好的方式来开始所有这些当前的想法,还有一些,如符号计算。

    你应该知道什么是函数式编程,哪些函数编程不好。这对一切都不好。有些问题最好以 side-effects 表示,其中同一问题根据问题的时间给出不同的答案。

  • 3

    @Creighton:

    在 Haskell 中有一个名为product的库函数:

    prouduct list = foldr 1 (*) list
    

    或者干脆:

    product = foldr 1 (*)
    

    所以“惯用的”因子

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

    就是这样

    fac n = product [1..n]
    
  • 2

    过程编程将语句和条件结构的序列划分为称为过程的单独块,这些过程在(non-functional)值的参数上进行参数化。

    函数编程是相同的,除了函数是 first-class 值,因此它们可以作为参数传递给其他函数,并作为函数调用的结果返回。

    请注意,函数式编程是此解释中的过程式编程的概括。然而,少数人将“函数式编程”解释为 side-effect-free,这与 Haskell 之外的所有主要函数式语言完全不同但不相关。

  • 1

    要理解差异,需要理解程序和函数式编程的“教父”范式是命令式编程

    基本上,程序编程只是构建命令式程序的一种方式,其中主要的抽象方法是“过程”。 (或某些编程语言中的“函数”)。甚至面向对象编程也只是构造命令式程序的另一种方式,其中状态被封装在对象中,成为具有“当前状态”的对象,此外该对象还有一组函数,方法和其他东西可以让你程序员操纵或更新状态。

    现在,关于函数式编程,其方法的要点是它确定要采用的值以及如何传输这些值。 (因此没有状态,也没有可变数据,因为它将函数作为第一类值并将它们作为参数传递给其他函数)。

    PS:理解每个编程范例用于澄清所有这些范例之间的差异。

    PSS:在一天结束时,编程范式只是解决问题的不同方法。

    PSS:这个 quora 答案有很好的解释。

  • 0

    这里的答案都没有显示惯用的函数式编程。递归因子答案非常适合表示 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
    

    虽然纯函数式语言通常是研究语言(因为 real-world 喜欢 free side-effects),real-world 过程语言将在适当时使用更简单的函数语法。

    这通常使用像Lodash这样的外部库实现,或者使用更新的语言(如)实现 built-in。函数式编程的繁重工作是使用 functions/concepts,如mapfilterreducecurryingpartial完成的,最后三个可以查找以便进一步理解。

    附录

    为了在野外使用,编译器通常必须在内部解决如何将函数版本转换为过程版本,因为函数调用开销太高。递归情况(如显示的因子)将使用诸如尾巴电话之类的技巧来删除 O(n)内存使用情况。没有副作用的事实允许函数编译器实现&& ret优化,即使最后完成.reduce也是如此。在 JS 中使用 Lodash 显然不允许进行任何优化,因此它对性能的影响很大(这通常不是 Web 开发的关注点)。 Rust 之类的语言将在内部进行优化(并具有try_fold等功能以帮助优化&& ret)。

相关问题