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

Posted 全栈前端精选

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了函数式编程了解一下(上)相关的知识,希望对你有一定的参考价值。

点击上方“蓝字”带你每天阅读全栈前端精选好文

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

部分简介

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

  • 入门简介

  • HOC简介

  • 函数柯里化与偏应用

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

  • 组合与管道

  • 函子和Monad

  • 再回首Generator

入门简介

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

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

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

  • 函数必须总是接受一个参数

  • 函数必须总是返回一个值

  • 函数应该依据接受到的参数,而不是外部的环境运行

  • 对于一个指定的x,必须返回一个确定的y

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

引用透明

所有的函数对于相同的输入都返回相同的结构,这一特性,我们称之为引用透明。
比如:

let identity = (i) => {return i};

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

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

命令式、声明式和抽象

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

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

  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中豁然开朗获得到很多启发哦~~


以上是关于函数式编程了解一下(上)的主要内容,如果未能解决你的问题,请参考以下文章

《On Java 8》中文版 第十三章 函数式编程

JavaScript:了解一下函数式编程

lambda与函数式

致开发人员:沉迷面向对象编程不可自拔?函数式编程了解一下

lambda与函数式——响应式Spring的道法术器

函数式编程学习总结