zoukankan      html  css  js  c++  java
  • 笔记:函数式编程学习

    2020-04-04:

    坚持「相同输入得到相同输出」原则 


    一、什么是纯函数:

      定义: 对相同的输入它保证能返回相同的输出。

    例子:

    var xs = [1,2,3,4,5];
    
    // 纯的
    xs.slice(0,3);
    //=> [1,2,3]
    
    xs.slice(0,3);
    //=> [1,2,3]
    
    xs.slice(0,3);
    //=> [1,2,3]
    
    
    // 不纯的
    xs.splice(0,3);
    //=> [1,2,3]
    
    xs.splice(0,3);
    //=> [4,5]
    
    xs.splice(0,3);
    //=> []

    二、什么是副作用:

      定义:副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。(作者认为,副作用是产生bug 的温床)

      副作用让一个函数变得不纯是有道理的:从定义上来说,纯函数必须要能够根据相同的输入返回相同的输出;如果函数需要跟外部事物打交道,那么就无法保证这一点了。

     三、为什么要追求“纯”

    (1)因为同一输入总是能得到唯一输出,因此结果可缓存:

       toolz.memoize 实现以上功能

      值得注意的一点是,可以通过延迟执行的方式把不纯的函数转换为纯函数:

     

    (2)可移植性/自文档性,更易于观察和理解:换言之,不存在函数以外的动作,搭配类型签名完美。

    (3)可测试性:只需简单地给函数一个输入,然后断言输出就好了。

    (4)合理性,也就是引用透明性:意味着,同一输入下,改段代码可以替换成它执行所得的结果

    (5)并行代码:能做成pipe(管道)执行,因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态(race condition)。

    四、“柯里化”:

      这个平时用的最多,总结为:是一种“预加载”函数的能力,通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。

    // 有趣的练习:
    
    // 1、包裹数组的 `slice` 函数使之成为 curry 函数
    // //[1,2,3].slice(0, 2)
    
    var slice = _.curry(function(start, end, xs){ return xs.slice(start, end); });
    
    
    // 2、借助 `slice` 定义一个 `take` curry 函数,该函数调用后可以取出字符串的前 n 个字符。
    var take = slice(0);
    
    // 使用:
    [1, 2, 3, 4].take(2) == [1, 2]

    五、代码组合: 

    var compose = function(f,g) {
      return function(x) {
        return f(g(x));
      };
    };

      (1)f 和 g 都是函数,x 是在它们之间通过“管道”传输的值。(2)g 将先于 f 执行,因此就创建了一个从右到左的数据流。这样做的可读性远远高于嵌套一大堆的函数调用

      (3)compose 里多少个函数都可以 

    Pointfree:

      定义:函数无须提及将要操作的数据是什么样的。

      好处:pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用。对函数式代码来说,pointfree 是非常好的石蕊试验,因为它能告诉我们一个函数是否是接受输入返回输出的小函数。比如,while 循环是不能组合的。不过你也要警惕,pointfree 就像是一把双刃剑,有时候也能混淆视听。并非所有的函数式代码都是 pointfree 的,不过这没关系。可以使用它的时候就使用,不能使用的时候就用普通函数。

    Debug:

      使用不纯的 trace 函数来追踪代码的执行情况:

    // trace:
    
    var trace = curry(function(tag, x){
      console.log(tag, x);
      return x;
    });
    
    // 例子:
    
    var dasherize = compose(join('-'), toLower, trace("after split"), split(' '), replace(/s{2,}/ig, ' '));
    // after split [ 'The', 'world', 'is', 'a', 'vampire' ] 

    六、什么是声明式代码:

      它指明的做什么不是怎么做。

    // 命令式硬编码了那种一步接一步的执行方式。而 compose 表达式只是简单地指出了这样一个事实:用户验证是 toUser 和 logIn 两个行为的组合。

    //
    命令式 var authenticate = function(form) { var user = toUser(form); return logIn(user); }; // 声明式 var authenticate = compose(logIn, toUser);

    七、Hindley-Milner 类型签名(TypeScript):

      类型签名是以 “Hindley-Milner” 系统写就的

      类型签名在写纯函数时所起的作用非常大:(1)短短一行,就能暴露函数的行为和目的;(2)让它们保持通用、抽象;(3)类型签名不但可以用于编译时检测(compile time checks),还是最好的文档。

    八、特百惠(容器思想):

    (1)首先定义一个什么功能都没有的容器,只用于装数据,和定义了一个创建对象的简便方法:

    var Container = function(x) {
      this.__value = x;
    }
    
    Container.of = function(x) { return new Container(x); };
    
    
    // 使用:
    Container.of(3)
    //=> Container(3)
    
    
    Container.of("hotdogs")
    //=> Container("hotdogs")

    (2)functor容器,其实就是在container基础上加了个map方法,使之mappable:

    // (a -> b) -> Container a -> Container b
    Container.prototype.map = function(f){
      return Container.of(f(this.__value))
    }
    
    
    // 使用:
    Container.of(2).map(function(two){ return two + 2 })
    //=> Container(4)

    (3)Maybe容器,functor基础上,在map的方法中加入一个判断,使之出现两个可能的结果:

      实际当中,Maybe 最常用在那些可能会无法成功返回结果的函数中。

    Maybe.prototype.isNothing = function() {
      return (this.__value === null || this.__value === undefined);
    }
    
    Maybe.prototype.map = function(f) {
      return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
    }
    
    
    // Maybe 会先检查自己的值是否为空,然后才调用传进来的函数。这样我们在使用 map 的时候就能避免恼人的空值了(注意这个实现出于教学目的做了简化)。
    
    // 使用:
    Maybe.of("Malkovich Malkovich").map(match(/a/ig));
    //=> Maybe(['a', 'a'])
    
    Maybe.of(null).map(match(/a/ig));
    //=> Maybe(null)

    (4) 纯的错误处理:left和right

      right容器执行正常操作,left容器无视正常操作而是内部嵌入一个错误消息达到纯的错误处理。

      left跟maybe的区别:用 Maybe(null) 来表示失败并把程序引向另一个分支,但是这并没有告诉我们太多信息。

      left跟maybe的共同点:就像 Maybe(null),当返回一个 Left 的时候就直接让程序短路。

    var moment = require('moment');
    
    //  getAge :: Date -> User -> Either(String, Number)
    var getAge = curry(function(now, user) {
      var birthdate = moment(user.birthdate, 'YYYY-MM-DD');
      if(!birthdate.isValid()) return Left.of("Birth date could not be parsed");
      return Right.of(now.diff(birthdate, 'years'));
    });
    
    getAge(moment(), {birthdate: '2005-12-12'});
    // Right(9)
    
    getAge(moment(), {birthdate: 'balloons!'});
    // Left("Birth date could not be parsed")

    -- either容器:将left和right整合

    //  fortune :: Number -> String
    var fortune  = compose(concat("If you survive, you will be "), add(1));
    
    //  zoltar :: User -> Either(String, _)
    var zoltar = compose(map(console.log), map(fortune), getAge(moment()));
    
    
    //  either :: (a -> c) -> (b -> c) -> Either a b -> c
    var either = curry(function(f, g, e) {
      switch(e.constructor) {
        case Left: return f(e.__value);
        case Right: return g(e.__value);
      }
    });
    
    //  zoltar :: User -> _
    var zoltar = compose(console.log, either(id, fortune), getAge(moment()));
    
    zoltar({birthdate: '2005-12-12'});
    // "If you survive, you will be 10"
    // undefined
    
    zoltar({birthdate: 'balloons!'});
    // "Birth date could not be parsed"
    // undefined

    九、Monad(洋葱):

    (1)pointed functor :实现了of方法的functor,of方法实际是用来把值放到默认最小化上下文(default minimal context)中的(希望容器类型里的任意值都能发生 lift,然后像所有的 functor 那样再 map 出去。)。

    (2)monad 是可以变扁(flatten)的 pointed functor:一个 functor,只要它定义个了一个 join 方法(有两层相同类型的嵌套可以用该方法压扁到一块)和一个 of 方法,并遵守一些定律,那么它就是一个 monad。

    var mmo = Maybe.of(Maybe.of("nunchucks"));
    // Maybe(Maybe("nunchucks"))
    
    Maybe.prototype.join = function() {
      return this.isNothing() ? Maybe.of(null) : this.__value;
    }
    
    
    // 使用:
    mmo.join();
    // Maybe("nunchucks")

    (3)chain函数:

      chain函数把 map/join 套餐打包到一个单独的函数中。如果你之前了解过 monad,那你可能已经看出来 chain 叫做 >>=(读作 bind)或者 flatMap;都是同一个概念的不同名称罢了。

    // chain 的实现:
    
    //  chain :: Monad m => (a -> m b) -> m a -> m b
    var chain = curry(function(f, m){
      return m.map(f).join(); // 或者 compose(join, map(f))(m)
    });
    
    
    Maybe.of(3).chain(function(x) {
      return Maybe.of(2).map(add(x));
    });
    // Maybe(5);

      另一种实现:chain 可以自动从任意类型的 map 和 join 衍生出来,就像这样:t.prototype.chain = function(f) { return this.map(f).join(); }

    (4)一些例子:

    十、applicative functor:实现了 ap 方法的 pointed functor

      问题来了:假设有两个同类型的 functor,我们想把这两者作为一个函数的两个参数传递过去来调用这个函数,

      

      解决办法ap  能够把一个 functor 的函数值应用到另一个 functor 的值上。

        

        Container(3) 从嵌套的 monad 函数的牢笼中释放了出来。需要再次强调的是,本例中的 add 是被 map 所局部调用(partially apply)的,所以 add 必须是一个 curry 函数。

       关于ap:

    // ap实现:
    
    Container.prototype.ap = function(other_container) {
      return other_container.map(this.__value);
    }
    
    
    // ap特性:
    // of/ap 等价于 map
    F.of(x).map(f) == F.of(f).ap(F.of(x))
    //因此它是个从左到右填入参数的:
    
    
    // 应用:sign in
    var $ = function(selector) {
      return new IO(function(){ return document.querySelector(selector) });
    }
    
    //  getVal :: String -> IO String
    var getVal = compose(map(_.prop('value')), $);
    
    // Example:
    // ===============
    //  signIn :: String -> String -> Bool -> User
    var signIn = curry(function(username, password, remember_me){ /* signing in */  })
    
    IO.of(signIn).ap(getVal('#email')).ap(getVal('#password')).ap(IO.of(false));
    // IO({id: 3, email: "gg@allin.com"})

      signIn 是一个接收 3 个参数的 curry 函数,因此我们需要调用 ap 3 次。在每一次的 ap 调用中,signIn 就收到一个参数然后运行,直到所有的参数都传进来,它也就执行完毕了。

      我们可以继续扩展这种模式,处理任意多的参数。另外,左边两个参数在使用 getVal 调用后自然而然地成为了一个 IO,但是最右边的那个却需要手动 lift,然后变成一个 IO,这是因为 ap 需要调用者及其参数都属于同一类型。

    十一、关于 lift:pointfree 的方式调用 applicative functor

    (1)lift 实现:

    var liftA2 = curry(function(f, functor1, functor2) {
      return functor1.map(f).ap(functor2);
    });
    
    var liftA3 = curry(function(f, functor1, functor2, functor3) {
      return functor1.map(f).ap(functor2).ap(functor3);
    });
    
    //liftA4, etc

    (2)举例:liftA2的应用(A2指需要两个参数)

    // checkEmail :: User -> Either String Email
    // checkName :: User -> Either String String
    
    //  createUser :: Email -> String -> IO User
    var createUser = curry(function(email, name) { /* creating... */ });
    
    Either.of(createUser).ap(checkEmail(user)).ap(checkName(user));
    // Left("invalid email")
    //等价于
    liftA2(createUser, checkEmail(user), checkName(user)); // Left("invalid email")

    十二、of、ap、map之间的替代:

      总结1:含of方法:容器、  map的容器 :functor、  join的容器:monad、   map+join容器:chain、 ap的functor:applicative、 lift是特殊的applicative

      总结2: of/ap == map

      总结3: chain可以衍生出map、  chain/map能衍生出ap

    // 从 chain 衍生出的 map
    X.prototype.map = function(f) {
      var m = this;
      return m.chain(function(a) {
        return m.constructor.of(f(a));
      });
    }
    
    // 从 chain/map 衍生出的 ap
    X.prototype.ap = function(other) {
      return this.chain(function(f) {
        return other.map(f);
      });
    };

    (1)定律:

    -- 同一律: A.of(id).ap(v) == v

    -- 同态:A.of(f).ap(A.of(x)) == A.of(f(x))    同态就是一个能够保持结构的映射(structure preserving map)。实际上,functor 就是一个在不同范畴间的同态,因为 functor 在经过映射之后保持了原始范畴的结构。

        所以,不管是把所有的计算都放在容器里(等式左边),还是先在外面进行计算然后再放到容器里(等式右边),其结果都是一样的。

    -- 互换:v.ap(A.of(x)) == A.of(function(f){ return f(x) }).ap(v)   选择让函数在 ap 的左边还是右边发生 lift 是无关紧要的。

     (2)练习:

    require('../../support');
    var Task = require('data.task');
    var _ = require('ramda');
    
    // fib browser for test
    var localStorage = {};
    
    
    
    // Exercise 1
    // ==========
    // Write a function that add's two possibly null numbers together using Maybe and ap()
    
    var ex1 = function(x, y) {
      return Maybe.of(_.add).ap(Maybe.of(x)).ap(Maybe.of(y));
    };
    
    
    // Exercise 2
    // ==========
    // Rewrite 1 to use liftA2 instead of ap()
    
    var ex2 = liftA2(_.add);
    
    
    
    // Exercise 3
    // ==========
    // Run both getPost(n) and getComments(n) then render the page with both. (the n arg is arbitrary)
    var makeComments = _.reduce(function(acc, c){ return acc+"<li>"+c+"</li>" }, "");
    var render = _.curry(function(p, cs) { return "<div>"+p.title+"</div>"+makeComments(cs); });
    
    
    var ex3 = Task.of(render).ap(getPost(2)).ap(getComments(2));
    // or
    // var ex3 = liftA2(render, getPost(2), getComments(2))
    
    
    
    
    // Exercise 4
    // ==========
    // Write an IO that gets both player1 and player2 from the cache and starts the game
    localStorage.player1 = "toby";
    localStorage.player2 = "sally";
    
    var getCache = function(x) {
      return new IO(function() { return localStorage[x]; });
    }
    var game = _.curry(function(p1, p2) { return p1 + ' vs ' + p2; });
    
    var ex4 = liftA2(game, getCache('player1'), getCache('player2'));
  • 相关阅读:
    三级菜单的实现方式
    简单登录接口实践
    C++中的set和java的hashset有何区别?
    css 利用文档结构给列表添加样式
    谈谈我对JS中this的理解
    谈谈我对JS原型的理解
    使用node.js,实现简单的JS文件合并小工具
    谈谈我对JS闭包的理解
    谈谈我对JS作用域的理解
    模拟实现 百度翻译 右下方的可折叠的分享按钮列表
  • 原文地址:https://www.cnblogs.com/marvintang1001/p/12635658.html
Copyright © 2011-2022 走看看