zoukankan      html  css  js  c++  java
  • 函数式编程介绍

    历史来源

    讲述历史来源,不喜欢的可以跳过。但是我个人认为这对理解有帮助。

    在计算机的世界中,有两位巨擘对问题的可计算性做了模型化描述[4]。

    一位是阿兰.图灵(Alan Turing),他提出的图灵机。计算机系的各种学科中都充斥着这个概念,假设有一个纸带和一个打孔机,然后有一套指令,能够控制打孔机在纸带上移动、能够读取当前位置是否打了孔、能够在当前位置打一个孔,这就是一个图灵机,假设一个问题能够靠这个纸带+打孔机+指令的方式解决,那就说明这个问题是“可计算的”。

    另外一个位巨擘,是阿隆佐·邱奇(Alonzo Church)。邱奇是个数学家,他提出了Lambda演算(Lambda Calculus)的概念,用函数组合的方式来描述计算过程,换句话来说,如果一个问题能够用一套函数组合的算法来表达,那就说明这个问题是可计算的。

    我个人对这两种计算过程的模型是这样理解的,不一定对:

    图灵机是通过一系列指令和状态来完成某种过程的,即,在某种状态下使用怎样的指令转移到某种状态。
    Lambda演算是通过一个函数来解决这个问题,而这个函数又是由一系列别的函数组成,这样递归下去,最终达到常量。有些类似动态规划那种一个问题由多个子问题组成,子问题又有自己的子问题,最后到达一些已知解的问题。
    不是全部的Lambda演算思想都可以运用到实际中,因Lambda演算在设计的时候就不是为了在各种现实世界中的限制下工作的。
    编程范式

    编程语言主要有三种类型[3]:

    命令式编程(Imperative Programming): 专注于”如何去做”,这样不管”做什么”,都会按照你的命令去做。解决某一问题的具体算法实现。
    函数式编程(Functional Programming):把运算过程尽量写成一系列嵌套的函数调用。
    逻辑式编程(Logical Programming):它设定答案须符合的规则来解决问题,而非设定步骤来解决问题。过程是事实+规则=结果。
    关于这个问题也有一些争议,有人把函数式归结为声明式的子集,还有一些别的七七八八的东西,这里就不做阐述了。
    声明式编程:专注于”做什么”而不是”如何去做”。在更高层面写代码,更关心的是目标,而不是底层算法实现的过程。
    如, css, 正则表达式,sql 语句,html,xml…
    在这儿来对比一下命令式和函数式的一些区别。

    命令式编程

    命令式编程是通过赋值(由表达式和变量组成,assignment,我觉得翻译成任务有点奇怪,也不懂翻译成赋值行不行)以及控制结构(如,条件分支、循环语句等)来修改可修改的变量。

    它的运作方式具有图灵机特性,且和冯诺依曼体系的对应关系非常密切。甚至可以说,命令式程序就是一个冯诺依曼机的指令序列:

    变量 →
    内存
    变量引用 →
    输入设备
    变量赋值 →
    输出设备
    控制结构 →
    跳转操作
    我们知道的,冯诺依曼结构需要用总线来传输数据,我们只能一个字节一个字节处理。

    这也就形成了运行的瓶颈。程序执行的效率取决于执行命令的数量,因此才会出现大O表示法等等表示时间空间复杂度的符号。

    函数式编程

    相比于命令式编程关心解决问题的步骤,函数式编程是面向数学的抽象,关心数据(代数结构)之间的映射关系。函数式编程将计算描述为一种表达式求值。

    在狭义上,函数式编程意味着没有可变变量,赋值,循环和其他的命令式控制结构。即,纯函数式编程语言。

    Pure Lisp, XSLT, XPath, XQuery, FP
    Haskell (without I/O Monad or UnsafPerformIO)
    在广义上,函数式编程意味着专注于函数

    Lisp, Scheme, Racket, Clojure
    SML, Ocaml, F#
    Haskell (full language)
    Scala
    Smalltalk, Ruby
    函数

    函数式编程中的函数,这个术语不是指命令式编程中的函数(我们可以认为C++程序中的函数本质是一段子程序Subroutine),而是指数学中的函数,即自变量的映射(一种东西和另一种东西之间的对应关系)。也就是说,一个函数的值仅决定于函数参数的值,不依赖其他状态。

    在函数式语言中,函数被称为一等函数(First-class function),与其他数据类型一样,作为一等公民,处于平等地位,可以在任何地方定义,在函数内或函数外;可以赋值给其他变量;可以作为参数,传入另一个函数,或者作为别的函数的返回值。

    纯函数

    纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

    不依赖外部状态
    不改变外部状态
    比如Javascript里slice和splice,这两个函数的作用相似[5]。
    slice符合纯函数的定义是因为对相同的输入它保证能返回相同的输出。
    splice却会嚼烂调用它的那个数组,然后再吐出来;这就会产生可观察到的副作用,即这个数组永久地改变了。

    var xs = [1,2,3,4,5];

    // 纯的
    xs.slice(0,3);
    //=> [1,2,3]
    xs.slice(0,3);
    //=> [1,2,3]
    xs.slice(0,3);
    //=> [1,2,3]


    // 不纯的
    xs.splice(0,3);
    //=> [1,2,3]
    xs.splice(0,3);
    //=> [4,5]
    xs.splice(0,3);
    //=> []
    在函数式编程中,我们讨厌这种会改变数据的笨函数。我们追求的是那种可靠的,每次都能返回同样结果的函数,而不是像splice这样每次调用后都把数据弄得一团糟的函数,这不是我们想要的。

    来看看另一个例子。

    // 不纯的
    var minimum = 21;

    var checkAge = function(age) {
    return age >= minimum;
    };


    // 纯的
    var checkAge = function(age) {
    var minimum = 21;
    return age >= minimum;
    };

    在不纯的版本中,checkAge的结果将取决于minimum这个可变变量的值。换句话说,它取决于系统状态(system state)。

    除了输入值之外的因素能够左右checkAge的返回值,不仅让它变得不纯,而且会增加程序的认知难度(cognitive load),使我们理解整个程序的时候都非常痛苦。甚至,这种依赖状态会影响系统的复杂度(http://www.curtclifton.net/storage/papers/MoseleyMarks06a.pdf)。

    函数与方法

    当然在现在很多(非纯)函数式编程语言中也有方法和函数的区别。比如scala[1]:

    scala> def m(x:Int) = 2*x //定义一个方法
    m: (x: Int)Int

    scala> m //方法不能作为最终表达式出现
    <console>:9: error: missing arguments for method m;
    follow this method with '_' if you want to treat it as a partially applied function
    m
    ^

    scala> val f = (x:Int) => 2*x //定义一个函数
    f: Int => Int = <function1>

    scala> f //函数可以作为最终表达式出现
    res9: Int => Int = <function1>

    方法就是命令式编程中的函数,而函数则是函数式编程中的函数。

    变量与表达式

    纯函数式编程语言中的变量也不是命令式编程语言中的变量(存储状态的内存单元),而是数学代数中的变量,即一个值的名称。

    变量的值是不可变的(immutable),也就是说不允许像命令式编程语言中那样能够多次给一个变量赋值。比如说在命令式编程语言我们写x = x + 1。

    函数式语言中的条件语句,循环语句等也不是命令式编程语言中的控制语句,而是一种表达式。

    “表达式”(expression)是一个单纯的运算过程,总是有返回值;
    “语句”(statement)是执行某种操作(更多的是逻辑语句。),没有返回值。
    函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。比如在Scala语言中,if else不是语句而是三元运算符,是有返回值的。

    严格意义上的函数式编程意味着不使用可变的变量,赋值,循环和其他命令式控制结构进行编程。 当然,很多所谓的函数式编程语言并没有严格遵循这一类的准则,只有某些纯函数式编程语言,如Haskell等才是完完全全地依照这种准则设计的。

    状态

    首先要意识到,我们的程序是拥有“状态”的。 想一想我们调试C++程序的时候,经常会在某处设置一个断点。程序执行断点就暂停了,也就是说程序停留在了某一个状态上。

    这个状态包括了当前定义的全部变量,以及一些当前系统的状态,比如打开的文件、网络的连接、申请的内存等等。具体保存的信息和语言有关系。

    比如使用过Matlab、R之类的科学计算语言的朋友会知道,在退出程序的时候它会让你选择是否要保存当前的session,如果保存了,下次打开时候它会从这个session开始继续执行,而不是清空一切重来。你之前定义了一个变量x = 1,现在这个x还在那里,值也没变。

    这个状态就是图灵机的纸带。有了状态,我们的程序才能不断往前推进,一步步向目标靠拢的。

    函数式编程不一样。函数式编程强调无状态,不是不保存状态,而是强调将状态锁定在函数的内部,不依赖于外部的任何状态。更准确一点,它是通过函数创建新的参数或返回值来保存程序的状态的。

    状态完全存在栈上。

    函数一层层的叠加起来,其中每个函数的参数(是参数,不是变量)或返回结果来代表了程序的一个中间状态。如果你需要保存一个状态一段时间并且时不时的修改它,那么你可以编写一个递归函数。

    举个例子,试着写一个计算斐波那契数的函数:

    // C++
    int fab(int n) {
    return n == 1 || n == 2 ? 1 : fab(n - 1) + fab(n - 2);
    }

    -- Haskell
    fab :: (Num a) => a -> a
    fab n = if n == 1 || n == 2 then 1 else fab(n - 1) + fab(n - 2)

    对于C++而言,调用fab(5)的过程是:

    fab(5) →
    fab(4) →
    fab(3) →
    fab(2)
                    →
    fab(1)
                 →
    fab(2)
              →
    fab(3) →
    fab(2)
                 →
    fab(1)

    我们看到,fab(3)被求值了两遍。为了计算fab(5),我们实际执行了8次函数调用。

    对于Haskell而言,如果没有使用尾递归,那么情况和C++的调用方式相同。如果使用了尾递归优化,调用fab(5)的过程是:

    fab(5) →
    fab(4) →
    fab(3) →
    fab(2)
                    →
    fab(1)
                 →
    fab(2)
              →
    fab(3)

    总共只有6次应用。注意我说的是应用而不是调用。因为函数式语言里的函数本意并不是命令式语言里面的“调用”或者“执行子程序”的语义,而是“函数与函数之间的关系”的意思。比如fab函数中出现的两次fab的应用,实际上说明要计算fab函数,必须先计算后续的两个fab函数。这并不存在调用的过程。因为所有的计算都是静态的。Haskell可以认为所有的fab都是已知的。因此实际上所有遇到的fab函数,Haskell只是实际地计算一次,然后就缓存了结果。

    函数式编程的特性

    高阶函数(Higher-order function)
    偏应用函数(Partially Applied Functions)
    柯里化(Currying)
    闭包(Closure)
    ---------------------
    作者:SuPhoebe
    来源:CSDN
    原文:https://blog.csdn.net/u013007900/article/details/79104110
    版权声明:本文为博主原创文章,转载请附上博文链接!

  • 相关阅读:
    Javascript 获得数组中相同或不同的数组元素
    JS 获取(期号、当前日期、本周第一天、最后一天及当前月第一、最后天函数)
    Intellij IDEA2020.2.3最新激活码激活破解方法(2020.11.26)
    【jQuery 区别】.click()和$(document).on("click","指定的元素",function(){});的区别
    pytorch repeat 和 expand 函数的使用场景,区别
    python小技巧
    提高GPU利用率
    pyinstaller 打包文件(包括使用管理员模式)
    frp 开机自启动
    AUC指标深度理解
  • 原文地址:https://www.cnblogs.com/feng9exe/p/11252205.html
Copyright © 2011-2022 走看看