函数式编程

栏目: 编程语言 · 发布时间: 5年前

内容简介:总结:函数式编程不是指用函数编程函数式编程是指用一些数学的方式加上js的语法进行的编程 函数式编程里面没有if else while等语句 以及变量的概念
  • 函数式编程是范畴论的数学分支,是一门复杂的数学,认为世界上所有的概念体系都可以抽象出一个个范畴论。
  • 成员彼此存某种关系概念、事物、对象等等,解构成范畴。任何事物只要找出他们之间的关系,就能定义成为范畴
  • 反应范畴成员之间的关系叫做 态射 ,范畴论认为, 同一个范畴的所有成员通过“态射”,一个成员可以变形为另外一个成员

总结:

函数式编程不是指用函数编程

函数式编程是指用一些数学的方式加上js的语法进行的编程 函数式编程里面没有if else while等语句 以及变量的概念

函数式编程特性

  • 函数式一等公民。所谓“第一等公民”,指的式函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另外一个函数,或者作为别的函数返回值。
  • 不可改变量。在函数式编程中,我们通常理解的变量在函数式编程中也被函数替代了;在函数式编程中变量仅仅代表某个表达式。这里所说的“变量”是不能被修改的。所有的变量只能被赋一次初值
  • map & reduce 他们是常用的函数式编程的方法。

总结:

  1. 函数是‘第一等公民’
  2. 只有‘表达式’,不用‘语句’
  3. 没有“副作用”
  4. 不修改状态
  5. 引用透明(函数运行只靠参数)

##纯函数

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

例如:

let xs = [1,2,3,4,5];
//Array.slice是纯函数,因为他没有副作用,对于固定的输入,输出总是固定的
slice  √
xs.slice(0,3);
xs.slice(0,3);
//总是得到的输出是 [1,2,3,4]

splice ×
xs.splice(0,3);//[1,2,3]  
xs.splice(0,3);//[4,5]
复制代码

优缺点

  • 优点 纯函数不仅可以有效的降低系统的复杂度,还有很多很棒的特性,例如可缓存性
  • 缺点 例如
    //不纯的
      var min = 18;
      var checkage = age => age > min;
    
      //纯的
      var checkage = age => age > 18;
    复制代码
    在不纯的版本中,checkage不仅取决于参数age 还有外部依赖的变量 min。 纯的checkage 把关键字18 硬性 编码在函数内部,扩展性较差,所以就引出了函数柯里化来优化这一问题

函数的柯里化

传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

例 Ⅰ 柯里化上面的 checkage函数

var checkage = min => (age => age >min);
checkage(18)(20);//true
复制代码

例 Ⅱ

//柯里化之前
function add(x,y){
    return x + y;
}
add(1,2) //3

//柯里化之后
function addX(y){
    return function(x){
        return x + y;
    }
}
addX(2)(1) //3
复制代码

例 III 利用bind 传递部分参数 实现柯里化

function foo(p1,p2){
    this.val = p1 + p2;
}
var bar = foo.bind(null,'p1');
var baz = new bar('p2');
console.log(baz.val);
复制代码

函数的组合

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

const compose = (f,g) => f(x => f(g(x)));
var first = arr => arr[0];
var reverse = arr => arr.reverse();
var last = compose(first, reverse);
last([1,2,3,4,5]);  //5
复制代码

Point Free

  • 把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量
  • 例如 如下函数,我们使用str作为我们中间的变量,但是这个中间变量除了让代码变得长了一点之外是毫无意义的。
const f = str => str.toUpperCase().split(' ');
复制代码

转化为如下风格

var toUpperCase = word => word.UpperCase();
    var split = x => (str => str.split(x));

    var f = compose(split(' '),toUpperCase);

    f("abcd efgh");
复制代码

这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。

声明式与命令式代码

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

//命令式:
    let CEOs = [];
    for(var i = 0; i < companies.length;i++){
        CEOs.push(companies[i].CEO);
    }

    //声明式
    let CEOs = companies.map(c => c.CEO);
复制代码

优缺点

函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部式如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。
复制代码

相反,不纯的函数式的代码会产生副作用或者依赖外部系统环境,使用它们的时候总要考虑这些 不干净的副作用。在复杂的系统种,这对于程序猿 的心智来说是极大的负担。

惰性求值、惰性函数

在指令式语言中以下代码会按顺序执行,由于每个函数都有可能改动或者依赖于其外部的状态,因此必须顺序执行。

function somewhatlongOperation(){somewhatlongOperation}
复制代码

高阶函数

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

//命令式
var add = function(a,b){
    return a + b;
};
function math(func,array){
    return func(array[0],array[1]);
};
math(add,[1,2]);   //3 
复制代码

尾调用优化

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

function factorial(n){
    if(m === 1) return 1;
    return n * factorial(n -1);
}
function factorial(n,total){
    if(n === 1) return total;
    return factorial(n-1,n * total)
} //ES6强制使用尾递归
复制代码

console.trace() 查看调用帧

闭包

缓存了当前上下文执行环境的词法作用域

范畴与容器

  • 函数不仅可以用于统一范畴之中值的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子( Functor

  • 函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依此作用于每一个值,将当前容器变形成为另一个容器。

函子 Functor

  • Functor 遵守一些特定规则的容器类型。
  • Functor 是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力。把东西装进一个容器,只留出一个接口map给容器外的函数,map一个函数时,我们让容器自己来运行这个函数,这样容器就可以自由地选择何时何地如何操作这个函数,以致于拥有惰性求值,错误处理,异步调用等等非常牛掰的特性。
var Container = function(x){
    this.__value = x;
}
//函数式编程一般约定,函子有一个 of 方法
Container.of = x => new Container(x);
//Container.of('abcd');
//一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。

Container.prototype.map = function(f){
    return Container.of(f(this.__value))
}

Container.of(3)
    .map(x => x + 1)        //结果为  Container(4)
    .map(x => 'Result is' + x); //结果为 Container('Result is 4')
复制代码

map

class Functor{
    constructor(val){
        this.val = val;
    }
    
    map(f){
        return new Functor(f(this.val));
    }
}

(new Functor(2)).map(function(tow){
    return tow + 2;
})

//Functor(4)
复制代码

上面代码中,Functor是一个函子,它的map方法接受函数f作为参数,然后返回一个新的函子,里面包含的值是被f处理过的(f(this.val))。 一般约定,函子的标志就是容器具有map方法。该方法将容器里的每一个值,映射到另一个容器。 上面的例子说明,函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器---函子。函子本身具有对外接口(map方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。

总结:

首先 函子 是一个容器 它特殊在 有map 方法 ,通过map方法 接受一个变形关系,映射出另外一个函子。编程面向不直接操作值,而是通过操作函数,将原来的集合 转化为一个新的集合

of方法

函数式编程一般约定,函子有一个of方法,用来生成新的容器。 所以上面代码改写成

class Functor{
    constructor(val){
        this.val = val;
    }
    static of(val){
        return new Functor(val);
    }
    map(f){
        return new Functor(f(this.val));
    }
}

Functor.of(2).map(function(tow){
    return tow + 2;
})

//Functor(4)
复制代码

maybe 函子 (函数式编程中的 if else)

函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如null), 而外部函数未必又处理空值的机制,如果传入空值,很可能就会报错。

例如如下代码 就熄火了

Functor.of(null).map(function(s){
    return s.toUpperCase();
})

class Maybe extends Functor{
    map(f){
        return this.val?Maybe.of(f(this.val)):Maybe.of(null);
    }
}
Maybe.of(null).map(function(s){
    return s.toUpperCase();
})
//Maybe(null)
复制代码

如何实现 maybe 函子

var Maybe = function(x){
    this.__value = x;
}
Maybe.of = function(x){
    return new Maybe(x);
}
Maybe.prototype.map = function(f){
    return this.isNothing()?Maybe.of(null):Maybe.of(f(this.__value));
}
Maybe.prototype.isNothing = function(){
    return (this.__value === null || this.__value === undefined);
}
复制代码

错误处理、Either

  • 我们的容器能做的事情太少了,try/catch/throw 并不是“纯”的,因为它从外部接管了我们的函数,并且在这个函数出错时抛弃了它的返回值。
  • Promise是可以调用catch来集中处理错误的
  • 事实上Either并不只是用来做错误处理的,它表示了逻辑或与,范畴学里的coproduct.

Either的实现

条件运算符if..else是最常见的运算之一,函数式编程里面,使用Either函子表达。Either函子内部又两个值:左值(left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在的时候使用的默认值。

class Either extends Functor{
    constructor(left,right){
        this.left = left;
        this.right = right;
    }
    static of(left,right){
        return new Either(left,right);
    }
    map(f){
        return this.right?
            Either.of(this.left,f(this.right)):
            Either.of(f(this.left),this.right);
    }
}

var addOne = function(x){
    return x + 1;
}

Either.of(5,6).map(addOne);
//Either(5,7);

Either.of(1,null).map(addOne);
//Either(2,null)

Either.of({address:'xxx'},currentUser.address).map(updateField);
复制代码

AP函子

函子里面包含的值,完全可能是 函数。我们可以想象这样一个情况,一个函子的值是数值,另一个函子的值是函数。

class Ap extends Functor{
    ap(F){
        return Ap.of(this.val(F.val));
    }
}
Ap.of(addTwo).ap(Functor.of(2));
复制代码

IO函子

  1. 真正的程序总要去接触肮脏的世界。
    function readLocalStorage(){
         return window.localStorage;
     }
    复制代码
  2. IO跟前面那几个Functor不同的地方在于,它的__value是一个函数。它把不纯的操作(比如IO、网络请求、DOM)包裹到一个函数内,从而延迟这个操作的执行。所以我们认为,IO包含的是被包裹的操作的返回值
  3. IO其实也算是惰性求值
  4. IO负责了调用链积累了很多很多不纯的操作,带来的复杂性和不可维护性
import _ from 'lodash';
var compose = _,flowRight;

var IO = function(f){
    this.__value = f;
}

IO.of = x => new IO(_=>x);

IO.prototype.map = function(f){
    return new IO(compose(f,this.__value))
};

//ES6 写法
import _ from 'lodash';
var compose = _.flowRight;
class IO extends Monad{
    map(f){
        return IO.of(compose(f,this.__value))
    }
}
复制代码
var fs = require('fs');
var readFile = function(filename){
   return new IO(function(){
       return fs.readFileSync(filename,'utf-8';)
   });
};

readFile('./user.txt')
.flatMap(tail)
.flatMap(print)

//等同于
readFile('./user.txt')
.chain(tail)
.chain(print)
复制代码

Monad

  • Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相链接的多个步骤。你只要提供下一步运算所需要的函数。整个运算就会自动执行下去
  • Promise就是一种Monad。
  • Monad让我们避开了嵌套式地狱,可以轻松地进行深度嵌套的函数式编程,比如IO和其他异步任务。
  • 记得让IO继承Monad.
class Monad extends Functor{
    join(){
        return this.val;
    }
    flatMap(f){
        return this.map(f).join();
    }
}
复制代码

Monad函子的作用是,总是返回一个单层的函子。它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会去除后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。 如果 函数 f 返回的是一个函子,那么this.map(f)就会生成一个嵌套的函子。所以,join方法保证了flatMap方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平。

总结: 这里不是很明白 如何结合IO???


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数据结构与算法分析

数据结构与算法分析

Frank.M.Carrano / 金名 / 清华大学出版社 / 2007-11 / 98.00元

“数据结构”是计算机专业的基础与核心课程之一,Java是现今一种热门的语言。本书在编写过程中特别考虑到了面向对象程序设计(OOP)的思想与Java语言的特性。它不是从基于另一种程序设计语言的数据结构教材简单地“改编”而来的,因此在数据结构的实现上更加“地道”地运用了Java语言,并且自始至终强调以面向对象的方式来思考、分析和解决问题。 本书是为数据结构入门课程(通常课号是CS-2)而编写的教......一起来看看 《数据结构与算法分析》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试