函数式编程

函数式编程

 - 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
 - 我们可以把一个函数的执行结果交给另一个函数去处理
 - 函数是一等公民
 - 高阶函数-函数作为参数
 - 高阶函数-函数作为返回值
   
纯属函数的概念
纯函数: 相同的输入永远会得到相同的输出,  而且没有任何可观察的副作用
* 纯函数就类似数学中的函数(用来描述输入和输出之间的关系), y = f(x);
举例:
数组中的 slice 和 splice 分别是纯函数和不纯函数
* slice 返回数组中的指定部分, 不会改变原数组
* splice 对数组进行操作返回该数组,会改变原数组

好处: 
    可缓存
        因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
    可测试
        纯函数让测试更方便
    并行处理
        在多线程环境下并行操作共享的内存数据很可能会意外情况
        纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker)
lodash 中有个记忆函数
_.memoize

// 模拟memoize方法的实现
function memoize (f) {
    let cache = {};
    return function () {
        let key = JSON.stringify(arguments);
        cache[key] = cache[key] || f.apply(f, arguments);
        return cache[key]
    }
}

函数的副作用

副作用让一个函数变的不纯,纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。

副作用来源:
 - 配置文件
 - 数据库
 - 获取用户的输入
 - ......
所有的外部交互都有可能代理副作用,副作用也使得方法能用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患 给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。

柯里化 (Haskell Brooks Curry)

  1. 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
  2. 然后返回一个新的函数接收剩余的参数,返回结果
function checkAge (age) {
    let min = 18;
    return age >= min;
}
  • 使用柯里化解决上一个案例中硬编码的问题
// 柯里化
function checkAge (min) {
    return function (age) {
        return age >= min;
    }
}
// es6
let checkAge = min => (age => age >= min);

lodash中的柯里化函数
  • _.curry(func)
  • 功能:创建一个函数,该函数接收一个或多个func的参数,如果func所需要的参数都被提供则执行func并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
  • 参数:需要柯里化的函数
  • 返回值:柯里化后的函数
const _ = require('lodash')
// 要柯里化的函数
function getSum (a, b, c) {
    return a + b + c;
}
// 柯里化后的函数
let curried = _.curry()


手写curry



function getSum(a, b, c) {
  return a + b + c;
}

const curried = curry(getSum);

console.log(curried(1, 2, 3));
console.log(curried(1)(2, 3));
console.log(curried(1, 2)(3));

function curry (func) {
  return function curriedFn (...args) {
    // 判断实参和形参的个数
    if (args.length < func.length) {
      return function() {
        // 注意:这里有些博客用的 arguments.callee
        // 这个在es5的严格模式下禁止了
        // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/arguments/callee
        return curriedFn(...args.concat(Array.from(arguments)));
      }
    }
    return func(...args);
  }
}
柯里化总结
  • 柯里化可以让我闪给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
  • 这是一种对函数参数的'缓存'
  • 让函数变得更灵活,让函数的粒度更小
  • 可以把多无函数转换成一元函数,可以组合使用函数产生强大的功能

函数组合

  • 纯属函数和柯里化很容易写出洋葱代码 h(g(f(x)))
    • 获取数组的最后一个元素再转换成大写字母 .toUpper(.first(_.reverse(array)))
  • 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
函数组合先了解一下 管道

a -> fn(也可以是多个管理-多个函数) -> b

fn = compose(f1, f2, f3);
b = fn(a);
函数组合定义
  • 函数组合(compose): 如果一个函数要经过多个函数处理才能得到最终值,这个时候嗯可以把中间过程的函数合并成一个函数
    • 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
    • <font color="red">函数组合默认是从右到左执行</font>
// 组合函数
function compose (f, g) {
   return function (x) {
       return f(g(x))
   }
}

lodash 中的组合函数
  • lodash中组合函数flow() 或者 flowRight(), 他们都是可以组合多个函数
  • flow() 是从左到右运行
  • flowRight() 是从右到左运行,使用的更多一些
// 模拟 lodash  中的 flowRight

const reverse = arr => arr.reverse();
const first = arr => arr[0];
const toUpper = s => s.toUpperCase();

const f = compose(toUpper, first, reverse);
console.log(f(['one', 'two', 'three']))

function compose (...args) {
   return function (value) {
       return args.reverse().reduce(function (acc, fn){
           return fn(acc);
       }, value)    
   }
}

// es6
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value);

函数的组合要满足 组合律(associativity):
  • 我们既可以把g和h组合,还可以把f和g组合,结果都是一样的。
// 结合律 (associativity)
let f = compose(f, g, h);
let associative = compose(compose(f, g), h) == compose((f, compose(g, h));


函数组合 调试

  // 举例
  // NEVER SAY DIE  --> never-say-die
  const _ = require('lodash')
  
  // _.split()
  const split = _.curry((sep, str) => _.split(str, sep))
  
  // _.toLower()
  const join = _.curry((sep, array) => _.join(array, sep))
  
  const map = _.curry((fn, array) => _.map(array, fn))
  const f = _.flowRight(join('-'), map(_.toLower), split(' '))
  
  console.log(f('NEVER SAY DIE'))

lodash/fp 模块

  • lodash 的 fp 模块提供了实用的对 函数式编程友好 的方法
  • 提供了不可变 auto-curried iteratee-first data-last 的方法 (函数优先,数据之后)
// lodash 模块
const _ = require('lodash')

_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C']
_.map(['a', 'b', 'c'])
// => ['a', 'b', 'c']

_.split('Hello World', ' ')


// lodash/fp 模块

const fp = require('lodash/fp')

fp.map(fp.toUpper, ['a', 'b', 'c'])
fp.map(fp.toUpper)(['a', 'b', 'c'])

fp.split(' ', 'Hello World')
fp.split(' ')('Hello World')


// 举例
/ NEVER SAY DIE  --> never-say-die
const fp = require('lodash/fp')

const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE'))

lodash 和 lodash/fp 模块中 map 方法的区别
const _ = require('lodash')
console.log(_.map(['23', '8', '10', parseInt))
// => [23, NaN, 2]
// 剖析
// 进制 2-36  0就是没传,默认10进制
// 1. parseInt('23', 0, array)
// 2. parseInt('8', 1, array)
// 3. parseInt('10', 2, array)


Point Free

  • "Point Free": 我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
    • 不需要指明处理的数据
    • 只需要合成运算过程
    • 需要定义一些辅助的基本运算函数
    const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), fp.split(' '))
    
    • 案例演示
    // 非 Point Free 模式
    // Hello World => hello_world
    function f (word) {
        return word.toLowerCase().replace(/\s+/g, '_')
    }
    
    // Point Free
    const fp = require('lodash/fp')
    
    const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
    
    console.log(f('Hello World'))
    
    
    // 把一个字符串中的首字母提取并转换成大写,使用. 作用分隔符
    // world wild web ==> W. W. W
    
    const fp = require('lodash/fp')
    
    const firstLetterToUpper = fp.flowRight(fp.join('. ') ,fp.map(fp), fp.split(' '))
    
    // 改进
    const firstLetterToUpper = fp.flowRight(fp.join('. ') ,fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
    
    console.log(firstLetterToUpper('world wild web'))
    

Functor (函子)

  • 为什么要学函子
到目前为止已经学习了函数式编程的一些基础,但是我们还没有演示在函数式编程中如何把副作用控制在可控的范围内、异常处理、异步操作等。

  • 什么是Functor
    • 容器:包含值和值的变形关系(这个变形关系就是函数)
    • 函子:是一个特殊的容器,通过一个普通 的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)

// Functor 函子
class Container {
  constructor(value) {
    this._value = value;
  }

  map(fn) {
    return new Container(fn(this._value));
  }
}


MayBe函子
  • 我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
  • MayBe函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)

class Maybe {

  static of(value) {
    return new Maybe(value)
  }

  constructor(value) {
    this._value = value
  }

  // 如果对宿舍变形的话直接返回 值为 null 的函子
  map(fn) {
    return this.isNothing() ? Maybe.off(null) : Maybe.of(fn(this._value))
  }

  isNothing() {
    return this._value === null || this._value === undefined
  }
}

// 传入具体值
// let r = Maybe.of('Hello World')
//           .map(x => x.toUpperCase())
// console.log(r)

let r = Maybe.of(null)
          .map(x => x.toUpperCase())
console.log(r)


Either函子
  • Either两者中的任何一个,类似于if...else...的处理
  • 异常会让函数变的不纯,Either函子可以用来做异常处理


class Left {

  static of(value) {
    return new Left(value)
  }

  constructor(value) {
    this._value = value
  }

  map(fn) {
    return this
  }
}



class Right {

  static of(value) {
    return new Right(value)
  }

  constructor(value) {
    this._value = value
  }

  map(fn) {
    return Right.of(fn(this._value))
  }
}


// let r1 = Right.of(12).map(x => x + 2)
// let r2 = Left.of(12).map(x => x + 2)

// console.log(r1)
// console.log(r2)

function parseJSON(str) {
  try {
    return Right.of(JSON.parse(str))
  } catch(e) {
    return Left.of({ error: e.message })
  }
}
// let r = parseJSON('{ name: zs }')
let r = parseJSON('{ "name": "zs" }').map(x => x.name.toUpperCase())
console.log(r)

IO函子
  • IO函子中的_value是一个函数,这里是把函数作为值来处理
  • IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
  • 把不纯属的操作交给调用者来处理


const fp = require('lodash/fp')

class IO {

  static of(value) {
    return new IO(function() {
      return value;
    })
  }

  constructor(fn) {
    this._value = fn;
  }

  map(fn) {
    return new IO(fp.flowRight(fn, this._value));
  }
}

// 调用
const r = IO.of(process).map(p => p.execPath);

console.log(r._value());

folktale
  • Task异步执行
    • 异步任务的实现过于复杂,我们使用folkatle中的Task来演示
    • folktale一个标准的函数式编程库
      • 和lodash、remda不同的是,他没有提供很多功能函数
      • 只提供了一些函数式处理的操作,例如:compose, curry等,一些函数Task、Either、MayBe等
// folktale 中的 compose、curry
const { compose, curry } = require('folktale/core/lamba')
const { toUpper, first } = require('lodash/fp')

// 第一个参数是传入函数的参数个数
let f = curry(2, function(x, y) {
  console.log(x + y)
})
f(3, 4)
f(3)(4)

// 函数组合
let f = compose(toUpper, first);
f(['one', 'two'])

Task函子
  • folktale(2.3.2)2.x中的TaskT 1.0中的Task区别很大,1.0中的用法更接近我们现在演示的函子
  • 这里以2.3.2来演示

const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')

function readFile(filename) {
    return task(resolver => {
      fs.readFile(filename, 'utf-8', (err, data) => {
      if (err) resolver.reject(err)
      resolver.resolve(data)
    })
  })
}
readFile('package.json')
    .map(split('\n'))
    .map(find(x => x.includes('version')))
    .run()
    .listen({
      onRejected: err => {
        console.log(err)
      },
      onResolved: value => {
        console.log(value)
      }
    })

Pointed函子
  • Pointed函子是实现了of静态方法的函子
  • of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Content(把值放到窗口中,使用map来处理值)
// 就是of函数 
class Container {

  static of(value) {
    return new Container(value)
  }

  constructor(value) {
    this._value = value;
  }

  map(fn) {
    return new Container(fn(this._value));
  }
}
Container.of(2)
  .map(x => x + 5)

IO函子的问题
  • Monad (单子)
    • 在使用IO函子的时候,如果我们写出如下代码

var fs = require('fs');
const fp = require('lodash/fp')

class IO {

  static of(value) {
    return new IO(function() {
      return value;
    })
  }

  constructor(fn) {
    this._value = fn;
  }

  map(fn) {
    return new IO(fp.flowRight(fn, this._value));
  }
}

let readFile = function(filename) {
  return new IO(function() {
    return fs.readFileSync(filename, 'utf-8')
  })
}

let print = function(x) {
  return new IO(function() {
    console.log(x)
    return x
  })
}

let cat = fp.flowRight(print, readFile)

// 嵌套问题
let r = cat('package.json')._value()._value();
console.log(r)

  • Monad函子是可以变扁的 Pointed函子,IO(IO(x))
  • 一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad

var fs = require('fs');
const fp = require('lodash/fp')

class IO {

  static of(value) {
    return new IO(function () {
      return value;
    })
  }

  constructor(fn) {
    this._value = fn;
  }

  map(fn) {
    return new IO(fp.flowRight(fn, this._value));
  }

  join() {
    return this._value()
  }

  flatMap(fn) {
    return this.map(fn).join()
  }
}

let readFile = function (filename) {
  return new IO(function () {
    return fs.readFileSync(filename, 'utf-8')
  })
}

let print = function (x) {
  return new IO(function () {
    console.log(x)
    return x
  })
}

let r = readFile('package.json')
  .flatMap(print)
  .join()

console.log(r)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容