系统研究一下函数式编程

导读

  1. 最近杠上的是函数式编程,简称FP。最开始对它的接触大概是一年前左右,那是公司大牛做的一次技术分享。听的过程中是这样的感觉: 我X,还有这种操作,听完以后:我是谁,我在哪,我在干什么,最后仅仅模糊的记得一个概念:函数柯里化。由此我的脑子里稀里糊涂的多了一个概念:函数柯里化就是函数式编程。
  2. 随着这次对它系统的学习,才发现之前对FP的理解实在是太狭隘了,太狭隘了,太狭隘了…。下面就对自己的学习成果做下记录,后续也会随着自己的理解加深不断的纠错与补充。

阅读目标

  1. 理解函数式编程的概念
  2. 了解函数式编程使用的一些基本概念
  3. 认识并使用几种函子

什么是函数式编程

1. 函数式编程是一种开发范式

  • 常见开发范式有两种:面向过程和面向对象,而函数式编程就是将要接触的第三种开发范式
  • 在学习函数式编程时,要和面向过程对比着学,找到类似于刚从面向过程开发转到面向对象开发的那种感觉
  • 函数式编程和面向对象编程在某些地方有些类似,在学习的过程中脑袋里经常会有这样的疑问:这跟面向对象差不多嘛。此时你要做的就是忽略这种想法,这就是我提到第二点的原因。

2. 函数式编程起源于数学

  • 准确的说起源于数学中的一个分支:范畴学。我将它简单的归纳为:研究两个集合之间的关系。它的运算方法就是函数式编程,而这个方法正好可以用来学代码。
  • 函数式编程的起源决定了它的一个基调:使用到的函数必须是纯函数。因为数学本身就是一门不允许模棱两可的学问。
  • 如果对范畴学有兴趣,可以多多研究,可以加深对函数式编程的理解,反之,就多注意一下函数式编程的特点,同样也可以无压力的使用它,虽然我还没有做到。

函数式编程的基本理论

1. 函数是一等公民

我对他的理解有两部分:

  1. 每个函数要满足单一责任最小意外等原则
  2. 函数可以跟其他类型的数据一样,当做参数传递,赋值给变量,存放到数组中…

2. 使用到的函数必须是纯函数

  • 纯函数的定义:相同的输入,永远会得到相同的输出,而且没有可观察的副作用
  • 副作用的定义:在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。它可能包含但不限于如下操作:更改文件系统-往数据库插入记录-发送一个http请求-可变数据-打印log-获取用户输入-DOM查询-访问系统状态

3. 追求纯函数的理由

  1. 可缓存-移植性强-可测试性-引用透明
  2. 原来想对每个理由展开说明,后来感觉这些基本都见名知意,而且这也不是特别需要注意的内容,就不想占用过多篇幅,有问题留言即可

函数式编程的两个核心函数

如果要使用函数式编程我们需要借助一些第三方库,比如lodash(普通版本和FP版本)ramda,后续所有的示例都是基于FB lodash

1. curry函数-用于将指定函数柯里化

  • 函数柯里化:可以跟普通函数一样直接调用,也可只传递给函数传递一部分参数,让它返回一个函数去处理剩下的参数

    // 正常函数
    let add = function(x, y) {
      return x+y;
    }
    let result = add(1+1); // 2
    
    
    // 柯里化后的函数
    let curryAdd = _.curry(add);
    
    let addOne = curryAdd(1); // Function
    let addTen = curryAdd(10); // Function
    
    let result1 = addOne(1) // 2
    let result2 = addTen(1) // 11
    
    let result3 = curryAdd(1, 20) // 21
    

    至于原理,就是_.curry函数将我们add函数转化为如下类型的函数:

    // 通过闭包的方式保留参数x
    var add = function(x) {
      return function(y) {
        return x + y;
      };
    };
    
  • FP lodash除了提供_.curry函数帮助将自定义的函数转化成柯里化函数,FB lodash还将一些普通lodash版本中的函数封装成柯里化供我们使用,比如_.add/_.head/_.first等等,下面我们查看几个列子,更多查看FB lodash

      // 使用FB lodash中_.add直接实现addOne和addTen
      const addOne = _.add(1);  // Function
      const addTen = _.add(10); // Function
      const result1 = addOne(1); // 2
      const result2 = addTen(10); // 20
      
      // 使用FB lodash中_.head获取数组的第一个元素
      const result3 = _.head([2,34,8); // 2
      const result4 = _.head('abcd); // a
      
      // _.heade的实现原理
      const getElementByIndex = function(index, arr) { // head原方法
      	return arr[index];
      }
      const curryHead = _.curry(getElementByIndex);
    
      const getFirtElement = curryHead(0);
      getFirtElement([3,4,8]); // 3
    
  • 在柯里化的函数中,参数顺序也是有讲究的,仔细观察上面的这些柯里化后的函数,会发现我们都将要操作的数据放到了最后一个参数里,现在只需注意这点就行,后续就会慢慢明白为什么这样做了

2. compose函数-组合函数

  • 概念:将传入的函数组合起来,返回一个从右到左执行的管道函数
    let toUpperCase = function(x) { return x.toUpperCase(); };
    let exclaim = function(x) { return x + '!'; };
    let sayHi = function(x) { return 'Hi,' + x ; };
    let shout = _.compose(sayHi, toUpperCase, exclaim);
    	
    shout('We are handsome'); // Hi,WE ARE HANDSOME!
    
    简易源码如下:
    // f和g都是函数,x是组合后形成函数的需传参数
    var compose = function(f,g) {
      return function(x) {
        // compose函数执行顺序都是从右到左
        return f(g(x));
      };
    };
    
  • compose函数其实就是帮助我们创建了一个从左到右的数据流,再加上每一步都是纯函数,大大增强了代码的可读性
  • 所有的compose都遵循一个规律:数学中的结合律。它可以让我们的组合更加灵活,而且肯定不会影响结果,举个栗子:
    let toUpperCase = function(x) { return x.toUpperCase(); };
    let exclaim = function(x) { return x + '!'; };
    let sayHi = function(x) { return 'Hi,' + x ; };
    
    // toUpperCase/exclaim/sayHi 三个函数只要保证顺序不变,随意我们组合,比如
    _.compose(sayHi, toUpperCase, exclaim) 
    ==> _.compose(_.compose(sayHi, toUpperCase), exclaim) 
    ==> _.compose(sayHi, _.compose(toUpperCase, exclaim));
    

使用函数式编程做个小栗子

需求描述:请求接口,将接口中的图片都渲染到页面中

1. 面向过程编程示例

function getDataAppendBody(word) {
  $.getJSON('https://api.flickr.com/services/feeds/photos_public.gne?tags=' + word + '&format=json&jsoncallback=?', (data) => {
     console.log(data);
     $('body').html(data.items.map((item) => {
       return $('<img />', { src: item.media.m });
     }));
  });
}
getDataAppendBody('dogs');

2. 函数式编程示例

/********************* 准备工作 ***************************/
// 强调: getJSON和setHtml都是柯里化函数
const Impure = {
  getJSON: _.curry(function(callback, url) {
    $.getJSON(url, callback);
  }),

  setHtml: _.curry(function(sel, html) {
    $(sel).html(html);
  })
};

const img = function (url) {
  return $('<img />', { src: url });
};

const trace = _.curry(function(tag, x) {
  console.log(tag, x);
  return x;
});

const url = function (word) {
  return 'https://api.flickr.com/services/feeds/photos_public.gne?tags=' + word + '&format=json&jsoncallback=?';
};

/********************* 开始操作 ***************************/
// _.map和_.prop 建议先
var images = _.compose(_.map(img),_.map(_.compose(_.prop('m'), _.prop('media'))), _.prop('items'));
var renderImages = _.compose(Impure.setHtml("body"), images);

var app = _.compose(Impure.getJSON(renderImages), url);

app("cats");

3. 总结

  • 函数式编程开发前,会将所有的步骤都处理成纯函数,而且这些纯函数的移植性都特别强,其他业务也都可以用。麻烦一次,永久方便,更爽的是可以拿这些纯函数,用_.compose随意组合,裂变出更多功能的函数,这点是面向对象做不到的
  • 但从代码量上看,其实面向过程完爆函数式编程,但从长远考虑,函数式编程留下了更强的扩展性,可读性也更加强(这一点等写习惯了才会慢慢了解)。
  • 在上面的栗子中有两个问题急需我们解决:如何判空如何捕捉异步的error,这就涉及到我们之后要接触的两个函子MaybeEither

不可或缺的函子(functor)

1. 啥是函子

functor 是实现了 map 函数并遵守一些特定规则的容器类型。 如下是一个最为基础的函子

const Container = function(val) {
  this.__value = val;
}
Container.of = function(x) {
  return new Container(x);
}
Container.prototype.map = function(f) {
  return Container.of(f(this.__value))
}

Container.of(2).map(function(two){ return two + 2 })
Container.of("bombs").map(concat(' away')).map(_.prop('length'))

上面提到的规则,总结如下几个:

  • 只有一个属性,并且该属性可以是任意类型
  • of函数可有可无,它仅仅是用来避免在创建容器时避免忘记写new
  • map函数要返回一个新的函子对象,这样我们就可以连续map了
  • 函子的原型方法可以有扩展

2. Maybe函子-判空

首先看个栗子,针对如下对象取到name的值

const serverResponce = {
  company: { department: { name: 'xxxxx' } },
}

我们使用如下两种方式来取值,

 // 正常方式
 const name = serverResponce.company.department.name;
 
 // Maybe函子取值
const name = Maybe.of(serverResponce).map(_.prop('company')).map(_.prop('department')).map(_.prop('name'));

分析一波:

  1. 第一种方式,如果company和department某一项为null或undefine,程序将直接报错。当然可以在每层取值都进行判断,避免程序中断,但成大过大
  2. 第二种方式,如果出现中间某项为空,最终会返回一个Maybe.of(null),是否异常,只需判断name是否为Maybe.of(null)即可。

接下来研究一下Maybe函子的实现

const Maybe = function(val) {
  this.__value = val;
}

Maybe.of = function(x) {
  return new Maybe(x);
}

Maybe.prototype.isNothing = function() {
  return (this.__value === null || this.__value === undefine);
}

// 还没有搞清楚怎么数组结构
Maybe.prototype.map = function(f) {
   return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value))
}

跟Container主要区别,就是多了一个函数isNothing,接着在每次调用Map的时候优先执行一下this.isNothing(),如果为空就直接返回一个Maybe.of(null)

3. Either函子

除了Either函子,还有IO/Task/Monad…, 后续可能会在单独开一篇博客详细介绍函子。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值