荷枪实弹的练习
在浏览器中,你可能需要去跑这样的一段代码
function showList(ssn) {
var student = db.get(ssn)
if(student!==null) {
// 读取了外部变量elementId
document.querySelector(`#${elementId}`).innerHTML = `${student.ssn} ${student.firstname}`
} else {
throw new Error('Student not found') // 抛出异常
}
}
分析这段代码:
- 该函数为访问数据,于一个外部变量进行了交互,应为该函数声明中没有该参数,如果在一个时间点,该变量的引用为null,则会导致不可想象的后果,从而破坏了程序的完整性
- 全局变量elementId可能随时改变
- html元素被直接修改了,HTML文档本身是一个可变的、共享的全局资源
- 如果没有找到该学生,会抛出一个异常,导致程序栈回退并突然结束
我们可以对该长函数进行分解,分解的思路为
- 长函数改为职责单一的短函数
- 通过显示定义形参完成函数所需要的依赖来定义为函数参数来减少副作用
此处使用了柯里化的技巧,柯里化允许部分地传递函数的参数,从而将函数的参数减少为一个
const find = curry((db,id)=> {
let obj = db.get(id)
if(obj === null) return throw new Error('Object not found')
return obj
})
var csv = function(student){
return `${student.ssn} ${student.firstname}`
}
var append = curry(function(){
document.querySelector(elementId).innerHTML = info
})
var showStudent = run(append('#student-info'),csv,find(db))
showStudent('444-44-4444')
这司马书没有给这个柯里化函数的解释,就说了以下优点
- 灵活,现在又三个可以复用的函数
- 这种小函数的复用可以提高工作效率,大大减少需要主动维护的代码量
- 这种代码风格提高了额程序愮执行那些高阶函数一个清晰视图(我他妈都快晕了),提高了代码的可读性
- 于HTML对象的交互移动到了一个单独的函数中,将纯函数从不纯的行为抽离了出来。
引用透明与可置换性
引用透明是顶一个函数较为正确的方式,纯度指入参与返回值映射的关系。例如刚刚严重依赖全局变量的counter
引用透明指对于一个函数,对于相同的输入始终产生相同的输出结果,那么就说它是引用透明的。
为什么说counter那个函数不是引用透明的呢?这是因为他的输出结果严重依赖外部变量,影响函数连续调用的结果。
我们追求函数的这种特质,是因为它能方便测试,还可以更容易的推理整个程序。
// 命令式版本
var counter = 0
function increment() {
return ++counter
}
// 函数式版本
var increment = (counter)=> {
return counter+1
}
然我们初窥run
// 命令式版本
increment()
increment()
console.log(counter)
// 函数式版本
var plus2 = run(increment,increment)
console.log(run(0)) // 2
构建这样的程序更容易推理,例如,gulp推荐将一个任务组合
export default = gulp.service(css,javascript,htmloutput,systemBuild)
假设,任何程序为一组函数的执行,对于一个给定的输入,会产生一个输出
则可以表示为 : Program = [input] + [fn1,fn2,fn3,...] -> output
更因为fn...是纯函数,所以可以推导为Program = [input] + [val1,val2,val3,...] ->output
例如学生的平均成绩
var input =[80,90,100]
var average = arr=>divide(sum(arr),size(arr))
average(input) // ->90
由于函数sum和size都是引用透明的,对于给定的输入,可以很容易的推到这个表达式。
var average = divide(270,3)
对于又副作用的函数,这种推导基本是不可能的
存储不可变数据
不可变数据指那些被创建后不能修改的数据,同绝大部分语言一样,javascript的所有基本类型(String、Boolean、Number)从本质上来讲是不可变的。但是对象里的属性是可以改变的。
var sortDesc = function(arr) {
return arr.sort((a,b)=> {
return b-a
})
}
这种修改改变了arr的原始引用,实际上,这种情况在reducer会很常见。
函数式编程是指创建不可变的程序,通过消除外部可见的副作用,来对纯函数的声明式求值的过程
如果想要改变 ; 必须强迫自己思考纯的操作,将函数看作永不会修改入参的闭合功能单元。
函数式编程的优点
- 将任务分解为简单的函数
- 使用流式的调用链
- 通过响应式范式降低事件驱动代码的复杂性(?)
鼓励复杂任务的分解
从宏观上讲,函数式编程实际上是分解和组合之间的相互作用。正式这种二元性,使得函数式程序模块化与高效
run函数可以组合各种函数,从而实现整个程序,在现实中,run是组合的别名,两个函数组合成一个新的函数(类似柯里化),它将第一个函数的输出,传递到另外一函数中,假设函数f和g,形式上,其组合如下描述 : f*g = f(g(x))
// *其实是.
这个公式读作"f组合g",他在g的返回值与f的参数之间构建一个送耦合且安全的联系。两个函数的组成前提是 上一个函数的返回结果作为下一个函数的入参必须数目及类型一致
现在使用compose构建组合函数showStudent
var showStudent = compose(append('#student-info'),csv,find(db))
showStudent('444-44-444')
没错,它的执行顺序是反的。
graph LR
A-->B
444-44-444 -> find -> student -> csv ->ssn.firstname -> append
函数链式调用
还记得$('#id').append('<div>23232</div>')
这样的函数调用么?
这是一种惰性计算程序,这意味着当需要时才会执行。(不太懂)
网友评论