美文网首页
函数式编程

函数式编程

作者: 我竟无言以对_1202 | 来源:发表于2018-05-30 22:33 被阅读0次

    函数式编程不是用函数来编程,旨在将复杂的函数符合成简单的函数。

    1.函数是一等公民。所谓”第一等公民”(first class),指的是函数 与其他数据类型一样,处于平等地位,可以赋值给其他变量,也 可以作为参数,传入另一个函数,或者作为别的函数的返回值。

    2..不可改变量。在函数式编程中,我们通常理解的变量在函数式 编程中也被函数代替了:在函数式编程中变量仅仅代表某个表达 式。这里所说的’变量’是不能被修改的。所有的变量只能被赋一次 初值 。

    3.map & reduce他们是常用的函数式编程的方法。

    函数式编程常用核心概念:

    1.纯函数

      对于相同的输入,永远会得到相同的输出,而且没有任 何可观察的副作用,也不依赖外部环境的状态。

    var x = [1,2,3,4,5,6];
    console.log(x.slice(0,3));  //[1,2,3]
    console.log(x.slice(0,3));  //[1,2,3]
    console.log(x.splice(0,3)); //[1,2,3]
    console.log(x.splice(0,3)); //[4,5,6]
    

    对于上述例子,slice就是一个纯函数,而splice就不是,因为两次得到的结果不同。

    //不纯的
    var min = 18;
    var checkage = age => age>min;
    
    //纯的
    var checkage = age => age>18;
    

    上面的例子,不纯是因为依赖了外部变量min。
      但是上面的纯函数有个问题就是18被写死了,这样函数的扩展性就比较差,下面我们用柯里化来解决这个问题。

    2.函数的柯里化

      柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

    对于前面说的问题,现用柯里化解决

    var checkage = min => (age => age>min);
    var checkage18 = checkage(18);
    console.log(checkage18(20));    //true
    
    // 上面的函数等价于
    function checkage(min){
       return function(age){
           return age>min
        }
    }
    console.log(checkage(18)(20));
    
    

      事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数, 得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种 对参数的“缓存”,是一种非常高效的编写函数的方法。

    3.函数组合

      纯函数以及如何把它柯里化写出的洋葱代码 h(g(f(x))), 为了解决函数嵌套的问题,我们需要用到“函数组合”:

    const compose = (f,g) => (x =>f(g(x)));
    var first = arr => arr[0];
    var reverse = arr => arr.reverse();
    var last = compose(first,reverse);
    console.log(last([1,2,3,4,5]));    //5
    
    //第一行代码等价于
    function compose(f,g){
        return function(x){
            return f(g(x));
        }
    }
    
    4.point Free

      point Free把一些对象自带的方法转化成纯函数,不要命名转瞬即逝 的中间变量。

    const compose = (f,g) => (x => f(g(x)));
    var toUpperCase = word =>word.toUpperCase();
    var split = x => (str => str.split(x));
    
    var f = compose(split(''),toUpperCase);
    console.log(f("abcd efgh"));    // ["A", "B", "C", "D", " ", "E", "F", "G", "H"]
    
    5.声明式与命令式代码

       命令式代码的意思就是,我们通过编写一条又一条指令去让计算 机执行一些动作,这其中一般都会涉及到很多繁杂的细节。而声 明式就要优雅很多了,我们通过写表达式的方式来声明我们想干 什么,而不是通过一步一步的指示。

     //命令式 
    let ceos= [];
    for(var i = 0; i < companies.length; i++){   
        ceos.push(companies[i].ceo) 
    } 
    
    //声明式 
    let ceos = companies.map(c => c.ceo);
    

       函数式编程的一个明显的好处就是这种声明式的代码,对 于无副作用的纯函数,我们完全可以不考虑函数内部是如何实 现的,专注于编写业务代码。优化代码时,目光只需要集中在 这些稳定坚固的函数内部即可。
       相反,不纯的函数式的代码会产生副作用或者依赖外部系 统环境,使用它们的时候总是要考虑这些不干净的副作用。在 复杂的系统中,这对于程序员的心智来说是极大的负担。

    6.惰性求值、惰性函数

       在指令式语言中以下代码会按顺序执行,由于每个函数 都有可能改动或者依赖于其外部的状态,因此必须顺序 执行。
      一旦我们接纳了函数式哲学,惰性(或延迟)求值这一技术会变得非常有趣。在讨论并行时已经见过下面的代码片断:

    String s1 = somewhatLongOperation1();
    
    String s2 = somewhatLongOperation2();
    
    String s3 = concatenate(s1, s2);
    

       在一个命令式语言中求值顺序是确定的,因为每个函数都有可能会变更或依赖于外部状态,所以就必须有序的执行这些函数:首先是 somewhatLongOperation1,然后 somewhatLongOperation2,最后 concatenate,在函数式语言里就不尽然了。
       前面提到只要确保没有函数修改或依赖于全局变量,somewhatLongOperation1 和 somewhatLongOperation2 可以被并行执行。
       假设我们不想并行运行这两个函数,那是不是就按照字面顺序执行他们好了呢?答案是否定的,我们只在其他函数依赖于 s1 和 s2 时才需要执行这两个函数。我们甚至在 concatenate 调用之前都不必执行他们——可以把他们的求值延迟到 concatenate 函数内实际用到他们的位置。
       如果用一个带有条件分支的函数替换 concatenate 并且只用了两个参数中的一个,另一个参数就永远没有必要被求值。在 Haskell 语言中,不确保一切都(完全)按顺序执行,因为 Haskell 只在必要时才会对其求值。

    7.高阶函数

       函数当参数,把传入的函数做一个封装,然后返回这个封装 函数,达到更高程度的抽象。

    //命令式
    var add = function(a,b){
        return a+b;
    }
    function math(func,array){
        return func(array[0],array[1]);
    }
    math(add,[1,2]);    //3
    
    8.尾调用优化

      指函数内部的后一个动作是函数调用。该调用的返回值,直接返回给函数。。 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。递归需要保存大 量的调用记录,很容易发生栈溢出错误,如果使用尾递归优化,将递归变为循 环,那么只需要保存一个调用记录,这样就不会发生栈溢出错误了。

    //不是尾递归
    function sum(n){
        if(n===1){
            return 1;
        }else{
            n+sum(n-1);
        }
    }
    

    其运算过程如下:

    sum(5)
    (5+sum(4))
    (5+(4+sum(3)))
    (5+(4+(3+sum(2))))
    (5+(4+(3+2+sum(1))))
    (5+(4+(3+(2+1))))
    (5+(4+(3+3)))
    (5+(4+6))
    (5+10)
    15

    //细数尾递归
    function sum(x,total){
        if(x === 1){
            return x+toal;
        }else{
            return sum(x-1,x+total);
        }
    }
    

    其运算过程如下:

    sum(5,0)
    sum(4,5)
    sum(3,9)
    sum(2,12)
    sum(1,14)
    15

      整个计算过程是线性的,调用一次sum(x, total)后,会进入下一个栈,相关的数据信息和 跟随进入,不再放在堆栈上保存。当计算完后的值之后,直接返回到上层的 sum(5,0)。这能有效的防止堆栈溢出。 在ECMAScript 6,我们将迎来尾递归优化,通过尾递归优化,javascript代码在解释成机器 码的时候,将会向while看起,也就是说,同时拥有数学表达能力和while的效能。

    9.闭包

      闭包就是能够读取其他函数内部变量的函数

    10.范畴与容器

      我们可以把”范畴”想象成是一个容器,里面包含两样东西。值 (value)、值的变形关系,也就是函数。
      范畴论使用函数,表达范畴之间的关系
      伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法 起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今 天的”函数式编程"。
       本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、 行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。为 什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种 数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数 运算法则了。
      函数不仅可以用于同一个范畴之中值的转换,还可以用于将 一个范畴转成另一个范畴。这就涉及到了函子(Functor)。
      函子是函数式编程里面重要的数据类型,也是基本的运算 单位和功能单位。它首先是一种范畴,也就是说,是一个容 器,包含了值和变形关系。比较特殊的是,它的变形关系可 以依次作用于每一个值,将当前容器变形成另一个容器

    当下函数式编程比较火热的库

    RXJS*
    cycleJS
    lodashJS、lazy*
    underscoreJS
    ramdaJS*

    资料:https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/

    相关文章

      网友评论

          本文标题:函数式编程

          本文链接:https://www.haomeiwen.com/subject/mvztsftx.html