函数式编程

来源:互联网 发布:sqlserver 大数据导入 编辑:程序博客网 时间:2024/06/05 02:47

引言

自react流行开来,函数式编程这个概念也跟着火了起来,记得去年面试时就问到了函数式编程的理解,当时对于函数式编程的理解只局限于自己开发过的一个react项目以及部分阅读过的文章而已,现在回想起来回答的真是不堪入目,所以在这之后好好的过了一遍相关概念,系统的了解一下函数式编程。

从bug产生的温床副作用讲起

程序员最怕出现的bug,但是bug为什么会出现呢?可能所有人得看法都不一样,可能是由于开发人员的水平所致,可能是软件的复杂度导致的等等。Franklin Risby教授认为bug产生的环境是副作用,副作用是bug产生的温床。什么是副作用呢?引用几个例子

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态

概括来讲,只要是跟函数外部环境发生的交互就都是副作用。为什么bug容易在副环境中产生呢?简单来说因为外部环境是不可靠得,任何的变动都可能引起bug的产生,例如:
外部环境依赖示例图
在这种情况下,当有新的开发者去开始依赖外部环境的方法D时,发现结果不是预期的,于是更改了外部环境的变量,虽然此时方法D是没有问题了,但是之前的方法ABC都出现了问题,这就是常见的修复了一个bug另一个地方又出现了bug。那么要怎样避免这种问题的出现呢?一个很直接的方法进行柯里化,将外部的环境变量转换为方法内部的变量,不依赖不可靠的外部环境。
柯里化降低副作用
这样以来依赖不可靠的外部环境就会变成新方法内部的一个缓存下来的变量,当外部的环境发生变化的时候,起内部的缓存变量是不会变化的,减少副作用,这也是函数式编程的一个重要的点。

几个重要的基本概念

面向对象与函数式编程是一个相对的概念,如果说面向对象是通过人类的思维方式去解决软件工程里的难题的话,那么函数式编程就是通过数学的思维方式去解决软件工程里的问题。
学习函数式就像当初学习面向对象编程的感觉一样。有很多新的概念需要学习,但又与面向对象不一样,函数式编程里的概念要比面向对象里的概念抽象,毕竟是数学的思维,数学的本质就是抽象。下面就从几个重要的概念说起,来一步步的深入到函数式编程的精髓中。

纯函数

纯函数是函数式编程的基础,函数式编程是建立在一个个纯函数的基础上实现的,那么什么是纯函数呢?就像之前说的函数式编程是数学的思维方式,所以就先回顾下初中数学函数的概念吧。

A function relates an input to an output.函数是不同数值之间的特殊关系:每一个输入值返回且只返回一个输出值。

函数有个重要的判断方式就是一个f(x)只能对应一个y,但是一个y可以对应多个f(x),例如:
function-sets
上面这个图就是一个函数,而下面这个就不是,因为一个x对应了多个y
http://7xr8op.com1.z0.glb.clouddn.com/relation-not-function.gif

说完了数学里函数的概念,那么再说下什么是纯函数,纯函数的意思和数学里函数的概念一样,即相同的输入产生相同的输出,若是相同的输入产生了不同的输出那这个就不是纯函数,会产生副作用,因为这个函数是不可靠的,他的输出结果是不确定的。先从个简单的例子开始:

let age = 18;let showAge = () => {  console.log(age)}

上面这个showAge函数就不是一个纯函数,因为相同的输入不一定会产生相同的输出,当外部变量age改变时其输出也就改变了,若是要改成纯函数吧age这个变量变成一个非外部变量即可

let showAge = () => {  let age = 18;  console.log(age)}

这样一来这就是纯函数了,因为无论怎么调用这个函数,它的输出值是恒定不会改变的。
下面来一个稍微复杂点的。

let getUserInfo = AjaxGet('/getUserInfo',(json) =>{  console.log(json);}) 

上面这个函数就不是纯函数,因为调用了’/getUserInfo’这个接口返回的参数并不会是总是一样的,若是要把它编程纯函数,就需要用到柯里化,将这个函数改成惰性调用就可以了。

//ES6let getUserInfo = (callback) => AjaxGet('/getUserInfo',callback)ES5var getUserInfo = function(callback) {  return AjaxGet('/getUserInfo', callback);};

通过改造后能够看出getUserInfo这个函数这个函数无论调用多少次它的返回都是不会改变的,那就是返回一个调用/getUserInfo的接口函数,这样依赖就将一个非纯函数改造成了一个纯函数。

柯里化(curry)

首先什么是柯里化呢?这个如果只是从单纯的从概念上讲还是挺抽象的,所以先从例子说起,最后在说下什么是柯里化,这样更容易让人理解。
柯里化是函数编程中另一个十分重要的概念,且在应用上十分的广泛和重要,使用柯里化不光可以将一个非纯函数改造成一个纯函数,即使不用来改造函数,在非函数式开发中也能给我们带来很大的便利。举个例子,在接口模块中我们常常会这么定义接口调用的函数:

//ES6let getUserInfo = Ajaxget('/getUserInfo',(json) => json)//ES5var getUserInfo = AjaxGet('/getUserInfo',fcuntion(json){  return json;})//ES6let getItemInfo = Ajaxget('/getItemInfo',(json) => json)//ES5var getItemInfo = AjaxGet('/getItemInfo',fcuntion(json){  return json;})//ES6let getPrice = Ajaxget('/getPrice',(json) => json)//ES5var getPrice = AjaxGet('/getPrice',fcuntion(json){  return json;})//......等等众接口

这样写看起来不错没什么问题,但是如果有一天接到变动说通信协议要变,对每个返回的json加上安全检测,那这改动就麻烦了,成了一个纯体力活。改动就成了

//ES6let getUserInfo = $.getJson('/getUserInfo',(json) =>{  if (isSecruity(json)){    return callback  } })//ES5var getUserInfo = $.getJson('/getUserInfo',fcuntion(json){  if (isSecruity(json)){    return callback  } })//...省略掉

使用了柯里化这个问题就变得简单了不少

//ES6let getJson = (api,callback) => ajaxGet(api,callback)//ES5var getJson = function(api,callback){  return ajaxGet(api,callback)}let getUserInfo = getJson('/getUserInfo')let getItemInfo = getJson('/getItemInfo')let getPrice = getJson('/getPrice')

对于上面提到的协议变动,我们只要对getJson这个柯里化函数做修改就可以了,其他的由getJson这个柯里化函数生出来的小函数都一个个的自动修改了。

//ES6let getJson = (api,callback) => ajaxGet(api,(json)=>{  if (isSecruity(json)){    return callback  } })//ES5var getJson = function(api,callback){  return ajaxGet(api,function(json){    if (isSecruity(json)){      return callback    }   })}

通过例子可以看出柯里化就是

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

通过闭包将第一个参数缓存起来供给给返回的函数使用,使之成为一个新的函数。柯里化函数看起来就像小时候玩的小霸王游戏机一样,输入一个参数,也就是放入一个游戏机卡,它就会返回这个参数对应的函数,电视里就会出现这个游戏卡的游戏画面。
柯里化函数推荐使用ramda这个库,通过里面的curry方法可以轻松的讲一个普通函数转换成柯里化函数,十分的简单方便。

组合

组合是这个几个重要的基本概念中的最后一个,将组合之前要先从pointfree开始

point-free

是的就是pointfree,没有对应的中文翻译名字。

point-free又称作tacit-programming,是一种编程范式即不通过使用变量的形式声明函数。for example:

let getReverse = (str) => str.split(' ').reverse()

上面这个函数就不是point-free的,因为它用到了str这个变量做了split和reverse方法的连接器。那么现在问题来了,能够不通过声明变量来声明一个函数吗?

使用compose连接方法

这里就要引出组合这个概念,组合的概念很简单,就是将两个以上的纯函数组合成一个新的函数。一个最简单的组合代码如下:

//ES6let compose = (f,g) => (x) => f(g(x))//ES5var compose = function(f,g) {  return function(x) {    return f(g(x));  };};

现在就用compose把上面的那个非point-free的函数变成point-free的函数,注意compose对于参数里函数的执行顺序是从右到左执行的。

let reverse = (arr) => arr.reverse();let splitSpace = (str) => str.split(' ');let getReverse = compose(reverse,splitSpace);getReverse('a b c') // result ==> [b,c,a]

通过compose(组合)就可以不使用中间参数来串联起两个方法,引入ramda库,使用里面的compose方法,我们可以通过组合构建起更加清晰的point-free的函数。

import R from 'ramda'let getUserInfo = R.compose(  setUserHTML,  getUserProp,  checkUserInfo,  getUserJson)getUserInfo('/getUserInfo')

我们可以清晰看到getUserInfo这个函数调用了4个函数,且执行顺序是从下往上的。这样的代码可读性是非常强的,想一下如果不用compose而使用变量将这个4个函数连接起来,这个代码阅读的难度是多么的费劲吧。

函数式编程的优点

明白上几个重要的基本概念,函数式编程旅程就可以开始了。但是像纯函数、柯里化、组合这些用起来与面向对象相比有什么好处呢呢?这里就简单的坐下阐述

可缓存性(Cacheable)

通过纯函数和柯里化可以轻松缓存技术

import R from 'ramda'let getJson = R.curry((api,callback)=>{  let cache = {}  if (cahce[api]){    return callback && callback(cache[api])  }else{    ajaxGet(api,(json)=>{        cache[api] = json;        return callback && callback(json)    }))  } }getUserInfo = getJson('/getUserInfo')getItemInfo = getJson('/getItemInfo')getPriceInfo = getJson('/getPriceInfo')

通过柯里化轻松的将ajax的返回的json缓存了起来,并且柯里化函数的可配置性更强,对于通用性很强只是参数差异的函数,使用柯里化可以将它们抽象起来方便统一的管理。

可移植性/自文档化(Portable / Self-Documenting)

纯函数是自给自足的,依赖清晰的,这意味着纯函数相比于面向对象更容易理解和观察。

// 不纯的let signUp = (attrs) => {  let user = saveUser(attrs);  welcomeUser(user);}let saveUser = (attrs) =>{    var user = Db.save(attrs);    ...}let welcomeUser =(user) => {    Email(user, ...);    ...}

上述这种代码是项目中经常看到的,一个功能背后的函数是无法一眼就看出来的,需要一层层的阅读才能看出其中的依赖关系。与此相比函数式就先的更为的清晰和诚实。

//纯函数let signUp = (Db, Email, attrs) => () =>{    let user = saveUser(Db, attrs);    welcomeUser(Email, user);}let saveUser = (Db, attrs) => {    ...};let welcomeUser = (Email, user) => {    ...};//使用point-free与compose会更加的清晰let signUp = compse(  welcomeUser,  saveUser)

引用下Erlang 语言的作者 Joe Armstrong 说的一句话

“面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩…以及整个丛林”

可测试性(Testable)

可测试性就不用过多的赘述了,由于纯函数是可靠的,因此在测试上不用过多的配置,只需要简单输入断言输出即可。

与面向对象的对比

看起来好像函数式好想比面向对象好用太多了,使用函数式编程就像是在玩乐高一样,把每一个定义好的纯函数进行组合就能构建出一个个复杂可靠的功能。但是函数式编程并不是一个通用的最优解,它有自己适合的地方,也有自己的不足,讲了这么多的好处也要说一下它的缺点。
这块主要引用下王垠对于函数式不足的看法

所谓纯函数,基本上就是忽略了物质基础(硅片、晶体等)表现的特性。纯函数式的编程语言试图通过函数——在函数中传入传出整个宇宙——来重新实现整个宇宙。但物理的和模拟的是有区别的。“副作用”是物理的。它们真实的存在于自然界中,对计算机的效用的实现起着不可或缺的作用。利用纯函数来模拟它们是注定低效的、复杂的、甚至是丑陋的。你是否发现,在C语言里实现一个环形数据结构或随机数发生器是多么的简单?但使用Haskell语言就不是这样了。

还有,纯函数编程语言会带来巨大的认知成本。如果你深入观察它们,你会看到monads使程序变得复杂,难于编写,而且monad的变体都是拙劣的修改。monads跟Java的“设计模式”具有相同的精神本质。使用monad来表现副作用就像是visitor模式来写解释器。你是否发现,在很多其它语言里很简单的事情,放到Haskell语言就变成了一个课题来研究如何实现。

由于这遍文章已经被本人删掉了,可能是为了避免一些不必要的争执吧,原文可以看为什么说面向对象编程和函数式编程都有问题。总的来看对于函数式有两个重要的不足之处,一个是副作用是不可能完全避免的,用纯函数模拟会复杂低效,其次monads使程序变得复杂,它和面向对象的设计模式一样是弥补自身缺陷的一个补丁。偏激的选择函数式或者面向对象都是不可取的,最好的办法往往都是折中的相互借鉴的。

遗漏部分

文章对函数式编程还有几个重要的概念没有说明,一个是Functor,另一个是Monad,这些是为了在函数式中处理错误处理,异步等更复杂的场景而存在,属于更高阶的函数式用法,这里就先不做赘述了,用空专门好好整理下这两点

个人思考

总的来看如果用函数式来完全的替代面向对象,个人认为是不可取的。函数式在学术界虽然已经研究了多年,但是仍然处在完善中,使用函数式编程的思想来完善和提高项目代码的控制还是十分不错的,比如通过纯函数和柯里化管理接口功能,mock测试等等都是十分不错的方案。

参考

http://www.mathsisfun.com/sets/function.html
JS函数式编程指南

0 0
原创粉丝点击