函数式编程了解一下(上)

一直以来没有对函数式编程有一个全面的学习和使用,或者说没有一个深入的思考。最近看到一些博客文章,忽然以为函数式编程仍是蛮有意思的。看了些书和文章。这里记载下感悟和收获。 欢迎团队姜某人多多指点@姜少。 因为博客秉持着简短且全面原则。遂分为上下两篇javascript

原文地址 Nealyang前端

部分简介

函数式编程了解一下(上)

  • 入门简介
  • HOC简介
  • 函数柯里化与偏应用

函数式编程了解一下(下)

  • 组合与管道
  • 函子和Monad
  • 再回首Generator

入门简介

函数的第一原则是要小,函数的第二原则是要更小java

什么是函数式编程?为何他重要

在理解什么是函数式编程的开始,咱们先了解下什么数学中,函数具备的特性node

  • 函数必须老是接受一个参数
  • 函数必须老是返回一个值
  • 函数应该依据接受到的参数,而不是外部的环境运行
  • 对于一个指定的x,必须返回一个肯定的y

因此咱们说,函数式编程是一种范式,咱们可以以此建立仅依赖输入就能够完成自身逻辑的函数。这保证了当函数屡次调用时,依然能够返回相同的结果。所以能够产生可缓存的、可测试的代码库git

引用透明

全部的函数对于相同的输入都返回相同的结构,这一特性,咱们称之为引用透明。 好比:es6

let identity = (i) => {return i};
复制代码

这么简单?对,其实就是这样,也就是说他没有依赖任何外部变量、外部环境,只要你给我东西,我通过一顿鼓捣,老是给你返回你所能预测的结果。github

这也为咱们后面的并发代码、缓存成为可能。编程

命令式、声明式和抽象

函数式编程主张声明式编程和编写抽象代码。其实这个比较有意思,感受更像是面向对象的编程。redux

光说不练都是扯淡。举个栗子数组

var array = [1,2,3,4,5,6];
  for(let i = 0;i<array.length;i++){
    console.log(array[i])
  }
复制代码

这段代码的做用简单明了,就是遍历!可是你有没有感受这个代码呆呆的。没有一丁点的灵气?都是我告诉你该怎么该怎么作的。咱们告诉编译器,你先去获取下数组的长度的,而后挨个log出来。这种编码方式,咱们一般称之为“命令式”解决方案。

而在函数式编程中,咱们其实更加主张用“声明式”解决方案

let array = [1,2,3,4,5,6];
array.forEach(item=>{console.log(item)})
复制代码

简单体会下,是否是有那么一丢丢的灵感来了?等等,你这个forEach函数哪来的嘛!对,也是本身写的,可是不是咱们经过编写这种抽象逻辑代码,而让总体的业务代码更加的清晰明了了呢?开发者是须要关心手头上的问题就行了,只须要告诉编译器去干吗而不是怎么干了。是否是轻松了?

其实函数式编程主张的就是以抽象的方式建立函数。这些函数能够在代码的其余部分被重用。

函数式编程的好处

好处我的不喜欢扯太多,不是由于他没有好处,而是对于刚刚接触函数式编程的哥们,上来就说好处实际上是没什么概念的,因此这里我简单提一提,后面文章会细细说明。

纯函数 => 可缓存

熟悉redux的同窗应该对这个词语都不陌生,所谓的纯函数,其实也就是咱们说的引用透明,稳定输出!好处呢?可预测嘛,容易编写测试代码哇,可缓存嘛。什么是可缓存?能够看我以前发的文章哈,这里简单举个栗子

let longRunningFunction = (input)=>{
  //进行了很是麻烦的计算,而后返回出来结果
  return output;
}
复制代码

若是longRunningFunction是一个纯函数,引用透明。咱们就能够说对于一样的输出,老是返回一样的结果,因此咱们为何不可以运用一个对象将咱们每一次的运算结果存起来呢?

let longRunningFunctionResult = {1:2,2:3,3:4};
//检查key是否存在,存在直接用,不存在再计算
longRunningFunctionResult.hasOwnProperty(input)?longRunningFunctionResult[input]:longRunningFunctionResult[input] = longRunningFunction(input)
复制代码

比较直观。很少说了哈。其实好处还有以前说到的并发。不说的这么堂而皇之了,啥并不并发呀,我不依赖别人的任何因素,只依据你的输出我产出。你说我支持什么就是什么咯,只要你给我对的参数传进来就能够了。

结束语

匆匆收尾!仅做为抛砖引玉。后面我们在系统性的学习下函数式编程。

高阶函数(HOC)简介

概念

JavaScript做为一门语言,将函数视为数据。容许函数代替数据传递是一个很是强大的概念。接受一个函数做为参数的函数成为高阶函数(Higher-Order Function)

从数据入门HOC

JavaScript支持以下几种数据类型:

  • Number
  • String
  • Boolean
  • Object
  • null
  • undefined

这里面想强调的是JavaScript将函数也一样是为一种数据类型。当一门语言容许将函数做为数据那样传递和使用的时候,咱们就称函数为一等公民。

因此说这个就是为了强调说明,在JavaScript中,函数能够被赋值,做为参数传递,也能够被其余函数返回。

//传递函数
let tellType = (arg)=>{
  if(typeof arg === 'function'){
    arg();
  }else{
    console.log(`this data is ${arg}`)
  }
}

let dataFn = ()=> {
  console.log('this is a Function');
}

tellType(dataFn);
复制代码
//返回函数
let returnStr = ()=> String;

returnStr()('Nealyang')

//let fn = returnStr();
//fn('Nealyang');
复制代码

从上咱们能够看到函数能够接受另外一个函数做为参数,一样,函数也能够将两一个函数做为返回值返回。

因此高阶函数就是接受函数做为参数而且/或者返回函数做为输出的函数

HOC 到底你是干吗的

当咱们了解到如何去建立并执行一个高阶函数的时候,同行咱们都想去了解,他究竟是干吗的?OK,简单的说,高阶函数经常使用于抽象通用的问题。换句话说,高阶函数就是定义抽象。简单的说,其实就相似于命令式的编程方式,将具体的实现细节封装、抽象起来,让开发者更加的关心业务。抽象让咱们专一于预约的目标而不是去关心底层的系统概念。

理解这个概念很是重要,因此下面咱们将经过大量的栗子来讲明

举斤栗子

const every = (arr,fn)=>{
  let result = true;
  for(const value of arr){
    result  = result && fn(value);
  }
  return result;
}

every([NaN,NaN,4],isNaN);

const some = (arr,fn)=>{
  let result = true;
  for(const value of arr){
    result  = result || fn(value);
  }
  return result;
}
some([3,1,2],isNaN);
//这里都是低效的实现。这里主要是理解高阶函数的概念
复制代码
let sortObj = [
  {firstName:'aYang',lastName:'dNeal'},
  {firstName:'bYang',lastName:'cNeal'},
  {firstName:'cYang',lastName:'bNeal'},
  {firstName:'dYang',lastName:'aNeal'},
];

const sortBy = (property)=>{
  return (a,b) => {
    return (a[property]<b[property])?-1:(a[property]>b[property])?1:0
  }
}

sortObj.sort(sortBy('lastName'));
//sort函数接受了被sortBy函数返回的比较函数,咱们再次抽象出compareFunction的逻辑,让用户更加关注比较,而不用去在意怎么比较的。
复制代码

HOC必然离不开闭包

上面的sortBy其实你们都应该看到了闭包的踪迹。关于闭包的产生、概念这里就不啰嗦了。总之咱们知道,闭包很是强大的缘由就是它对做用域的访问。

简单说下闭包的三个可访问的做用域:

  • 在它自身声明以内的变量
  • 对全局变量的访问
  • 对外部函数变量的访问(*)

接着举栗子

const forEach = (arr,fn)=>{
  for(const item of arr){
    fn(item);
  }
}
//tap接受一个value,返回一个带有value的闭包函数
const tap = (value)=>(fn)=>{
  typeof fn === 'function'?fn(value):console.log(value);
}

forEach([1,2,3,4,5],(a)=>{
  tap(a)(()=>{
    console.log(`Nealyang:${a}`)
  })
});

复制代码

函数柯里化与偏应用

函数柯里化

概念

直接看概念,柯里化是把一个多参函数转换为一个嵌套的一元函数的过程

不理解,莫方!举个栗子就明白了。

假设咱们有一个函数,add:

const add = (x,y)=>x+y;
复制代码

咱们调用的时候固然就是add(1,2),没有什么特别的。当咱们柯里化了之后呢,就是以下版本:

const addCurried = x => y => x + y;
复制代码

调用的时候呢,就是这个样子的:

addCurried(4)(4)//8
复制代码

是否是很是的简单?

说到这,咱们在来回顾下,柯里化的概念:把一个多参函数转换成一个嵌套的一元函数的过程。

如何实现多参函数转为一元

上面的代码中,咱们实现了二元函数转为一元函数的过程。那么对于多参咱们该如何作呢?

这个是比较重要的部分,咱们一步一步来实现

咱们先来添加一个规则,最一层函数检查,若是传入的不是一个函数来调用curry函数则抛出错误。当若是提供了柯里化函数的全部参数,则经过使用这些传入的参数调用真正的函数。

let curry = (fn) => {
if(typeof fn !== 'function'){
  throw Error('not a function');
}
return function curriedFn (...args){
  return fn.apply(null,args);
}
}
复制代码

因此如上,咱们就能够这么玩了

const multiply = (x,y,z) => x * y * z;
curry(multiply)(1,2,3);//6
复制代码

革命还未成功,咱们继续哈~下面咱们的目的就是把多参函数转为嵌套的一元函数(重回概念)

const multiply = (x,y,z) => x * y * z;
let curry = (fn) => {
  if(typeof fn !== 'function'){
    throw Error('not a function');
  }
  return function curriedFn (...args){
    if(args.length < fn.length){
      return function(){
        return curriedFn.apply(null,args.concat([].slice.call(arguments)));
      }
    }
   return fn.apply(null,args);
  }
}
curry(multiply)(1)(2)(3)
复制代码

若是是初次看到,可能会有些疑惑。咱们一行行来瞅瞅。

args.length < fn.length
复制代码

这段代码比价直接,就是判断,你传入的参数是否小于函数参数长度。

args.concat([].slice.call(arguments))
复制代码

咱们使用cancat函数连接一次传入的一个参数,并递归调用curriedFn。因为咱们将全部的参数传入组合并递归调用,最终if判断会失效,就返回结果了。

####小小实操一下 咱们写一个函数在数组内容中查找到包含数字的项

let curry = (fn) => {
  if(typeof fn !== 'function'){
    throw Error('not a function');
  }
  return function curriedFn (...args){
    if(args.length < fn.length){
      return function(){
        return curriedFn.apply(null,args.concat([].slice.call(arguments)));
      }
    }
   return fn.apply(null,args);
  }
}
let match = curry(function(expr,str){return str.match(expr)});

let hasNumber = match(/[0-9]+/);

let filter = curry(function(f,ary){
  return ary.filter(f)
});

filter(hasNumber)(['js','number1']);
复制代码

经过如上的例子,我想咱们也应该看出来,为何咱们须要函数的柯里化:

  • 程序片断越小越容易被配置
  • 尽量的函数化

偏应用

假设咱们须要10ms后执行某一个特定操做,咱们通常的作法是

setTimeout(() => console.log('do something'),10);
setTimeout(() => console.log('do other thing'),10);
复制代码

如上,咱们调用函数都传入了10,能使用curry函数把他在代码中隐藏吗?我擦,咱curry多牛逼!确定不行的嘛~

由于curry函数应用参数列表是从最左到最右的。因为咱们是根据须要传递函数,并将10保存在常量中,因此不能以这种方式使用curry。咱们能够这么作:

const setTimeoutFunction = (time , fn) => {
  setTimeout(fn,time);
}
复制代码

可是若是这样的话,咱们是否是太过于麻烦了呢?为了减小了10的传递,还须要多造一个包装函数?

这时候,偏应用就出来了!!!

简单看下代码实现:

const partial = function (fn,...partialArgs){
  let args = partialArgs;
  return function(...fullArgs){
    let arg = 0;
    for(let i = 0; i<args.length && fullArgs.length;i++){
      if(arg[i] === undefined){
        args[i] = fullArgs[arg++];
      }
    }
    return fn.apply(null,args)
  }
}

let delayTenMs = partial(setTimeout , undefined , 10);

delayTenMs(() => console.log('this is Nealyang'));
复制代码

如上你们应该都可以理解。这里不作过多废话解释了。

简单总结的说:

因此,像map,filter咱们能够轻松的使用curry函数解决问题,可是对于setTimeout这类,最合适的选择固然就是偏函数了。总之,咱们使用curry或者partial是为了让函数参数或者函数设置变得更加的简单强大。

下节预告

上一部分说的比较浅显基础,但愿你们也可以从中感觉到函数式编程的精妙和灵活之处。大神请直接略过~求指正求指导~

下一节中,将主要介绍下,函数式编程中的组合、管道、函子以及Monad。最后咱们在介绍下es6的Generator,或许咱们能从最后的Generator中豁然开朗得到到不少启发哦~~

技术交流

nodejs 技术交流 群号:698239345

React技术栈群号:398240621

前端技术杂谈群号:604953717

相关文章
相关标签/搜索