摘要:函数式编程函数式,可能并不是那么难。在学习函数式编程之初,首先要知道在这一技能叶子中包含有多少个相关词,其次要知道它和我们是否从未有过遇见。
JS函数式编程
函数式,可能并不是那么难。
在学习JS函数式编程之初,首先要知道在这一“技能叶子”中包含有多少个相关词,其次要知道它和我们是否从未有过遇见。
一等公民、纯函数、柯里化、代码组合、pointfree、命令式与申明式、
Hindley-Milner类型签名、“特百惠”(Container、functor、Maybe、Either)、lift
Monad(pointed functor、chain)、Applicative Functor
接下来,我将根据JS函数式编程说说自己对每个相关词的看法。
一等公民(将函数与数字做平等对待)// 数字 var x = 1; var y = x; // 函数, 不平等对待 var fx = function(x) { return x; } var fy = function(y) { return fx(y); } // 函数,平等对待 // ... var fy = fx;
在编程之初,我们就将函数与其他数据类型从思想上分隔开,但在函数式编程中,我们要摒弃这种思想,将函数和其他数据类型做相等看待。
让我们来看看下面这个是否将函数当做一等公民?
var fz = function(f){ return fy(function(y){ return f(y); }); }
上述代码让我们看的很绕是吧,那我们将其转换一下如何?
var fz = function(f){ var fx = f; return fy(function(y){ return fx(y) }) }
根据之前的 fy = fx 等式便可知 function(y){return fx(y)} 其实就是等于 fx 的,所以就可以转化成
var fz = function(f){ return fy(f); } // 同上,fz 即等于 fy var fz = fy;
于是乎,这就是一等公民的函数。思想切莫先入为主。
纯函数纯函数是一种函数,即相同的输入,永远得到相同的输出。正如 O = kI + b,IO在数学上的函数关系。(O: 输出 , I: 输入)
函数式编程追求的是纯函数
var xs = [1,2,3,4,5]; // 纯 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]纯函数的好处
可缓存性、可移植性、可测试性、合理性、并行代码
这些好处都可依据“纯函数不会随外部环境的改变而改变其内部运算逻辑”而得出。
概念:只传递给函数我们需要传递的所有参数的一部分,让它返回一个函数去处理剩下的参数。
var add = function(x){ return function(y){ return x + y; } } var addOne = add(1); var addTen = add(10); addOne(2); // => 3 addTen(2); // => 12
柯里化很好的体现了纯函数一个输入对应一个输出的概念,柯里化就是每传递一个参数,就返回一个新函数处理剩下的参数。
代码组合组合(compare), 就像工厂流水线一样,将多个函数按顺序拼凑,从右到左 依次加工数据
var compare = function(f, g){ return function(x){ return f(g(x)); } }
下面是一个反转数组取第一个元素得到其首字母并大写的例子
var reduce = (f) => (arr) => arr.reduce(f) var reverse = reduce(function(src, next){return [next].concat(src)}, []); var head = (x) => x[0] var toUpperCase = (x) => x.toUpperCase(); // 记住是从右向左 var f = compare(compare(compare(head,toUpperCase), head), reverse); f(["abc","def","ghi"]) // => G
pointfree就是函数组合,而这些函数包含一等公民与柯里化的概念。
申明式与命令式作个对比就能知道这两个的区别了
// 命令式 var arr1 = [1,2,3,4,5]; var arr2 = []; for(let i = 0; i < arr1.length; i++) { arr.push(arr1[i]); } // 申明式 arr2 = arr1.map(function(c){return c;})
命令式是那种一步接着一步的执行方式,而申明式是并行运算,正如上述的代码组合的例子一样,如果我们用命令式来写肯定是一句一个逻辑,写起来看起来都很费劲,但是申明式不同,它能让我们只使用一次就可执行多条逻辑,而且可以在不同情况环境下重复使用,这就是纯函数的可移植性。
再看一个例子
// 命令式 var headToUpperCase = function(str){ var h = head(str); return h.toUpperCase(); } // 声明式 var headToUpperCase = compare(toUpperCase, head);Hindley-Milner类型签名
此玩意就是对你构造的函数进行一个说明
// 例一 // 下面就是Hindley-Milner类型签名 // strLength :: String -> Number var strLength = function(str){ return str.length; } // 例二 // join :: String -> [String] -> String var join = curry(function(what, xs){ return xs.join(what); }) // curry就是将传入的函数参数转换成curry函数并返回 // 第一个String指代what, [string]指代xs,第二个string指代return的值 // 例三 // concat :: a -> b -> c var concat = curry(function(src, next){ return src.concat(next); }) // a,b可以用任何字母代替,但不能相同,这样表示的是不同的类型,而不是从同一数据中脱离出来,如去数组中的某几个元素组成新的数组 // 例四 // map :: (a -> b) -> [a] -> [b] var map = curry(function(f, xs){ return xs.map(f); }) // a -> b 指代 f ; [a] 指代 xs ; [b] 指代 return 的值 // 例五 // reduce :: (a -> b -> a) -> b -> [a] -> b var reduce = curry(function(f, x, xs){ return xs.reduce(f, x) })
下面会介绍两个functor(Container, Maybe)
Container先给源码
var Container = function(){ this.__value = x; } Contaienr.of = function(x) { return new Container(x); } Container.map = function(f) { return Container.of(f(this.__value)) }
使用Container将我们的值进行包裹
使用Container.of让我们不用写new
使用Container.map让我们在不访问__value的情况下得到容器内部的值并进行运算
同样,先给源码
var Maybe = function(x){ this.__value = x; } Maybe.of = function(x){ return new Maybe(x); } 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 和 Container其实差不多,唯一的区别在于它出现了对空值的检测,让容器现在能够存储空值了。
Either(Left and Right)
先上源码
var Left = function(x){ this.__value = x; } Left.of = function(x){ return new Left(x); } Left.prototype.map = function(f){ return this; } var Right = function(x){ this.__value = x; } Right.of = function(x){ return new Right(x); } Right.prototype.map = function(f){ return Right.of(f(this.__value)) }
跟Maybe(null) 一样,当返回一个Left 时就直接让程序短路,但有了Left 至少可以让我们用if 做一次条件判断来知道是什么情况导致输出Left
lift
一个函数在调用的时候,如果被map包裹从一个非functor函数转换为一个functor函数,就叫做lift。这样让普通函数变成可以操作容器的函数,且兼容任意functor。
以下是例子
var headToUpperCase = map(compare(toUpperCase, head)); headToUpperCase(Container.of("hello!"));IO
var IO = function(f){ this.__value = f; } IO.of = function(){ return new IO(function(){ return x; }) } IO.prototype.map = function(f){ return new IO(compose(f, this.__value)); }
现在this.__value 是一个函数,因而如果执行map(head) 等操作时其实是将这个函数压入一个“执行栈”,而这栈中全部是要执行的函数,就想是代码组合一样,将所有压入的函数延迟执行。而看起来,我们容器的最终形态就是能容纳一个函数。
那么问题就来了,为什么要用容器,而且最好是容纳函数呢?
函数式程序即通过管道把数据在一系列纯函数间传递的程序,而我们之前所有的例子都是关于同步编码的,如果出现异步情况怎么办?如下:
var fs = require("fs"); var readFile = function(filename){ return function(reject, result){ fs.readFile(filename, "utf-8", function(reject, result){ err ? reject(err) : result(data); }) } }
ok ,这的确使用了函数式,但异步之后呢,依旧是回调阶梯,所以这么做并没有真正意义上的使用函数式。
我们需要延迟执行,因而我们需要一个类似IO但并非IO的容器类型,由于能力有限,我只能借用Quildreen Motta 所处理的Folktale 里的Data.Task
var fs = require("fs"); var readFile = function(filename){ return new Task(function(reject, result){ fs.readFile(filename, "utf-8", function(err, data){ err ? reject(err) : result(data); }); }) } readFile("helloworld").map(split(" ")).map(head).map(toUpperCase).map(head); // => Task("H")Monad
先给例子
var fs = require("fs"); // readFile :: String -> IO String var readFile = function(filename){ return new IO(function(){ return fs.readFileSync(filename, "utf-8"); }) } // print :: String -> IO String var print = function(x){ return new IO(function(){ return x }) } var hello = compose(map(print), readFile); hello("helloworld"); // => IO(IO("helloworld")) // 包了两层IO,于是要想得到值,我们就得执行两次__value hello("helloworld").__value().__value(); // => helloworld
那么如何才能消去这多的层数呢,我们需要使用join
IO.prototype.isNothing = function(){ return (this.__value === null || this.__value === undefined); } IO.prototype.join = function(){ return this.isNothing() ? IO.of(null) : this.__value; } var ioio = IO.of(IO.of("hello")); // => IO(IO("hello")) ioio.join(); // => IO("hello")
于是我们在map 之后就要使用 join ,让我们将其叫做chain
var chain = curry(function(f, m){ return m.map(f).join(); // or compose(join, map(f))(m) }) // map/join var hello = compose(join, map(print), readFile); // chain var hello = compose(chain(print), readFile); // 给Maybe也加上chain Maybe.of(3).chain(function(three){ return Maybe.of(2).map(add(three)) }) // => Maybe(5);applicative functor
如下实例
var add = curry(function(x, y){ return x + y; }) add(Container.of(2), Container.of(3)); // 很明显是不能这么进行计算的 // 但是用chain,我们可以 Container.of(2).chain(function(two){ return Container.of(3).map(add(two)) })
可是这看起来挺费劲的不是吗
于是我们就要使用applicative functor
Container.prototype.ap = function(other_container){ return other_container.map(this.__value); } Container.of(2).map(add).ap(Container.of(3));
ap 就是一种函数,能够把一个functor的函数值应用到另一个functor的值上。
而根据上述例子,我们可知map 是等价于 of/ap 的
F.prototype.map = function(f){ return this.constructor.of(f).ap(this); }
而chain 则可以分别得到 functor 和 applicative
// map F.prototype.map = function(){ var ct = this; return ct.chain(function(a){ return ct.constructor.of(f(a)) }) } // ap F.prototype.ap = function(other){ return this.chain(function(f){ return other.map(f); }) }定律
代码组合的定律
// 结合律 var _bool = compose(f, compose(g, h)) == compose(compose(f, g), h); // => true
map的组合律
var _bool = compose(map(f), map(g)) == map(compose(f, g)) // => true
Monad
// 结合律 var _bool = compose(join, map(join)) == compose(join, join) // => true // 同一律 compose(join, of) == compose(join, map(of)) var mcompose = function(f, g){ return compose(chain(f), chain(g)) } // 左同一律 mcompose(M, f) == f // 右同一律 mcompose(f, M) == f // 结合律 mcompose(mcompose(f,g), h) == mcompose(f, mcompose(g, h))
Applicative Functor
var tOfM = compose(Task.of, Maybe.of); tOfM("hello").map(concat).ap(tOfM(" world"))); // => Task(Maybe(hello world)) // 同一律 A.of(id).ap(v) == v // 同态 A.of(f).ap(A.of(x)) == A.of(f(x)) // 互换 var v = Task.of(reverse) var x = "olleh" v.ap(A.of(x)) == A.of(function(f){return f(x)}).ap(v) // 组合 var u = IO.of(toUpper) var v = IO.of(concat(" world")) var w = IO.of("hello") IO.of(compose).ap(u).ap(v).ap(w) == u.ap(v.ap(w))参考链接
https://www.gitbook.com/book/...
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/88354.html
摘要:在创业初期,你招来的工程师必须是能够独当一面的大神队友。要评估一个应聘者的真实水准,最佳方式就是结对编程。用微博的抓取消息并显示在时间线上,就是个很好的考察应聘者的面试项目。不过结对编程再好使,也没办法让你完全了解一个应聘者。 原文链接:10 Interview Questions Every JavaScript Developer Should Know 对大部分公司来说,招聘技...
摘要:前端面试总结先说背景,本人年月毕业,去年十月校招到今年月一直在做前端开发工作,年前打算换工作,就重新梳理下面试考点总结包含基础,基础,常见算法和数据结构,框架,计算机网络相关知识,可能有的点很细,有的点很大,参考个人情况进行总结,方便对知识 前端面试总结 先说背景,本人2018年7月毕业,去年十月校招到今年10月一直在做前端开发工作,年前打算换工作,就重新梳理下面试考点总结包含: ...
摘要:前端面试总结先说背景,本人年月毕业,去年十月校招到今年月一直在做前端开发工作,年前打算换工作,就重新梳理下面试考点总结包含基础,基础,常见算法和数据结构,框架,计算机网络相关知识,可能有的点很细,有的点很大,参考个人情况进行总结,方便对知识 前端面试总结 先说背景,本人2018年7月毕业,去年十月校招到今年10月一直在做前端开发工作,年前打算换工作,就重新梳理下面试考点总结包含: ...
摘要:为目前使用范围最广的网络保护协议。身处攻击目标周边的恶意人士能够利用密钥重装攻击,利用此类安全漏洞。本文和大家一起探讨下如何在三年内快速成长为一名技术专家。 业界动态 Vue 2.5 released Vue 2.5 正式发布,作者对于该版本的优化总结:更好的TypeScript 集成,更好的错误处理,更好的单文件功能组件支持以及更好的与环境无关的SSR WiFi爆惊天漏洞!KRACK...
阅读 1872·2021-11-09 09:46
阅读 2432·2019-08-30 15:52
阅读 2391·2019-08-30 15:47
阅读 1201·2019-08-29 17:11
阅读 1724·2019-08-29 15:24
阅读 3461·2019-08-29 14:02
阅读 2418·2019-08-29 13:27
阅读 1149·2019-08-29 12:32