《JS函数式编程指南》Part 1笔记

《JS函数式编程指南》这本书值得读很多遍,特别推荐哦~~~
看这本书初衷是看懂ramda,结果发现 函数式编程这个大萝卜。哈哈哈哈。。。

一. 相关术语

  • 函数范式(functional paradigm)
  • 命令式 (imperative)
  • 可变状态(mutable state)
  • 无限制副作用(unrestricted side effects)
  • 无原则设计(unprincipled design)
  • 认知负荷(cognitive load)
  • 可缓存性(Cacheable)
  • 可移植性/自文档化(Portable/Self-Documenting)
  • 类型签名(type signatures)
  • 类型约束(type constraints)
  • 。。。

我理解的函数式编程就是运用数学中函数的方式,以通用、可组合的组件形式进行编程,而不是过程化地命令计算机去怎么做。函数式编程优势主要体现在数据不变性函数无副作用两方面;

二. 应避免出现的情况

  • : 用一个函数将另一个函数包装起来, 目的只是延迟执行;

    // wrong
    var getServerStuff = function(callback){
      return ajaxCall(function(json){
        return callback(json);
      });
    };
    
    // right
    var getServerStuff = ajaxCall;
    

    因为:

    return ajaxCall(function(json){
       return callback(json);
     });
     
    // 等价于
    ajaxCall(callback);
    

第一种书写方式,虽然更易于理解,但是内层函数参数改变时,外层包裹函数也需要同时改变。

  • :在命名时将自己限定在特定的数据/情景中;
    这是重复造轮子的一大原因;

三. 什么是纯函数

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用

比如: 数组中的slice 函数则为纯函数,每次应用会得到相同的数据,而splice则不同;

纯函数就是数学上的函数,而且是函数式编程的全部

还有一种的情况就是:在函数中引入了外部的环境,从而增加了认知负荷;
举例:

  // 不纯的
  var minimum = 21;
  
  var checkAge = function(age) {
    return age >= minimum;
  };
     
  // 纯的
  var checkAge = function(age) {
    var minimum = 21;
    return age >= minimum;
  };

在不纯的版本函数中,其输入值依赖于系统状态。

对于纯函数定义中提到的副作用是指:
副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。
只要是和函数外部环境发生的交互就都是副作用;
在纯函数中,并不是要禁止一切副作用,而是让副作用发生在可控的范围内,在纯函数中使用functor和monad进行控制副作用。

四. 纯函数的好处

  1. 纯函数总能根据输入来做缓存。
    实现缓存的一种典型方式是memoize技术。
    var memoize = function(f) {
      var cache = {};
    
      return function() {
        var arg_str = JSON.stringify(arguments);
        cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
        return cache[arg_str];
      };
    }
    
    对于含有类似请求等不纯的函数,通过包裹一层新的函数的延迟执行的方式把不纯的函数变成纯函数。
  2. 可移植性/自文档化(Portable/Self-Documenting)
    纯函数的依赖都在参数中指明,更易于观察理解。
    🌰:
     // 不纯的
     var signUp = function(attrs) {
       var user = saveUser(attrs);
       welcomeUser(user);
     };
     // 纯的
     var signUp = function(Db, Email, attrs) {
       return function() {
         var user = saveUser(Db, attrs);
         welcomeUser(Email, user);
       }; 
    
    在JS中,可移植性意味着把函数序列化并通过socket发送,也可以意味着代码可以在web worker 在运行。
    命令式编程中的方法和过程都深深的和其运行环境相关,功能通过状态、依赖和有效作用达成。而纯函数正好相反,与环境无关,因此可以移植。
  3. 可测试性(Testable)
    只需要简单的给函数一个输入,然后断言输出就好了。
  4. 合理性(Reasonable)
    如果一段代码可以替换成它所执行的所得的结果,而且是在不改变整个程序行为的前提下替换的,则称这段代码是引用透明的(referential transparency)。
    由于纯函数总是相同的输入得到相同的输出,所以纯函数也是引用透明的。这也是纯函数的很大的一个优点。
  5. 并行代码
    我们可以并行运行任意的纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因为副作用而进入竞争态(race condition);

五. 快速实现纯函数化的工具--柯里化(curry)

  1. 什么是Curry?

    Curry:只传递函数的一部分参数来调用它,让它返回一个函数去处理剩下的参数。
    可以一次性地调用curry函数,也可以每次只传一个参数分多次调用。
    Ramda 函数本身都是自动柯里化;

  2. Curry 帮助函数
    Lodash、Ramda库中都有Curry 帮助函数。在使用这类函数时有一个很重要的模式就是将要操作的数据放在最后的一个参数中。
    🌰 1:

      const R = require('ramda');
      const match = R.curry((what, str) => {
        return str.match(what);
      })
      
      match(/\s+/g, 'hhhh');
      // or
      match(/\s+/g)('hhhh');
    

    🌰 2:

     // 使用帮助函数 `_keepHighest` 重构 `max` 使之成为 curry 函数
     // 无须改动:
     var _keepHighest = function(x,y){ return x >= y ? x : y; };
     
     // 重构这段代码:
     var max = function(xs) {
       return reduce(function(acc, x){
         return _keepHighest(acc, x);
       }, -Infinity, xs);
     };
     
     var max = R.reduce(_keepHighest, -Infinity);
     
    

    🌰 3:

     // 包裹数组的 slice 函数使之成为 curry 函数
     // [1,2,3].slice(0,1);
     var slice = R.curry(function(start, end, xs){ 
       return xs.slice(start, end);
     });
    

六. 代码组合(compose)

  1. 什么是Compose?
 var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
 }; 

上面的代码即为代码组合的本质。组合就是将两个或两个以上的函数进行结合返回新的函数。
在组合的定义中,g 将先于f 执行,因此就创建了一个从右到左的数据流。

组合负符合数学中的结合律

 compose(a,b,c) === compose(compose(a,b),c) === compose(a,compose(b,c))

结合律的一大好处是任何一个函数分组都可以被拆开来,然后再以它们自己的组合方式打包在一起。

组合中数据的转变如下图:

image.png

  1. Compose有利于实现pointfree
    pointfree 模式:函数无须表明要操作的数据的样子。一等公民的函数、curry、compose联合使用有利于实现这种模式。
    🌰 :
  // 非 pointfree,因为提到了数据:word
  var snakeCase = function (word) {
    return word.toLowerCase().replace(/\s+/ig, '_');
  };
  
  // pointfree
  var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

在pointfree的实现方式中不需要指明操作的数据为word,而在非pointfree的代码中则需要指明。pointfree 代码可以帮我们减少很多不必要的命名。
在pointfree的实现方式中, 是通过管道将数据在接受单个参数的函数间传递。通过Curry,使得compose中的每一个函数都先接受数据,然后操作数据,最后再把结果传递给下一个函数。

  1. Compose的调试方式
    在使用组合时,将多个函数组合在一起,除了最右边的函数可以一次性接收两个及两个以上的参数,其他的函数一次只能接收一个参数,因此经常会出现下面的错误。
 // wrong
 var latin = compose(map, angry, reverse);
 
 // right
 var latin = compose(map(angry), reverse);

调试上面代码的错误可以使用下面这个不纯的trace函数来追踪代码的执行情况:

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

🌰:

 var dasherize = compose(join('-'), toLower, split(' '), replace(/\s{2,}/ig, ' '));
 
 dasherize('The world is a vampire');
 //debug
 var dasherize = compose(join('-'), toLower, trace("after split"), split(' '), replace(/\s{2,}/ig, ' '));
 // after split [ 'The', 'world', 'is', 'a', 'vampire' ] 
  1. Compose使用举例
   const  _ = require('ramda');
  // 数据
  const CARS = [
      {name: "Ferrari FF", horsepower: 660, dollar_value: 700000, in_stock: true},
      {name: "Spyker C12 Zagato", horsepower: 650, dollar_value: 648000, in_stock: false},
      {name: "Jaguar XKR-S", horsepower: 550, dollar_value: 132000, in_stock: false},]
  // 🌰 1:
  // 使用 _.compose()、_.prop() 和 _.head() 获取第一个 car 的 name
  // answer
  const nameOfFirstCar = _.compose(_.prop('name',_.head));
  
  // 🌰 2:
  // 重写下面这个函数
  var isLastInStock = function(cars) {
    var last_car = _.last(cars);
    return _.prop('in_stock', last_car);
  };
 
 // answer 
 const isLastInStock = _.compose(_.prop('in_stock'), _.last);
 
 // 🌰 3:
 // 使用_average重写下面函数
 var _average = function(xs) { return reduce(add, 0, xs) / xs.length; };
 
 var averageDollarValue = function(cars) {
   var dollar_values = map(function(c) { return c.dollar_value; }, cars);
   return _average(dollar_values);
 };
 
 // answer
 const averageDollarValue = _.compose(_average,_.map(_.prop('dollar_value')));
 
 // 🌰 4:
 // 重构下面的代码
 var availablePrices = function(cars) {
   var available_cars = _.filter(_.prop('in_stock'), cars);
   return available_cars.map(function(x){
     return accounting.formatMoney(x.dollar_value);
   }).join(', ');
 };
 
  // answer
 var formatPrice = _.compose(accounting.formatMoney, _.prop('dollar_value'));
 var availablePrices = _.compose(join(', '), _.map(formatPrice), _.filter(_.prop('in_stock')));
 
 // 🌰 5:
 // 重构下面的代码
 var fastestCar = function(cars) {
   var sorted = _.sortBy(function(car){ return car.horsepower }, cars);
   var fastest = _.last(sorted);
   return fastest.name + ' is the fastest';
 }; 
 
 // answer
 var append = _.flip(_.concat);
 var fastestCar = _.compose(append(' is the fastest'),
                            _.prop('name'),
                            _.last,
                            _.sortBy(_.prop('horsepower')));

七. 声明式代码

  1. 什么是声明式代码?

    命令式代码是一步步地指示要做怎么做。声明式代码是告诉要做什么,而不是怎么做。虽然命令式代码并不错,但是命令式代码硬编码了一步接一步的执行方式。声明式代码不指定执行顺序,所以更适合于并行执行。
    🌰 :

      // 命令式
      var makes = [];
      for (i = 0; i < cars.length; i++) {
        makes.push(cars[i].make);
      }
      
      // 声明式
      var makes = cars.map(function(car){ return car.make; }); 
    
  2. 可用于重构的等式

     // map 的组合律
     var law = compose(map(f), map(g)) == map(compose(f, g));
    

    使用上面的等式进行重构可以将两层循环合并成一层循环。

八. Hindley-Milner 类型签名(type signatures)

  1. 什么是Hindley-Milner 类型签名?
    在 Hindley-Milner 系统中,函数都写成类似 a -> b 这个样子,其中 a 和b 是任意类型的变量。

    🌰 1:

    //  match :: Regex -> String -> [String]
    //  match :: Regex -> (String -> [String]) 
    var match = curry(function(reg, s){
      return s.match(reg);
    }); 
    
    

对于Hindley-Milner 类型签名:

  • 与普通代码一样,类型签名中也使用变量,把变量命名为a 和b 只是一种约定俗成的习惯;
    对于相同的变量名,其类型也一定相同。 a -> b 可以是从任意类型的 a 到任意类型的 b,但是 a -> a 必须是同一个类型;
  • 可以将最后一个类型理解成返回值;
  • 将(a -> b)理解成一个类型为a的参数,返回类型为b的结果的函数;
  • 签名可以把类型约束为一个特定的接口(interface);这就是类型约束(type constraints)

以上面的规则对reduce进行解释:

🌰 2:

 //  reduce :: (b -> a -> b) -> b -> [a] -> b
 var reduce = curry(function(f, x, xs){
  return xs.reduce(f, x);
 });

首先reduce接收(b -> a -> b)这样的一个函数作为参数1,函数的两个参数为类型b 和a, b和a
的值来自于 reduce接收的第2个和第3个参数,最终返回类型b 的结果值, 并可以看到结果值类型b和reduce的第一个参数(这个函数)的返回类型相同,则可以看出reduce的返回值则为reduce接收的一个参数的返回值。
🌰 3:

    // sort :: Ord a => [a] -> [a]

胖箭头左边指明 a 一定是个 Ord 对象。

  1. parametricity
    一旦引入一个类型变量,就会出现一个奇怪的特性叫parametricity。parametricity 是指此函数将会以一种统一的行为作用于所有的类型。
    🌰:
 // fun:: [a] -> a

a 告诉我们它不是一个特定的类型,这意味着它可以是任意类型;那么我们的函数对每一个可能的类型的操作都必须保持统一。这就是 parametricity 的含义。

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

推荐阅读更多精彩内容

  • 上周8班作文为《瞧,这一家子》。 从孩儿们的写作效果来看,贴近家庭实际。语言或质朴,或俏皮幽默,让人忍俊不禁,结构...
    辰辰2008阅读 356评论 0 2
  • PHP基本语法 变量相关的函数 isset() 判断变量是否被定义 empty() 判断变量是否为空值, unse...
    阿尔方斯阅读 188评论 0 0