函数式编程
函数式编程
长文预警!!!!!今天给大家分享的主题是最近兴起的一阵热潮——函数式编程,实际上函数式编程的历史,是比面向对象还要久远的,第二古老的高级语言,LISP 正是函数式编程语言的鼻祖;1990年出现的 Haskell 甚至影响了 java 等一大批编程语言的设计,至今仍然是函数式编程语言中无法撼动的王者。
代码可以进来试试 https://codesandbox.io/s/reverent-bassi-jpe8d晨阳推荐的 https://github.com/getify/Functional-Light-JS ,不错的书What描述变换为主,采用声明式的方式进行编程。有以下的特点:a. 函数是一等公民,说的即是函数与普通的数字、字符串、对象一样可以被创建、赋值、传递。 甚至连四则运算也应该以函数的形式使用。为什么使用Ramdaconst r = require('ramda'); // 之后的例子中也会使用 ramda 函数式库const print = r.unary(console.log);[1, 2, 3].forEach(print);
// 顺带一提,我喜欢给对象定义一个 logObject.prototype.log = function(indent){ console.log(JSON.stringify(this, null, indent))}[1,2,3].log();[1,2,3].map(e=>e*2).log();
将函数作为参数传入一个高阶函数,可以利用这种抽象快速的开发出新的功能。// DRY const sum = function (arr) { let ret = 0; for (let v of arr) ret += v; return ret;}
const product = function (arr) { let ret = 1; for (let v of arr) ret *= v; return ret;}
// Why not trying this?const sum2 = r.reduce(r.add, 0) // 干净简洁const product2 = r.reduce(r.multiply, 1)
reduce实际上提取这种代码的模式,一个初始值,一个用于迭代转换数组元素的函数。函数式编程思维,就是从具体的函数中提取抽象的模式,再通过组合形成具体的函数的过程。
b. 只有表达式,没有语句。实际上,任何有用的程序都是具有语句的,表达式可以产生值,语句消费产生的值。存在语句就意味着副作用,伴随着IO或者变量状态的改变。我的理解是,产生值的过程中,使用连贯的表达式可以避免多返回类型的危害,这样可以确保结果类型的一致性。实际上ts的联合类型是对js弱类型、不支持重载的妥协。const doWithStatement = (e) => { if (e < 10) return true; else return 'false'; // 只要存在多个 return 语句,那么类型就可能出现分歧} // 这只是一个函数,如果分支再多一点,再在内部调用几个别的函数,别的函数也能返回多种类型,复杂度将指数爆炸const doWithExpression = (e) => e < 10;
// const sumColumns = r.pipe(r.transpose, r.map(r.sum)); // initconst matrix = // init [[1, 2, 3], [4, 5, 6], [7, 8, 9]];console.log(sumColumns(matrix)); // [12,15,18]
函数式可以很好地帮助你转换思路,更加简洁地解决问题,也能最大程度地复用已有函数。
c. 没有副作用,任何纯函数的调用都不应该改变全局(或参数)的状态,或者引起IO,而只是在完成它的工作后返回一个结果值,容器类型必须创建一个新的容器对象const arr = [-5, 2, -4, 3, 1, 6, 9, -5, -4, -3];const sortByAbs = r.sort(r.ascend(Math.abs)); // 按绝对值排序console.log(sortByAbs(arr));console.log(arr);
// 举个反例 r.concat([x],acc) const reverse_ = r.reduce((acc, x) => acc.unshift(x) && acc , []);const arr2 = r.range(1, 11);console.log(arr2);console.log(reverse(arr2));// << [ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]// unshift是个副作用的函数,当你很自信的写下这行代码,并运行一个通过了之后,就轻易相信了它// 实际上,再调用一次就炸了console.log(reverse(arr2));// << [ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]
其实链表用来表达函数式算法更加合适,至少许多操作不需要复制整个列表。
d. 不改变状态,这里的理解应该是下了定义就不能轻易的改变它的值,或者是只用常量。(PI就是3.1415926...... ,而不应该是别的什么东西)const pi = 3.1415926pi = 123456 // error!!!!!
f. 引用透明,指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。const square = (e)=>e*elet origin = {x:0,y:0}
const distance = ({x, y})=>Math.sqrt(square(x-origin.x)+square(y-origin.y))console.log(distance({x:3,y:4}))origin.x = origin.y = 10 // don't change the truth of the world!console.log(distance({x:3,y:4})) // shit !!!
Why
代码简洁,开发快速
const arr = [1,2,3,4,5,6,7,8,9]arr .map(e=>e*e) // 转换 .filter(e=>e<50) // 过滤 .reduce((a, b)=>a+b,0) // 求和
接近自然语言,易于理解
// 判断一个值非空const notNil = r.o(r.not, r.isNil) // r.complement(r.isNil) // 取反
可以看到not 和 isNil 被组合到一起,快速复用,也更具语义,不熟悉函数式的同学可能会写出这样的。const notNil = (x) => !r.isNil(x);
-- Haskell waynotNil = not . null -- 好吧这是判断数组非空的
更方便的代码管理
由于是纯函数,入参相同,返回值也相同,很容易进行单元测试。
易于并行开发
得益于不可变性,代码的执行变得可预测了。
代码的热更新
只要接口相同,就可以直接改良、直接替换。
你需要函数式的工具箱lodash其实我一开始接触的是lodash,官网对它的定位是这样的|没错,它是对 js 这门语言本身非侵入性的增强,提供了大量生产力工具函数,你能想到的基本操作但是js不提供的,它基本都有,像什么检查、排序、分组、去重、字符串处理,甚至一些异步的比如节流防抖、延迟……鲁棒性也都不错,比如:
_.map([1,2,3],e=>e*e) // [1,4,9]_.map({a:1,b:2,c:3},e=>e*e) // [1,4,9]_.map(null, e=>e*e) // []_.map(undefined, e=>e*e) // []
一般情况下,能用lodash,我都不会用原生的方法。也正如上文所述,这些方法,难以复用。
RamdaRamda是一个纯粹的函数式工具库,你不会在里面看到像lodash那样强的工具函数,但是它提供了创造生产力的能力。Ramda的出现,打破了人们使用js的方式,lodash中数据都是放在第一个参数的,用一次丢一次。ramda干脆地把函数操作的对象,放到了最后,并使用 curry 化。我的理解是,curry化函数的前面的参数,不管有多少,都是服务于最后的参数的,函数使用前面的参数来描述做什么事情,最后的参数才是这件事情的主角。配合curry化,生成的函数就是可复用的。const compact = r.filter(Boolean) // 清除集合中的假值compact([1,0,'', false, 2, undefined, 4, null]) // [1,2,4]compact({a:1, b:false, c:'', d:'good'}) // {a: 1, d: "good"}
[1,0,'', false, 2, undefined, 4, null].filter(Boolean) // ?????const compact2 = (arr) => arr.filter(Boolean) // 应该关注操作,而不是参数
实际上,lodash/fp 几乎搬走了Ramda的所有功能。
上面说的大家其实都知道的七七八八,那么函数式编程还有什么特性吗?
函数组合基本上就是管道,不过是从右往左结合的。|const r = require("ramda");const compose = (...fns) => initValue => r.reduceRight((fn, v) => fn(v), initValue, fns);
const f = compose( r.multiply(3), r.add(4));
console.log(f(3)); // (3+4)*3 = 21
递归递归是一种强大的,通过分解、解决子问题并规约的一种计算策略。// 最大公约数function GCD(a, b) { return a % b == 0 ? b : GCD(b, a % b);}
-- Haskell 斐波那契数列fib = 0 : 1 : zipWith (+) fib (tail fib)// 0 1 1 2 3 5 8 13 。。。// 1 1 2 3 5 8 13 。。。
柯里化(curry)这其实是Lambda算子的内容,每个函数实际上只接受一个参数,并返回一个值或函数,这个定义看起来奇怪,其实很有道理,一个函数,参数没接满,调用也没有意义(默认参数?很遗憾,curry化不支持默认参数,那并不严谨,显式传参能够让程序更安全),通过柯里化,其返回的函数对象上,就带有了先前的参数信息,为后面的调用提供了便利,多次调用,不需要再重复输入前面的参数了。或者可以这么理解,我们把这个过程看成是一个流水线,装上传送带、发动机、插上电源,最后把原料丢进去。我们所做的就是在给这个流水线做配置,传送带、发动机、电源甚至原料都是可以配置的,而这个流水线提供了一个机制(模式)——如何把这些东西协调起来运作。柯里化就提供了一步一步构建流水线的能力。-- HaskelladdThree a b c = sum [a, b, c]-- calling ,伟大的Haskell认为给函数调用加上括号完全是多此一举addThree 1 2 3
在函数式编程中我们需要如此看待标识符。
每个标识符其实都是函数
不需要参数的函数是常量函数
参数接收够了就是一次函数调用
// javascriptconst addThree = a => b => c => a + b + c const addThree_ = r.curry((a, b, c)=>a+b+c)// callingaddThree(1)(2)(3) // 很无奈addThree_(1,2,3) // 很随意addThree_(1)(2,3)addThree_(1)(2)(3)
写这部分内容的时候,我突然醒悟了curry化到底意味着什么。
一个标识符,代表着一个已定义的不能改变的固有对象,
参数相同,结果相同的表现是什么?f(arg1,arg2) === f(arg1,arg2)?
换一种理解方式,如果参数也是标识符的一部分呢?
f_arg1_arg2 === f_arg1_arg2
当你把参数字面量代入之后,整个标识符都将成为一个定义了的对象
把函数和参数看成一个整体,作为标识符,当我们试图模糊curry化函数与其他对象的时候,就会发现它的合理性
const add3and3 = addThree(3,3)add3and3(6) // 12
所以记忆函数为什么有效?
Free Point Style无参风格,当我们使用高阶柯里化函数构造一个新函数时,如果传入的参数是在最后调用,那么可以省去,简洁许多。// const sum = arr => r.reduce(r.add, 0, arr)const sum = r.reduce(r.add, 0)
柯里化能够让你的代码更可读、简洁// normalfetchFromServer() .then(JSON.parse) .then(data => data.posts) .then(
posts
=>
posts.map(post=>post.title) })
// curry fetchFromServer() .then(JSON.parse) // 解析JSON .then(r.
get
('posts')) // 获取一个字段的数据集 .then(r.map(r.
get
('title'))) // 取出我们关注的字段
高阶函数(high order function)函数可以作为参数传递,也可以作为返回值使用,这就是高阶函数了,同时它也是柯里化的好兄弟,通常一起使用。js代码中经常会看到filter、map、reduce这样的数组方法,单看它们其实并不知道实际要做什么,它们代表了一些通用的操作的抽象。
filter过滤,创建新集合并保留一些元素,但是不修改他们
map变换,对每个元素做变换再打包成新集合
reduce归约,对集合做聚合转换
高阶函数是一些通用的流水线,只有当你提供给它足够的辅助机器,它才知道具体如何运转。这也就是我在上面说的,高阶函数是创造函数的函数。
对不住。实际上我之前写的算是一个反例了,反省。这有点为了写函数式而写函数式的感觉,js并不适合表达,实际上代码复杂且相当不可读。// 这里举一个牛顿迭代求平方根的例子// 反省,不要太过追求free point style// number y -> number x -> boolean |y * y - x| < 0.0001const sqrt_good_enough = (y, x) => Math.abs(y*y-x) < 0.0001// r.pipe( // 不要再写这种代码,会被打死的// r.converge(r.subtract,// [(y) => y * y, r.nthArg(1)]),// Math.abs,// r.lt(r.__, 0.0001));
// number y -> number x -> number y' y' = (y + x/y) / 2const sqrt_improve = (y, x) => (y + x/y) / 2
// 这里涉及几个过程// 1. 判断迭代结果是不是够好,好就返回// 2. 对y做一次迭代// 3. 返回1步骤const try_it = r.curry(function ti(good_enough, improve, y, x) { return good_enough(y, x) ? y : ti(good_enough, improve, improve(y, x), x);});
const sqrt = try_it(sqrt_good_enough, sqrt_improve, 1);console.log(sqrt(10));
try_it高阶函数的表达的意思是,你可以使用任何good_enough,improve来实现迭代过程。// 初始值是1 , 如果不大于100 ,就每次 乘3try_it(r.gt, r.multiply(3), 1, 100); // 243
惰性求值(lazy evaluation)当你的数据(很大,100万)经过一系列的转换之后,你想要获取其中的前不知道多少个(可能很小),那么前面的转换需要做全吗?generateList(1,Infinity) .map(e=>e%2==0?e-3:e+3) .map(e=>e*e) .take(10) // 这里取10个,整个流水线,开始运作,但一段只计算10个数出来 // 代码只是随便写写的,实际上不能运行,如有需要,见lazy.js-- In Haskell , this work!take 10 . map (\x -> x*x) . map (\x -> if even x then x-3 else x+3) $ [1..]
这就是惰性求值的使用场景,感受一下就好。
一些概念
关于Functor、Applicative、Monad这里有一个介绍的不错的文章http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html (下面有中文翻译)
这几个抽象的概念,简单的说说就是我们使用函数时的一些场景,函子就是数据的容器,比如数组,集合、字典。Functor(函子)我们认为一个容器是Functor时,其必须定义一个 map 实例方法,这个方法将使用一个函数作用于容器中的数据,并将转换后的数据包回Functor里面。数组其实就可以看成是函子的一种。const arr2 = r.range(1,11)const square = r.pipe(r.repeat(r.__,2),r.product)const arr3 = r.map(square, arr2)console.log(arr3)const m1 = {a:1,b:2,c:3}const m2 = r.map(square, m1)console.log(m2)
没了,嗯。
Applicative(应用函子)函子里面是否可以放入函数?这种类型的函子称为 Applicative,要如何将函子里的函数应用到含值的函子上呢?// javascriptconst random0T200 = r.times(()=>Math.random()*200|0)const maximum = r.reduce(Math.max, Number.NEGATIVE_INFINITY)const minimum = r.reduce(Math.min, Number.POSITIVE_INFINITY)const randomData2 = random0T200(100)const [dataMin, dataMax] = r.ap([minimum, maximum],[randomData2])
-- Haskellarr = [5,2,4,8,9,-1,7][dataMin, dataMax] = [minimum, maximum] <*> [arr]
Monad(单子)这是一个令人恐惧的东西,但是想必大家都用过了,没错就是你——Promise在Haskell当中,Monad唯一用来写非纯代码的地方,我们可以让Monad去执行一些IO动作(也不仅限IO),当动作完成时,Monad就持有了一个值,它知道它接下来应该做什么(回调),拿出这个值,交给下一个Monad让它继续下一个动作。实际上,在浏览器上面的各种事件模型,都可以看成是Monad。Promise解决了什么问题,回调地狱。new Promise((res, rej)=>{ ... res("hello")}).then(str=>{ return new Promise((res, rej)=>{ ... res(str + " world!") })}).then(console.log)
可以想象到一连串的 Promise,每个都知道下一个 Promise 是谁,当自己手头的工作做完了,就将结果交给下一个 Promise。
无比纯粹的单子变换这个地方可能有点儿绕。JavaScript:当我们在写 Promise 的时候,可能就发现了一点,Promise 需要回调,回调的返回,还是Promise,没错,这就是Haskell 大哥教我的单子变换。Haskell:单子接收一个回调,单子通过某种方式取得一个值之后,会拿出这个值,调用回调函数,并保证回调函数一定返回一个单子。在函数式编程语言Haskell中,IO单子是唯一可以执行副作用的对象,而单子变换,保证了这种副作用的纯粹性。JavaScript:为了进一步简化Promise的使用,我创造了await语法糖,但只能在async函数里面调用,作用是从Promise(fulfilled状态)中拿出一个值,然后利用这个值做下一步操作,最后再返回一个Promise(话说 await 和 Haskell 的<- 语法几乎一致)。Haskell:如果你把一个IO单子传递给了一个函数,想从IO单子中拿出值就一定执行了单子变换,那么返回的一定也是一个单子,而返回IO单子的函数可能是不纯的,所以接受返回值为IO单子的函数的函数也是不纯的,故而纯函数一定不接受IO单子作为参数。JavaScript:大哥又在念经了,别晕,听我一言。如果你把 async function作为参数传到另一个函数F里面,调用它并使用await语法拿出来值,那么这个函数F必然也是个async function(因为await只能在async function中使用)。
实际上,用于描述变换行为的概念,不止上面三种。出自 https://github.com/fantasyland/fantasy-land ,最近才发现的,Ramda有面向这种些扩展类型的处理机制。|
啥,不够爽?再练习练习?
More Practice
给出两个长度相等的数组,在相同的位置上,第一个数组的值作为键,第二个数组的值为值,生成一个字典对象
const keys = 'this is a example'.split(' ')const values = 'here are some values'.split(' ')
// zipToDict :: [k] -> [v] -> {k:v}const zipToDict = r.pipe( r.zip, r.fromPairs) // 转为字典对象console.log(zipToDict(keys, values)) // {this: "here", is: "are", a: "some", example: "values"}
如何用函数式的方式实现 transpose?
// 命令式编程,你需要显式指定迭代指针 i 和 j 去遍历二维数组const transposeImperative = matrix => { const cols = matrix[0].length; const rows = matrix.length; let res = []; for (let i = 0; i < rows; i++) { for (let j = 0; j < cols; j++) { if (!res[j]) res[j] = []; res[j][i] = matrix[i][j]; } } return res;};
// r.nth(1) 是一个取出数组第1位元素的透镜方法。// nths 是个透镜数组。使用应用函子去映射一个矩阵,就可以一列一列顺序平铺。const transpose = matrix =>{ const cols = matrix[0].length; const rows = matrix.length; const nths = r.times(r.nth, cols); return r.pipe( r.ap(nths), // 使用了应用函子,结果是一个平铺的数组 r.splitEvery(rows) // 切开 ) (matrix)}
// 也是用到上面的nths,r.__作为占位符可以很方便的留出参数的位置。// 每个 map nth matrix都能提取一列作为结果的一行。const transpose_ = matrix => { const cols = matrix[0].length const nths = r.times(r.nth, cols) return r.map( r.map(r.__, matrix), nths) }
// 递归方式,挺好理解,就是提取一列作为结果的一行,连接上其余列的转置,就是这js太难看了。 function transpose__(matrix) { return r.ifElse( r.isEmpty, r.always([]), r.converge( r.concat, [ r.o(r.of, r.map(r.head)), r.compose(transpose__, r.filter(r.has(0)), r.map(r.tail)), ]) )(matrix)}
// 是时候来两行毁天灭地的 Haskell 代码了transpose [] = []transpose xss = [head xs| xs <- xss]:transpose [xst | xs <- xss, let xst = tail xs, not . null $ xst]
如何测试呢,尤其在pipeline很长的情况下?
// 其实可以很容易的在pipeline中间加上输出来观察const longPipe = r.pipe( r.map(e=>e*e), r.tap(console.log), r.map(r.ifElse( r.lt(r.__,50), r.add(100), r.subtract(r.__,200))), r.tap(console.log), r.sum, r.tap(console.log))
longPipe(r.range(1,20))
关于排序,你其实可以更敏捷的构造。
// 当你有个树形对象o = { a:1, b:{ c:1, d:{e:3} }}
// 如果我们有形如上面的数组数据,要对其排序,筛选等等// 对于排序,我们需要的,仅仅是如何取到一个key(透镜),以及是升序还是降序(我只想排序,不想在意是a-b还是b-a)const sortFn = r.sort(r.ascend(r.path(r.split('.','b.d.e')))) // decend 降序
反向映射
const invert = r.pipe( r.toPairs, r.map(r.reverse), r.fromPairs)console.log(invert({ a: 1, b: 2, c: 3 })); // { '1': 'a', '2': 'b', '3': 'c' }
对象扁平化
const flattenObj = obj => { const go = obj_ => chain(([k, v]) => { if (type(v) === 'Object' || type(v) === 'Array') { return map(([k_, v_]) => [`${k}.${k_}`, v_], go(v)) // 递归go并将前缀k连接上去 } else { return [[k, v]] } }, toPairs(obj_)) // 递归 return fromPairs(go(obj))}flattenObj({a:1, b:{c:3}, d:{e:{f:6}, g:[{h:8, i:9}, 0]}})
对象键改名,通常会用作数据的适配
const map = { "a.b.c": "ab.c", "a.b.e": "e", q: "d"};
const obj = { a: { b: { c: 1, e: 2, f: 3 } }, q: 3};
// 技艺不精,写不出更好的代码了const renameKeys = map => { // [(oldPath, newPath)] const parsedMap = r.pipe( r.toPairs, r.map(r.ap([r.split(".")])) )(map);
return obj => r.reduce( (acc, [oldpath, newpath]) => r.pipe( r.dissocPath(oldpath), r.assocPath(newpath, r.path(oldpath, acc)) )(acc), r.clone(obj), parsedMap );};
console.log(renameKeys(map)(obj)); // { a: { b: { f: 3 } }, ab: { c: 1 }, e: 2, d: 3 }
啥玩意?你还在写这种代码?|
总结
大多数程序员都是从类C语言起步的,使用命令式开发可以进行很精细的逻辑控制,要成功的转变编程思维不会很容易。函数式代码,与命令式的不同,就是如何把复杂过程简化、降维,降低思考的负担,减少思考的次数,最大限度的复用代码。
函数式编程提取了过程的模式,并将它们以更加简练的方式表达出来加以复用。
使用组合,与使用无状态的管道过滤器有着惊人的相似性。我们可以直接使用它,或是把它放入其他管道作为一个中间过程。
直线地思考问题,应该避免分支,如果有,也要确保他们能返回一致的类型,最理想的情况是在纯函数中使用表达式。
纯函数应该始终纯净,注意,js 语言本身并不能强迫我们像Haskell 一样将副作用绝对隔离,所以在函数组件当中,不要使用与props无关的数据(非参数变量),也不应该修改props上的属性。
尽量不要使用null和undefined,他们是面向对象和弱类型语言中最为失败的产物,软件行业已经为此付出了十分惨痛的代价。
好吧,是不是应该来点实在的
React 与 函数式编程这个部分我就不写代码了。 React官方定位是个专注于构建用户界面的库,使用JSX语法,提倡组件化……(废什么话)首先,React的一切,都是组件,尤其开发者们喜欢使用JSX语法来构建出组件(相当符合直觉)。我们可以很容易的通过嵌套组件来往组件上挂子组件,设置props来定制化组件的外观和行为。考虑React组件的生命周期,其实每个组件都有这样一个过程,创建、渲染、挂载、[渲染、更新]、 卸载。
创建:我们创建组件使用的语法JSX,实际上创建了一个组件的简易描述,称之React元素,有别于虚拟DOM,虚拟DOM是真正的React组件的实例,有着自己的生命周期,React元素仅仅用于提供给React实例关于组件树的描述、以及传递属性。React运作的过程中,接受React元素,并与React实例比较,如果React实例没有创建出来,那就根据React元素提供的线索(type属性,组件的构造函数),创建出对应的React实例,如果React实例已经存在,就比较他们的类型以及key,如果一致,就将React元素交给React实例,React实例决定是否更新、渲染。渲染:React组件实例中都有一个#render方法,#render的返回,其实也是React元素,返回值给了谁,子组件,React通过这种方式,递归的进行着渲染,渲染可能和大家想象的不太一样,实际上做的是将React元素递归分发给子组件。挂载:React系统使得React组件实例所代表的内容与真实DOM显示一致的过程,称为挂载。掐指一算,嗯,子组件挂载一定先于父组件(只有所有子组件都跟真实DOM一致了,父组件才算挂载完成)。更新:有两种情况会导致组件的更新,一是父组件#render调用,分发React元素给子组件,子组件的props发生改变;二是组件自身调用了#setState,改变了state。props或state发生了变化,就有可能触发更新,取决于#shouldComponentUpdate和#forceUpdate的调用情况,大家肯定比我懂啦。卸载:当父组件自己崩了,或者在#render之后意识到子组件没有与之对应的React元素,就会与子组件断绝关系,即子组件卸载。
好吧,上面内容是我YY出来的,因为我也没看过源码,但我觉得React大概就是这样运作的。
说这么多有什么用?我们的主题是函数式编程。从上面的过程中可以看出,React在运作的过程中,组件树的构建、更新,即数据的传递,都是从根组件到叶组件方向进行的。单向数据流:优秀React开发者认为,React应该有两种组件,容器组件与视图组件。容器组件容纳应用的数据、发起网络请求、响应来自视图组件的事件(IOC);视图组件接受容器组件传来的props数据并展示出来,视图组件上可以有状态、可以有交互,但是对容器组件数据的修改必须以事件的方式通知容器组件(视图组件不应该对容器组件的构成有任何认知,即它是被调用者,它接受容器的数据,但不能改变数据),容器组件响应事件并决定应用状态的改变,容器状态的改变又会反过来重新渲染视图组件,从而视图组件的显示内容与容器组件的相应状态保持一致。这种组件间的交互方式就称为数据下传,动作上传。
那么这种数据流应该用什么模型表示的呢?万能的Monad,我就知道你按耐不住了。Component :: [(k, v)] -> DomElement -> IO ()Component status dom = do (newDom, port) <- render status dom -- 渲染 (end, action) <- listen port -- 监听 if end then unMount newDom -- 卸载 else do let newStatus = reduce status action--根据action和当前的status获得新状态 Component newStatus newDom --状态的改变导致重新渲染,这是一种递归的Monad
这大概就是函数响应式了吧。
我与函数式的孽缘一次偶然,我在图书馆摸到了一本《Haskell趣学指南》,从此打开了新世界的大门,这门语言简洁的令人爱不释手,编码如写作,思路如潮涌。如果Scheme是编程语言的女王,那么Haskell应该是国王了。|
看过这张图,大概就明白了,Haskell是一门十分强悍的语言。 模式匹配功能(可以解构赋值)、完全柯里化、强大的高阶函数、可以自定义的运算符、惰性求值、严谨的代数类型系统。样样都走在编程语言的前列。它构造过程的能力异常的强大,尝试接触他,将大大提升一个人的编程思维,看待问题更能抓住本质。 有人说,Haskell注定只能是学术界的玩具,正是由于它在学术界酝酿多年,才得以快速发展,实际上,已经有一些公司选择用Haskell来开发软件系统了,在安全性上无出其右。
Last updated
Was this helpful?