摘要: JS函数式编程入门。javascript
Fundebug经受权转载,版权归原做者全部。前端
本系列的其它篇:java
引用透明是一个富有想象力的优秀术语,它是用来描述纯函数能够被它的表达式安全的替换,经过下例来帮助咱们理解。git
在代数中,有一个以下的公式:程序员
y = x + 10
复制代码
接着:github
x = 3
复制代码
而后带入表达式:编程
y = 3 + 10
复制代码
注意这个方程仍然是有效的,咱们能够利用纯函数作一些相同类型的替换。segmentfault
下面是一个 JavaScript 的方法,在传入的字符串两边加上单引号:数组
function quote (str) {
retrun "'" + str + "'"
}
复制代码
下面是调用它:浏览器
function findError (key) {
return "不能找到 " + quote(key)
}
复制代码
当查询 key 值失败时,findError 返回一个报错信息。
由于 quote 是纯函数,咱们能够简单地将 quote 函数体(这里仅仅只是个表达式)替换掉在findError中的方法调用:
function findError (key) {
return "不能找到 " + "'" + str + "'"
}
复制代码
这个就是一般所说的**“反向重构”**(它对我而言有更多的意义),能够用来帮程序员或者程序(例如编译器和测试程序)推理代码的过程一个很好的方法。如,这在推导递归函数时尤为有用的。
大多数程序都是单线程的,即一次只执行一段代码。即便你有一个多线程程序,大多数线程都被阻塞等待I/O完成,例如文件,网络等等。
这也是当咱们编写代码的时候,咱们很天然考虑按次序来编写代码:
1. 拿到面包
2. 把2片面包放入烤面包机
3. 选择加热时间
4. 按下开始按钮
5. 等待面包片弹出
6. 取出烤面包
7. 拿黄油
8. 拿黄油刀
9. 制做黄油面包
复制代码
在这个例子中,有两个独立的操做:拿黄油以及 加热面包。它们在 步骤9 时开始变得相互依赖。
咱们能够将 步骤7 和 步骤8 与 步骤1 到 步骤6 同时执行,由于它们彼此独立。当咱们开始作的时候,事情开始复杂了:
线程一
--------------------------
1. 拿到面包
2. 把2片面包放入烤面包机
3. 选择加热时间
4. 按下开始按钮
5. 等待面包片弹出
6. 取出烤面包
线程二
-------------------------
1. 拿黄油
2. 拿黄油刀
3. 等待线程1完成
4. 取出烤面包
复制代码
果线程1失败,线程2怎么办? 怎么协调这两个线程? 烤面包这一步骤在哪一个线程运行:线程1,线程2或者二者?
不考虑这些复杂性,让咱们的程序保持单线程会更容易。可是,只要可以提高咱们程序的效率,要付出努力来写好多线程程序,这是值得的。
然而,多线程有两个主要问题:
可是,若是顺序可有可无,全部事情都是并行执行的呢?
尽管这听起来有些疯狂,但其实并不像听起来那么混乱。让咱们来看一下 Elm 的代码来形象的理解它:
buildMessage message value =
let
upperMessage =
String.toUpper message
quotedValue =
"'" ++ value ++ "'"
in
upperMessage ++ ": " ++ quotedValue
复制代码
这里的 buildMessage 接受参数 message 和 value,而后,生成大写的 message和 带有引号的 value 。
注意到 upperMessage 和 quotedValue 是独立的。咱们怎么知道的呢?
在上面的代码示例中,upperMessage 和 quotedValue 二者都是纯的而且没有一个须要依赖其它的输出。
若是它们不纯,咱们就永远不知道它们是独立的。在这种状况下,咱们必须依赖程序中调用它们的顺序来肯定它们的执行顺序。这就是全部命令式语言的工做方式。
第二点必须知足的就是一个函数的输出值不能做为其它函数的输入值。若是存在这种状况,那么咱们不得不等待其中一个完成才能执行下一个。
在本例中,upperMessage 和 quotedValue 都是纯的而且没有一个须要依赖其它的输出,所以,这两个函数能够以任何顺序执行。
编译器能够在不须要程序员帮助的状况下作出这个决定。这只有在纯函数式语言中才有可能,由于很难(若是不是不可能的话)肯定反作用的后果。
在纯函数语言中,执行的顺序能够由编译器决定。
考虑到 CPU 没法一再的加快速度,这种作法很是有利的。别一方面,生产商也不断增长CPU内核芯片的数量,这意味着代码能够在硬件层面上并行执行。使用纯函数语言,就有但愿在不改变任何代码的状况下充分地发挥 CPU 芯片的功能并取得良好成效。
在静态类型语言中,类型是内联定义的。如下是 Java 代码:
public static String quote(String str) {
return "'" + str + "'";
}
复制代码
注意类型是如何同函数定义内联在一块儿的。当有泛型时,它变的更糟:
private final Map<Integer, String> getPerson(Map<String, String> people, Integer personId) {
// ...
}
复制代码
这里使用粗体标出了使它们使用的类型,但它们仍然会让函数可读性下降,你必须仔细阅读才能找到变量的名称。
对于动态类型语言,这不是问题。在 Javascript 中,能够编写以下代码:
var getPerson = function(people, personId) {
// ...
};
复制代码
这样没有任何的的类型信息更易于阅读,惟一的问题就是放弃了类型检测的安全特性。这样可以很简单的传入这些参数,例如,一个 Number 类型的 people 以及一个 Objec t类型的 personId。
动态类型要等到程序执行后才能知道哪里问题,这多是在发布的几个月后。在 Java 中不会出现这种状况,由于它不能被编译。
可是,假如咱们能同时拥有这二者的优异点呢? JavaScript 的语法简单性以及 Java 的安全性。
事实证实咱们能够。下面是 Elm 中的一个带有类型注释的函数:
add : Int -> Int -> Int
add x y =
x + y
复制代码
请注意类型信息是在单独的代码行上面的,而正是这样的分割使得其有所不一样。
如今你可能认为类型注释有错训。 第一次见到它的时候。 大都认为第一个 -> 应该是一个逗号。能够加上隐含的括号,代码就清晰多了:
add : Int -> (Int -> Int)
复制代码
上例 add 是一个函数,它接受类型为 Int 的单个参数,并返回一个函数,该函数接受单个参数 Int类型 并返回一个 Int 类型的结果。
如下是一个带括号类型注释的代码:
doSomething : String -> (Int -> (String -> String))
doSomething prefix value suffix =
prefix ++ (toString value) ++ suffix
复制代码
这里 doSomething 是一个函数,它接受 String 类型的单个参数,而后返回一个函数,该函数接受 Int 类型的单个参数,而后返回一个函数,该函数接受 String 类型的单个参数,并返回一个字符串。
注意为何每一个方法都只接受一个参数呢? 这是由于每一个方法在 Elm 里面都是柯里化。
由于括号老是指向右边,它们是没必要要的,简写以下:
doSomething : String -> Int -> String -> String
复制代码
当咱们将函数做为参数传递时,括号是必要的。若是没有它们,类型注释将是不明确的。例如:
takes2Params : Int -> Int -> String
takes2Params num1 num2 =
-- do something
复制代码
很是不一样于:
takes1Param : (Int -> Int) -> String
takes1Param f =
-- do something
复制代码
takes2Param 函数须要两个参数,一个 Int 和另外一个 Int,而takes1Param 函数须要一个参数,这个参数为函数, 这个函数须要接受两个 Int 类型参数。
下面是 map 的类型注释:
map : (a -> b) -> List a -> List b
map f list =
// ...
复制代码
这里须要括号,由于 f 的类型是(a -> b),也就是说,函数接受类型 a 的单个参数并返回类型 b 的某个函数。
这里类型 a 是任何类型。当类型为大写形式时,它是显式类型,例如 String。当一个类型是小写时,它能够是任何类型。这里 a 能够是字符串,也能够是 Int。
若是你看到 (a -> a) 那就是说输入类型和输出类型必须是相同的。它们是什么并不重要,但必须匹配。
但在 map 这一示例中,有这样一段 (a -> b)。这意味着它既能返回一个不一样的类型,也能返回一个相同的类型。
可是一旦 a 的类型肯定了,a 在整段代码中就必须为这个类型。例如,若是 a 是一个 Int,b 是一个 String,那么这段代码就至关于:
(Int -> String) -> List Int -> List String
复制代码
这里全部的 a 都换成了 Int,全部的 b 都换成了 String。
List Int 类型意味着一个值都为 Int 类型的列表, List String 意味着一个值都为 String 类型的列表。若是你已经在 Java 或者其余的语言中使用过泛型,那么这个概念你应该是熟悉的
JavaScript 拥有不少类函数式的特性但它没有纯性,可是咱们能够设法获得一些不变量和纯函数,甚至能够借助一些库。
但这并非理想的解决方法。若是你不得不使用纯特性,为什么不直接考虑函数式语言?
这并不理想,但若是你必须使用它,为何不从函数式语言中得到一些好处呢?
首先要考虑的是不变性。在ES2015或ES6中,有一个新的关键词叫const,这意味着一旦一个变量被设置,它就不能被重置:
const a = 1;
a = 2; // 这将在Chrome、Firefox或 Node中抛出一个类型错误,但在Safari中则不会
复制代码
在这里,a 被定义为一个常量,所以一旦设置就不能更改。这就是为何 a = 2 抛出异常。
const 的缺陷在于它不够严格,咱们来看个例子:
const a = {
x: 1,
y: 2
};
a.x = 2; // 没有异常
a = {}; // 报错
复制代码
注意到 a.x = 2 没有抛出异常。const 关键字惟一不变的是变量 a, a 所指向的对象是可变的。
那么Javascript中如何得到不变性呢?
不幸的是,咱们只能经过一个名为 Immutable.js 的库来实现。这可能会给咱们带来更好的不变性,但遗憾的是,这种不变性使咱们的代码看起来更像 Java 而不是 Javascript。
在本系列的前面,咱们学习了如何编写柯里化函数,这里有一个更复杂的例子:
const f = a => b => c => d => a + b + c + d
复制代码
咱们得手写上述柯里化的过程,以下:
console.log(f(1)(2)(3)(4)); // prints 10
复制代码
括号如此之多,但这已经足够让Lisp程序员哭了。有许多库能够简化这个过程,我最喜欢的是 Ramda。
使用 Ramda 简化以下:
const f = R.curry((a, b, c, d) => a + b + c + d);
console.log(f(1, 2, 3, 4)); // prints 10
console.log(f(1, 2)(3, 4)); // also prints 10
console.log(f(1)(2)(3, 4)); // also prints 10
复制代码
函数的定义并无好多少,可是咱们已经消除了对那些括号的须要。注意,调用 f 时,能够指定任意参数。
重写一下以前的 mult5AfterAdd10 函数:
const add = R.curry((x, y) => x + y);
const mult5 = value => value * 5;
const mult5AfterAdd10 = R.compose(mult5, add(10));
复制代码
事实上 Ramda 提供了不少辅助函数来作些简单常见的运算,好比R.add以及R.multiply。以上代码咱们还能够简化:
const mult5AfterAdd10 = R.compose(R.multiply(5), R.add(10));
复制代码
Ramda 也有本身的 map、filter和 reduce 版本。虽然这些函数存在于数组中。这几个函数是在 Array.prototype 对象中的,而在 Ramda 中它们是柯里化的
const isOdd = R.flip(R.modulo)(2);
const onlyOdd = R.filter(isOdd);
const isEven = R.complement(isOdd);
const onlyEven = R.filter(isEven);
const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(onlyEven(numbers)); // prints [2, 4, 6, 8]
console.log(onlyOdd(numbers)); // prints [1, 3, 5, 7]
复制代码
R.modulo 接受2个参数,被除数和除数。
isOdd 函数表示一个数除 2 的余数。若余数为 0,则返回 false,即不是奇数;若余数为 1,则返回 true,是奇数。用 R.filp 置换一下 R.modulo 函数两个参数顺序,使得 2 做为除数。
isEven 函数是 isOdd 函数的补集。
onlyOdd 函数是由 isOdd 函数进行断言的过滤函数。当它传入最后一个参数,一个数组,它就会被执行。
同理,onlyEven 函数是由 isEven 函数进行断言的过滤函数。
当咱们给函数 onlyEven 和 onlyOd 传入 numbers,isEven 和 isOdd 得到了最后的参数,而后执行最终返回咱们指望的数字。
全部的库和语言加强都已经获得了Javascript 的发展,但它仍然面临着这样一个事实:它是一种强制性的语言,它试图为全部人提供全部的东西。
大多数前端开发人员都不得不使用 Javascript,由于这旨浏览器也识别的语言。相反,它们使用不一样的语言编写,而后编译,或者更准确地说,是把其它语言转换成 Javascript。
CoffeeScript 是这类语言中最先的一批。目前,TypeScript 已经被 Angular2 采用,Babel能够将这类语言编译成 JavaScript,愈来愈多的开发者在项目中采用这种方式。
可是这些语言都是从 Javascript 开始的,而且只稍微改进了一点。为何不直接从纯函数语言转换到Javascript呢?
咱们不可能知道将来会怎样,但咱们能够作一些有根据的猜想。如下是做者的一些见解:
但愿这系列文章能帮助你更好容易更好帮助你理解函数式编程及优点,做者相信函数式编程是将来趋势,你们有时间能够多多了解,接着提高大家的技能,而后将来有更好的出路。
原文:
编辑中可能存在的bug无法实时知道,过后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具Fundebug。
你的点赞是我持续分享好东西的动力,欢迎点赞!
一个笨笨的码农,个人世界只能终身学习!
更多内容请关注公众号《大迁世界》!