diff --git a/README-EN.md b/README-EN.md index 93e8a5e..986dcc5 100644 --- a/README-EN.md +++ b/README-EN.md @@ -10,6 +10,7 @@ This document is compiled from [RubyFunctionalProgramming](http://code.google.co * [The theory](#the-theory) * [Functional programming in Ruby](#functional-programming-in-ruby) * [Don't update variables](#dont-update-variables) + * [Don't reuse variables](#dont-reuse-variables) * [Blocks as higher order functions](#blocks-as-higher-order-functions) * [OOP and funcional programming](#oop-and-funcional-programming) * [Everything is an expression](#everything-is-an-expression) @@ -90,7 +91,7 @@ Yes: ```ruby hash = {:a => 1, :b => 2} -new_hash = hash.merge(:c => 3) +new_hash = hash.merge(:c => 3) ``` #### Don't use bang methods which modify in-place @@ -127,6 +128,24 @@ Yes: output = [1, (2 if i_have_to_add_two), 3].compact ``` +### Don't reuse variables + +That's a common pattern we should avoid: + +```ruby +number = gets +number = number.to_i +``` + +While here we're not updating number but overriding the old variable, if updating variables is bad (from a FP perspective), so is overriding them. The principle is the same: once you write number = gets, number should have the same value for all the scope. If you want to apply some transformation, just use different names: + +```ruby +number_string = gets +number = number_string.to_i +``` + +Remember, as in math, var = value, should be a sacred contract between the coder and the future reader of the code: every time var is found in the scope, you can substitute it by value. + ### Blocks as higher order functions If a language is to be used functionally we need higher-order functions. That's it, functions can take other functions as parameters, and can also return other functions. @@ -264,7 +283,7 @@ def get_best_object(obj1, obj2, obj3) end ``` -Can be written as a real expression like this: +We are making the code harder to reader just to save some lines. Don't do that. This can be written as a more clear expression like this: ```ruby def get_best_object(obj1, obj2, obj3) @@ -359,7 +378,7 @@ Rich Hickey, the creator of Clojure (a functional Lisp-dialect for the JVM), dis You may write this: ```ruby -if found_dog == our_dog +if found_dog == our_dog name = found_dog.name message = "We found our dog #{name}!" else @@ -502,7 +521,7 @@ require 'enumerable/lazy' 2. Not so obvious: Lazy evaluation makes possible to write code that does not know (not want to know) more that it needs to. Let's see an example: you wrote a solver of some kind that yields infinite number of solutions, but at some point you only want to get the first 10. You'd write something like: ```ruby -solver(input, :max => 10) +solver(input, :max => 10) ``` When you are working with lazy structures there is no need to say when to stop. The caller decides how many values it wants. The code becomes simplier and the responsibility goes where it should be, to the caller: diff --git a/RADME-zhCN.md b/README-zhCN.md similarity index 56% rename from RADME-zhCN.md rename to README-zhCN.md index 0d14818..32b093b 100644 --- a/RADME-zhCN.md +++ b/README-zhCN.md @@ -6,19 +6,20 @@ ## 目录 -* [简介](#-1) -* [理论部分](#-2) -* [Ruby的函数式编程](#ruby) - * [不要更新变量](#-3) - * [用 Blocks 作为高阶函数](#-blocks-) - * [面向对象与函数式编程](#-6) - * [万物皆表达式](#-7) - * [递归](#-8) - * [惰性枚举器](#-9) - * [一个实际的例子](#-11) -* [结论](#-12) -* [简报](#-13) -* [延伸阅读](#-14) +* [简介](#简介) +* [理论部分](#理论部分) +* [Ruby的函数式编程](#ruby-的函数式编程) + * [不要更新变量](#不要更新变量) + * [不要重用变量](#不要重用变量) + * [用 Blocks 作为高阶函数](#用-blocks-作为高阶函数) + * [面向对象与函数式编程](#面向对象与函数式编程) + * [万物皆表达式](#万物皆表达式) + * [递归](#递归) + * [惰性枚举器](#惰性枚举器) + * [一个实际的例子](#一个实际的例子) +* [结论](#结论) +* [简报](#简报) +* [延伸阅读](#延伸阅读) ## 简介 @@ -27,39 +28,39 @@ x = x + 1 -在以前上小学的美好回忆裡,我们可能都曾对上面这个式子感到困惑。这个 `x` 到底是什么呢?为什么加了一之后,`x` 仍然还是 `x`。 +在美好的小学时光里,我们会对上面这行感到困惑。这个 `x` 到底是什么呢?为什么加了一之后,`x` 仍然还是 `x`。 -不知道为什么,我们就开始写程序了,也就不在乎这是为什么了。心想:“嗯”,“这不是什么大问题,编程就是事情做完最重要,没有必要去挑剔数学的纯粹性 (让大学裡的大鬍子教兽们去烦恼就好)” 。但我们错了,也因此付出极高的代价,只因我们不了解它。 +不知道为什么,当我们就开始编程时,就不在乎这是为什么了。心想:“嗯”,“这不是什么大问题,编程中将事情做完最重要,没有必要去挑剔数学的纯粹性 (让大学里的大鬍子教兽们去烦恼就好)” 。但我们错了,也付出极高的代价,只是我们还不知道而已。 ## 理论部分 [维基百科](http://en.wikipedia.org/wiki/Functional_programming)的解释:“函数式编程是一种写程序的范式,将计算视为对数学函数的求值,并避免使用状态及可变的数据” 换句话说,函数式编程提倡没有副作用的代码,不改变变量的值。这与命令式编程相反,命令式编程强调改变状态。 -令人惊讶的是,函数式编程就这样而已。那…有什么好处呢? +仅此而已。那有什么好处呢? * 更简洁的代码:“变量”一旦定义之后就不再改动,所以我们不需要追踪变量的状态,就可以理解一个函数、方法、类别、甚至是整个项目是怎么工作的。 -* 引用透明:表达式可以用本身的值换掉。如果我们用同样的参数调用一个函数,我们确信输出会是一样的结果(没有其它的状态可改变它的值)。这也是为什么爱因斯坦说:“重复做一样的事却期望不同的结果”是疯狂的理由。 +* 引用透明:表达式可以用本身的值换掉。如果我们用同样的参数调用一个函数,可以确信输出会是一样的结果(没有其它的状态可改变它的值)。这也是为什么爱因斯坦说:“重复做一样的事却期望不同的结果”是疯狂的理由。 引用透明打开了前往某些美妙事物的大门 -* 并行化:如果调用函数是各自独立的,则他们可以在不同的进程甚至是机器裡执行,而不会有竞态条件的问题。“平常” 写并发程序讨厌的细节(锁、semaphore…等)在函数式编程裡面通通消失不见了。 +* 并行化:如果调用函数是各自独立的,则他们可以在不同的进程甚至是机器里执行,而不会有竞态条件的问题。“平常” 写并发程序讨厌的细节(锁、semaphore…等)在函数式编程里面通通消失不见了。 * 记忆化:由于函数调用的结果等于它的返回值,我们可以把这些值缓存起来。 -* 模组化:代码裡不存有状态,所以我们可以将项目用小的黑箱连结起来,函数式编程提倡自底向上的编程风格。 +* 模组化:代码里不存有状态,所以我们可以将项目用小的黑箱连结起来,函数式编程提倡自下而上的编程风格。 * 容易调试:函数彼此互相隔离,只依赖输入与输出,所以很容易调试。 -## Ruby的函数式编程 +## Ruby 的函数式编程 -一切都是这么美好,但怎样才能将函数式编程,应用到每天写 Ruby(Ruby 不是个函数式语言)的程序开发裡呢?函数式编程广义来说,是一种风格,可以用在任何语言。当然啦,用在特别为这种范式打造的语言裡显得更自然,但某种程度上来说,可以应用到任何语言。 +听着不错,但怎样才能在每天的 Ruby 编程中运用函数式编程呢(Ruby 不是函数式语言)?函数式编程广义来说,是一种风格,任何语言都能使用。用在特别为这种范式打造的语言里当然会更自然,但某种程度上来说,可以应用到任何语言。 -让我们先釐清这一点:本文没有要提倡古怪的风格,比如仅仅为了要延续理论函数式编程的纯粹性所带来的古怪风格。反之,我想说的重点是,我们应该 **当可以提昇代码品质时,才使用函数式编程** ,不然这只不过是个糟糕的解决办法。 +说明一点:本文不提倡仅仅为了保持理论上的函数纯粹性去编写古怪风格的代码。相反,我要表达的是 **只要能提升代码品质,就可以用使用函数式编程** 。 ### 不要更新变量 -别更新它们,创造新的变量。 +别更新它们,创建新的变量。 #### 不要对数组或字串做 `append` @@ -95,7 +96,7 @@ hash = {:a => 1, :b => 2} new_hash = hash.merge(:c => 3) ``` -#### 牵扯到内存位置的地方,不要使用破坏性方法。 +#### 不要使用有破坏性的方法,即那些带惊叹号的方法 No: @@ -129,9 +130,27 @@ Yes: output = [1, (2 if i_have_to_add_two), 3].compact ``` +#### 不要重用变量 + +我们应该避免的常见模式: + +```Ruby +number = gets +number = number.to_i +``` + +这里,我们没有更新 number,但用新的值冲掉了之前的值。从函数式变成的观点看,更新变量和冲掉之前的变量都是不好的。它们都违背了同样的原则:一旦写下 number = gets , number 在其 scope 中值就不该变化。如果要做转化,使用其它变量名: + +```Ruby +number_string = gets +number = number_string.to_i +``` + +记住,把 “变量 = 值” 中的等号看做是数学中的等号。一旦等于了,就不应该改变。这该是程序员和将来读该代码的人之间的神圣约定:scope 中的变量总是可以直接被它们的值所替代。 + ### 用 Blocks 作为高阶函数 -如果一个语言要搞函数式,会需要高阶函数。高阶函数是什么?函数可以接受别的函数作为参数,并可以返回函数,就这么简单。 +语言需要有高阶函数才能支持函数式编程。高阶函数是可以接受别的函数作为参数,并可以返回函数的函数。 Ruby (与 Smalltalk 还有其它语言)在这个方面上非常特别,语言本身就内置这个功能: **blocks** 区块。区块是一段匿名的代码,你可以随意的传来传去或是执行它。让我们看区块的典型用途,来构造函数式编程的构造子。 @@ -197,7 +216,7 @@ length = ["milu", "rantanplan"].inject(0) do |accumulator, dog_name| end # => 14 ``` -在这个特殊情况下,当累积器与元素之间有操作进行时,我们不需要区块,只要将操作传给符号即可。 +在上例这种特殊情况下,累积器与元素之间的操作很简单,我们不需要区块,只要将二元操作符传给符号初始值即可。 ```Ruby length = ["milu", "rantanplan"].map(&:length).inject(0, :+) # 14 @@ -205,7 +224,7 @@ length = ["milu", "rantanplan"].map(&:length).inject(0, :+) # 14 #### empty + each + accumulate + push -> scan -想像一下,你不仅想要摺迭(fold)的结果,也想要过程中产生的部分数值。用命令式编程风格,你可能会这么写: +想像一下,你不仅想要摺迭(fold)的结果,也想得到过程中产生的部分值。用命令式编程风格,你可能会这么写: ```Ruby lengths = [] @@ -217,7 +236,7 @@ end lengths # [0, 4, 14] ``` -在函数式的世界裡,Haskell 称之为 [scan](http://zvon.org/other/haskell/Outputprelude/scanl_f.html), C++ 称之为 [partial_sum](http://www.cplusplus.com/reference/std/numeric/partial_sum/), Clojure 称之为 [reductions](http://clojuredocs.org/clojure_core/clojure.core/reductions)。 +在函数式的世界里,Haskell 称之为 [scan](http://zvon.org/other/haskell/Outputprelude/scanl_f.html), C++ 称之为 [partial_sum](http://www.cplusplus.com/reference/std/numeric/partial_sum/), Clojure 称之为 [reductions](http://clojuredocs.org/clojure_core/clojure.core/reductions)。 令人讶异的是,Ruby 居然没有这样的函数!让我们自己写一个。这个怎么样: @@ -227,7 +246,7 @@ lengths = ["milu", "rantanplan"].partial_inject(0) do |dog_name| end # [0, 4, 14] ``` -Enumerable#partial_inject 可以这么实现: +`Enumerable#partial_inject` 可以这么实现: ```Ruby module Enumerable @@ -240,7 +259,7 @@ module Enumerable end ``` -实作的细节不重要,重要的是,当认出一个有趣的模式可以被抽象化时,我们将其写在另一个函式库,撰写文档,反覆测试。现在只要让实际的需求去完善你的扩充即可。 +实现的细节不重要(为了效率我使用了 `<<`),重要的是,当发现有趣的模式可以被抽象化时,我们将其写在函式库中,为其撰写文档,对其反覆测试,然后就可在将来任意项目中使用了。 #### initial assign + conditional assign + conditional assign + ... @@ -252,7 +271,7 @@ name = obj2.name if !name name = ask_name if !name ``` -在此时你应该觉得这样的代码使你很不自在(一个变量一下是这个值,一下是这个;变量名 `name` 到处都是…等)。函数式的方式更简短,也更简洁: +在此时你应该觉得这样的代码使你很不自在(变量一会是这个值,一会是那个值;变量名 `name` 到处都是…等)。函数式的方式更简短,也更简洁: ```Ruby name = obj1.name || obj2.name || ask_name @@ -268,7 +287,7 @@ def get_best_object(obj1, obj2, obj3) end ``` -可以写成像是这样的一个表达式: +初衷是为了节省几行代码,结果是代码变得难读懂了。别这样。写成如下表达式会更清楚: ```Ruby def get_best_object(obj1, obj2, obj3) @@ -282,7 +301,7 @@ def get_best_object(obj1, obj2, obj3) end ``` -确实有一点囉嗦,但逻辑比一堆行内 `if/unless` 来得清楚。经验法则告诉我们,仅在你确定会用到副作用时,使用行内条件式,而不是在变量赋值或返回的场合使用: +确实有一点啰嗦,但逻辑(缩进让它更突出了)比一堆行内 `if/unless` 来得清楚。经验法则告诉我们,仅在你确定会用到副作用时,使用行内条件式,而不是在变量赋值或返回的场合使用: ```Ruby country = Country.find(1) @@ -292,7 +311,7 @@ country.invade if country.has_oil? #### 如何从 enumerable 创造一个 hash -Vanilla Ruby 没有从 Enumerable 转到 Hash 的直接对应(本人认为是一个遗憾的缺陷)。这也是为什么新手持续写出下面这个糟糕的模式(而你怎么能责怪他们呢?唉!): +Ruby 默认没有实现从 Enumerable 直接转到 Hash 的函数(本人认为这个缺陷令人遗憾)。这也是为什么新手持续写出下面这个糟糕的模式(而你又怎么能责怪他们呢?唉!): ```Ruby hash = {} @@ -302,7 +321,7 @@ end hash ``` -这真的非常可怕!阿~~~!但手边有没有更好的办法呢?过去 Hash 构造子需要一个有着连续键值对的 flatten 集合 (阿,用 flatten 数组来描述映射?Lisp 曾这么做,但还是很丑陋)。幸运的是,Ruby 的最新版本也接受键值对,这样更有意义(作为 `hash.to_a` 的逆操作),现在你可以这么写: +丑陋啊!但手边有没有更好的办法呢?过去 Hash 构造子需要一个有着连续键值对的扁平集合 (呃,用 flatten 数组来描述映射?Lisp 曾这么做,但还是很丑陋)。幸运的是,最新版本的 Ruby 也接受键值对,这样更有意义(作为 `hash.to_a` 的逆操作),现在你可以这么写: ```Ruby Hash[input.map do |item| @@ -310,7 +329,7 @@ Hash[input.map do |item| end] ``` -不赖嘛,但这打破了平常的撰写顺序。在 Ruby 我们期望从左向右写,给对象调用方法。而“好的”函数式方式是使用 `inject`: +不赖嘛,但这打破了平常的撰写顺序。在 Ruby 中我们期望从左向右写,给对象调用方法。而“好的”函数式方式是使用 `inject`: ```Ruby input.inject({}) do |hash, item| @@ -318,7 +337,7 @@ input.inject({}) do |hash, item| end ``` -我们都同意这还是很囉嗦,所以我们最好将它放在 Enumerable 模组,[Facets](http://rubyworks.github.com/facets/) 正是这么干的。它称之为 Enumerable#mash: +还是很啰嗦,所以我们最好将它放在 Enumerable 模组,[Facets](http://rubyworks.github.com/facets/) 正是这么干的。命名为 `Enumerable#mash`: ```Ruby module Enumerable @@ -336,7 +355,7 @@ end # {"functional"=>10, "programming"=>11, "rules"=>5} ``` -或使用 `mash` 及 选择性区块来一步完成: +或使用 `mash` 及选择性区块来一步完成: ```Ruby ["functional", "programming", "rules"].mash { |s| [s, s.length] } @@ -347,16 +366,16 @@ end [Joe Armstrong][JA] (Erlang 发明人) 在 “Coders At work” 谈论过面向对象编程的重用性: -“我认为缺少重用性是面向对象语言造成的,而不是函数式语言。面向对象语言的问题是,它们带着语言执行环境的所有隐含资讯四处乱窜。你想要的是香蕉,但看到的却是香蕉拿在大猩猩手裡,而大猩猩的后面是整个丛林” +“我认为缺少重用性是面向对象语言造成的,而不是函数式语言。面向对象语言的问题是,它们带着语言执行环境的所有隐含资讯四处乱窜。你想要的是香蕉,但看到的却是拿着香蕉的大猩猩和整个丛林” 公平点说,我的看法是这不是面向对象编程的本质问题。你可以写出函数式的面向对象程序,但确定的是: * 典型的 OOP 倾向强调改变对象的状态。 * 典型的 OOP 倾向层与层之间紧密的耦合。 -* 典型的 OOP 将同一性(identity)与状态的概念搞溷了。 -* 数据与代码的混合物,导致了概念与实际的问题产生。 +* 典型的 OOP 将同一性(identity)与状态的概念混在一起了。 +* 数据与代码的混合引出了概念与实际操作中的问题。 -[Rich Hickey][RH],Clojure 的发明人(一个给 JVM 用的函数式 Lisp 方言),在这场[出色的演讲](http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey)裡谈论了状态、数值以及同一性。 +[Rich Hickey][RH],Clojure 的发明人(运行在 JVM 上的的函数式 Lisp 方言),在这场[出色的演讲](http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey)里谈论了状态、数值以及同一性。 ### 万物皆表达式 @@ -382,7 +401,7 @@ else end ``` -这样子我们不用重复变量名 `message`,企图也更明显:当有段长的程序(用了一堆我们不在乎的变量),我们可以专注在程序在干什么(返回讯息)。再强调一次,我们在缩小程序的作用域。 +这样子我们不用重复变量名 `message`,意图也更明显。我们可以专注在程序在干什么(返回讯息),而不是冗长的代码和一堆我们并不在乎的变量。再强调一次,我们在缩小程序的作用域。 另一个函数式程序的好处是,表达式可以用来构造数据: @@ -397,9 +416,9 @@ end ### 递归 -纯函数式语言没有隐含的状态,大量利用了递归。为了避免栈溢出,函数式使用一种称为尾递归优化(TCO)的机制。Ruby 1.9 有实作这种机制,但缺省没有打开。要是你希望你的程序,在哪都可以动的话,就不要使用它。 +纯函数式语言没有隐含的状态,大量利用了递归。为了避免栈溢出,函数式使用一种称为尾递归优化(TCO)的机制。Ruby 1.9 实现了这种机制,但缺省没有打开。要是你希望你的程序,在哪都跑的话,就不要用尾递归。 -但是某些情况下,递归仍然是很有用的,即便是每次递归时都创建新的栈。注意!某些递归的用途可以用 foldings 来实现(像 Enumerable#inject)。 +但是某些情况下,递归仍然是很有用的,即便是每次递归时都创建新的栈。注意!某些递归的用途可以用 foldings 来实现(像 `Enumerable#inject`)。 在 MRI-1.9 启用 TCO: @@ -436,9 +455,9 @@ end ### 惰性枚举器 -惰性求值延迟了表达式的求值,在真正需要时才会求值。与 eager evaluation 相反,eager evaluation 当一个变量被赋值时、函数被调用时…甚至根本没用到变量等状况,都立马对表达式求值,惰性不是函数式编程的必需品,但这是个符合函数式范式的好策略(Haskell 大概是最佳的例子,瀰漫着懒惰的语言)。 +惰性求值延迟了表达式的求值,在真正需要时才会求值。与 eager evaluation(迫切求值) 相反,eager evaluation 当一个变量被赋值时、函数被调用时…甚至变量没有被使用的情况下,都立马对表达式求值,惰性不是函数式编程的必需品,但这是个符合函数式范式的好策略(Haskell 大概是最佳的例子,弥漫着懒惰的语言)。 -Ruby 所採用的基本上是 eager evaluation(虽然许多其它的语言,在条件还没满足前不对表达式求值,以及短路布林运算 `&&`, `||` 等)。然而,与任何内置高阶函数的语言一样,延迟求值是隐性支援,因为程序员自己决定区块何时被调用。 +Ruby 所猜用的基本上是迫切求值(虽然同许多其它的语言一样,在条件语句中不满足判定条件时,在短路布尔运算 `&&`, `||`等情况下,不对表达式求值)。然而,与任何内置高阶函数的语言一样,延迟求值是隐性支援,因为程序员可自己决定区块何时被调用。 Enumerators 同样 从 Ruby 1.9 开始支援(1.8 请用 backports),它们提供了一个简单的介面来定义惰性 enumerables。经典的例子是构造一个枚举器,返回所有的自然数: @@ -453,7 +472,7 @@ natural_numbers = Enumerator.new do |yielder| end ``` -可以用更函数式的精神改写: +可以写得更有函数味: ```Ruby natural_numbers = Enumerator.new do |yielder| @@ -468,7 +487,7 @@ natural_numbers.take(10) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ``` -现在,试试给 `natural_numbers` 做 `map`,发生什么事?它不会停止。标准的 enumerable 方法 (`map`, `select` 等)返回一个数组,所以在输入流是无穷大时,无法正常工作。让我们扩展 Enumerator 类别,比如加入这个惰性的 Enumerator#map: +现在,试试给 `natural_numbers` 做 `map`,发生什么事?它不会停止。标准的 enumerable 方法 (`map`, `select` 等)返回一个数组,所以在输入流是无穷大时,无法正常工作。让我们扩展 Enumerator 类别,比如加入这个惰性的 `Enumerator#map`: ```Ruby @@ -486,11 +505,11 @@ end 现在我们可以给所有自然数的流做 `map` 了: ```Ruby -natural_numbers.map { |x| 2*x }.take(10) +natural_numbers.map { |x| 2 * x }.take(10) # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] ``` -枚举器是用来构造惰性行为的区块的好东西,但你可以使用用懒惰风格,实作了所有 enumerable 方法的函式库: +枚举器是构造惰性行为的砖瓦,你也可使用实现了所有 Enumerable 方法惰性化的函式库: https://github.com/yhara/enumerable-lazy @@ -505,13 +524,13 @@ require 'enumerable/lazy' 1. 显而易见的好处: 无需在不必要的情况下,构造、储存完整的结构(也许,可以更有效率的使用 CPU 及内存) -2. 不太显而易见的好处: 惰性求值使写程序不需要了解超出你所需的范围。让我们看一个例子:你写了某种解题工具,可以提供无数种解法,但在某个时候,你只想要前十种解法。你可能会这么写: +2. 不太显而易见的好处: 惰性求值使写的程序无需了解超出它需要了解的范围。让我们看一个例子:你写了某种解题工具,可以提供无数种解法,但在某个时候,你只想要前十种解法。你可能会这么写: ```Ruby solver(input, :max => 10) ``` -当你与惰性结构一起工作时,不需要说什么时候该结束。调用者自己会决定他需要多少值。代码变得更简单,责任归属到对的地方,也就是调用者: +当你与惰性结构一起工作时,不需要说什么时候该结束。调用者自己会决定他需要多少值。代码更简单,编码者也不许负责默认输出多少个结果: ```Ruby solver(input).take(10) @@ -539,19 +558,19 @@ end sum #=> 275 ``` -我希望这个例子展示了这个文档裡讨论的函数式编程的优点: +我希望这个例子展示了本文档谈到的函数式编程优点: -1. 更简洁: 你会撰写更少的代码。函数式程序处理的是表达式,而表达式可以连锁起来;命令式程序处理的是变量的改动(叙述式),而这不能连锁。 +1. 简洁: 你会撰写更少的代码。函数式程序处理的是表达式,而表达式可以连锁起来;命令式程序处理的是变量的改动(叙述式),而这不能连锁。 -2. 更抽象: 你可以争论我们使用 `select`, `inject`…等等,来隐藏了一大堆代码,我很高兴你这么说,因为我们正是这么干的。将通用的、可重用的代码隐藏起来,这是所有编程的重点 –– 但函数式编程特别是关于如何撰写抽象。感到开心不是因为写了更少的代码,而是因为藉由认出可重用的模式,简化了代码的复杂性。 +2. 抽象: 你可以争论说我们使用 `select`, `inject`…等等,来隐藏了一大堆代码,我很高兴你这么说,事情就是这样。将通用的、可重用的代码隐藏起来,撰写抽象代码是任何编程的重点,函数式编程尤为这样。让人高兴的不是因为代码少了,而是因为依靠辨认出可重用的模式,简化了代码的复杂性。 -3. 更有声明式的味道: 看看命令式的版本,第一眼看起来是一沱无用的代码 –– 没有注解的话 –– 它会做什么你完全没有概念。你可能会说:“好吧,从这裡开始读,草草记下 `n` 与 `sum` 的值,进入某个迴圈,看看 `n` 与 `sum` 的值如何变化,看看最后一次迭代的情形” 等等。函数式版本另一方面是自我解释的,函数式版本描述、声明它在干的事,而不是如何干这件事。 +3. 更有声明式的味道: 看看命令式的版本,第一眼看起来是一沱无用的代码 ── 没有注解的话 ── 它会做什么你完全没有概念。你可能会说:“好吧,从这里开始读,草草记下 `n` 与 `sum` 的值,进入某个循环,看看 `n` 与 `sum` 的值如何变化,看看最后一次迭代的情形” 等等。函数式版本则不言自明,函数式版本描述、声明它在干的事,而不是如何干这件事。 -“函数式编程就像是将你的问题叙述给数学家一样。命令式编程像是给白痴下指令” (arcus 在 Freenode #scheme 频道所说) +“函数式编程像是将你的问题叙述给数学家。命令式编程像是给白痴下指令” (arcus 在 Freenode #scheme 频道所说) ### 结论 -更好的理解函数式编程的原理,帮助我们写出更清晰、重用性更高并更简洁的代码。Ruby 基本上是一个命令式语言,但它也有很大的函数式能力,明白什么时候用,及如何用(以及何时不该用)这些能力。将这句话当成你的座右铭吧 “状态是万恶的根源,尽可能避免它。” +更好地理解函数式编程原理,能帮助我们写出更清晰、重用性更高和更简洁的代码。Ruby 基本上是一个命令式语言,但它的函数式能力也很强,学会何时用和如何用(以及何时不该用)这种能力吧。“状态是万恶的根源,尽可能避免它”, 将这句话当成你的座右铭吧。 ### 简报 diff --git a/README.md b/README.md index e287701..62760f2 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,20 @@ ## 目錄 -* [簡介](#-1) -* [理論部分](#-2) -* [Ruby的函數式程式設計](#ruby) - * [不要更新變數](#-3) - * [用 Blocks 作為高階函數](#-blocks-) - * [物件導向與函數式程式設計](#-6) - * [萬物皆表達式](#-7) - * [遞迴](#-8) - * [惰性枚舉器](#-9) - * [一個實際的範例](#-11) -* [結論](#-12) -* [簡報](#-13) -* [延伸閱讀](#-14) +* [簡介](#簡介) +* [理論部分](#理論部分) +* [Ruby的函數式程式設計](#ruby-的函數式程式設計) + * [不要更新變數](#不要更新變數) + * [不要重用變量](#不要重用變量) + * [用 Blocks 作為高階函數](#用-blocks-作為高階函數) + * [物件導向與函數式程式設計](#物件導向與函數式程式設計) + * [萬物皆表達式](#萬物皆表達式) + * [遞迴](#遞迴) + * [惰性枚舉器](#惰性枚舉器) + * [一個實際的範例](#一個實際的範例) +* [結論](#結論) +* [簡報](#簡報) +* [延伸閱讀](#延伸閱讀) ## 簡介 @@ -51,7 +52,7 @@ * 容易除錯:函數彼此互相隔離,只依賴輸入與輸出,所以很容易除錯。 -## Ruby的函數式程式設計 +## Ruby 的函數式程式設計 一切都是這麼美好,但怎樣才能將函數式程式設計,應用到每天寫 Ruby(Ruby 不是個函數式語言)的程式開發裡呢?函數式程式設計廣義來說,是一種風格,可以用在任何語言。當然啦,用在特別為這種範式打造的語言裡顯得更自然,但某種程度上來說,可以應用到任何語言。 @@ -95,7 +96,7 @@ hash = {:a => 1, :b => 2} new_hash = hash.merge(:c => 3) ``` -#### 牽扯到記憶體位置的地方,不要使用破壞性方法。 +#### 不要使用破壞性方法 No: @@ -129,11 +130,29 @@ Yes: output = [1, (2 if i_have_to_add_two), 3].compact ``` +### 不要重用變量 + +以下是需要避免的常見模式: + +```ruby +number = gets +number = number.to_i +``` + +這裡沒有更新 `number` 變數,而是覆寫了舊的變數,(從函數式的角度來看)更新變數便是覆寫。這兩者的原理相同:一旦你說 `number = gets`,則 `number` 在所有的作用域裡,值應該都是一樣的。如果你想要變換 `number` 的值,換個名字就好: + +```ruby +number_string = gets +number = number_string.to_i +``` + +記得,數學裡 `var = value` 應該是編寫代碼的人與將來讀代碼的人的神聖約定:一旦在作用域裡找到 `var`,便可用 `value` 替換掉變量。 + ### 用 Blocks 作為高階函數 如果一個語言要搞函數式,會需要高階函數。高階函數是什麼?函數可以接受別的函數作為參數,並可以回傳函數,就這麼簡單。 -Ruby (與 Smalltalk 還有其它語言)在這個方面上非常特別,語言本身就內建這個功能: **blocks** 區塊。區塊是一段匿名的程式碼,你可以隨意的傳來傳去或是執行它。讓我們看區塊的典型用途,來建構函數式程式設計的建構子。 +Ruby(與 Smalltalk 還有其它語言)在這個方面上非常特別,語言本身就內建這個功能: **blocks** 區塊。區塊是一段匿名的程式碼,你可以隨意的傳來傳去或是執行它。讓我們看區塊的典型用途,來建構函數式程式設計的建構子。 #### init-empty + each + push = map @@ -205,7 +224,7 @@ length = ["milu", "rantanplan"].map(&:length).inject(0, :+) # 14 #### empty + each + accumulate + push -> scan -想像一下,你不僅想要摺疊(fold)的結果,也想要過程中產生的部分數值。用命令式程式設計風格,你可能會這麼寫: +想像一下,你不僅想要摺疊(fold)的結果,也想要過程中產生的部分數值。用命令式程式設計風格,可能會寫成這樣: ```Ruby lengths = [] @@ -219,7 +238,7 @@ lengths # [0, 4] 在函數式的世界裡,Haskell 稱之為 [scan](http://zvon.org/other/haskell/Outputprelude/scanl_f.html), C++ 稱之為 [partial_sum](http://www.cplusplus.com/reference/std/numeric/partial_sum/), Clojure 稱之為 [reductions](http://clojuredocs.org/clojure_core/clojure.core/reductions)。 -令人訝異的是,Ruby 居然沒有這樣的函數!讓我們自己寫一個。這個怎麼樣: +令人驚訝的是,Ruby 居然沒有這樣的函數!讓我們自己寫一個。這個怎麼樣: ```Ruby lengths = ["milu", "rantanplan"].partial_inject(0) do |dog_name| @@ -227,7 +246,7 @@ lengths = ["milu", "rantanplan"].partial_inject(0) do |dog_name| end # [0, 4, 14] ``` -Enumerable#partial_inject 可以這麼實現: +`Enumerable#partial_inject` 可以這麼實作: ```Ruby module Enumerable @@ -240,7 +259,7 @@ module Enumerable end ``` -實作的細節不重要,重要的是,當認出一個有趣的模式可以被抽象化時,我們將其寫在另一個函式庫,撰寫文件,反覆測試。現在只要讓實際的需求去完善你的擴充即可。 +實作的細節不重要,重要的是,當認出一個有趣的模式可以被抽象化時,將其寫成一個函式庫,撰寫文件,反覆測試。現在只要讓實際的需求去完善你的函式庫即可。 #### initial assign + conditional assign + conditional assign + ... @@ -292,7 +311,7 @@ country.invade if country.has_oil? #### 如何從 enumerable 創造一個 hash -Vanilla Ruby 沒有從 Enumerable 轉到 Hash 的直接對應(本人認為是一個遺憾的缺陷)。這也是為什麼新手持續寫出下面這個糟糕的模式(而你怎麼能責怪他們呢?唉!): +Vanilla Ruby 沒有從 Enumerable 轉到 Hash 的直接對應(本人認為是一個遺憾的缺陷)。這也是為什麼新手持續寫出下面這個糟糕的模式(而你又怎麼能責怪他們呢?唉!): ```Ruby hash = {} @@ -302,7 +321,7 @@ end hash ``` -這真的非常可怕!阿~~~!但手邊有沒有更好的辦法呢?過去 Hash 建構子需要一個有著連續鍵值對的 flatten 集合 (阿,用 flatten 陣列來描述映射?Lisp 曾這麼做,但還是很醜陋)。幸運的是,Ruby 的最新版本也接受鍵值對,這樣更有意義(作為 `hash.to_a` 的逆操作),現在你可以這麼寫: +這真的非常可怕!阿~~~!但手邊有沒有更好的辦法呢?過去 Hash 建構子需要一個有著連續鍵值對的 flatten 集合 (呃,用 flatten 陣列來描述映射?Lisp 曾這麼做,但還是很醜陋)。幸運的是,最新版本的 Ruby 也接受鍵值對,這樣更有意義(作為 `hash.to_a` 的逆操作),現在你可以這麼寫: ```Ruby Hash[input.map do |item| @@ -318,7 +337,7 @@ input.inject({}) do |hash, item| end ``` -我們都同意這還是很囉嗦,所以我們最好將它放在 Enumerable 模組,[Facets](http://rubyworks.github.com/facets/) 正是這麼幹的。它稱之為 Enumerable#mash: +我們都同意這還是很囉嗦,所以我們最好將它放在 Enumerable 模組,[Facets](http://rubyworks.github.com/facets/) 正是這麼幹的。它稱之為 `Enumerable#mash`: ```Ruby module Enumerable @@ -336,7 +355,7 @@ end # {"functional"=>10, "programming"=>11, "rules"=>5} ``` -或使用 `mash` 及 選擇性區塊來一步完成: +或使用 `mash` 及選擇性區塊來一步完成: ```Ruby ["functional", "programming", "rules"].mash { |s| [s, s.length] } @@ -345,7 +364,7 @@ end ### 物件導向與函數式程式設計 -[Joe Armstrong][JA] (Erlang 發明人) 在 “Coders At work” 談論過物件導向程式設計的重用性: +[Joe Armstrong][JA](Erlang 發明人)在 “Coders At work” 談論過物件導向程式設計的重用性: “我認為缺少重用性是物件導向語言造成的,而不是函數式語言。物件導向語言的問題是,它們帶著語言執行環境的所有隱含資訊四處亂竄。你想要的是香蕉,但看到的卻是香蕉拿在大猩猩手裡,而大猩猩的後面是整個叢林” @@ -353,7 +372,7 @@ end * 典型的 OOP 傾向強調改變物件的狀態。 * 典型的 OOP 傾向層與層之間緊密的耦合。 -* 典型的 OOP 將同一性(identity)與狀態的概念搞混了。 +* 典型的 OOP 將同一性(identity)與狀態的概念搞混了。 * 資料與程式碼的混合物,導致了概念與實際的問題產生。 [Rich Hickey][RH],Clojure 的發明人(一個給 JVM 用的函數式 Lisp 方言),在這場[出色的演講](http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey)裡談論了狀態、數值以及同一性。 @@ -397,9 +416,9 @@ end ### 遞迴 -純函數式語言沒有隱含的狀態,大量利用了遞迴。為了避免 stack overflow,函數式使用一種稱為尾遞迴優化(TCO)的機制。Ruby 1.9 有實作這種機制,但預設沒有打開。要是你希望你的程式,在哪都可以動的話,就不要使用它。 +純函數式語言大量利用了遞迴,狀態都很明確。為了避免 stack overflow,函數式使用一種稱為尾遞迴優化(TCO)的機制。Ruby 1.9 有實作這種機制,預設沒有開啟。要是你希望你的程式,在那都可以工作的話,就不要使用尾遞迴優化。 -但是某些情況下,遞迴仍然是很有用的,即便是每次遞迴時都創建新的堆疊。注意!某些遞迴的用途可以用 foldings 來實現(像 Enumerable#inject)。 +但是某些情況下,遞迴仍然是很有用的,即便是每次遞迴時都創建新的堆疊。注意!某些遞迴的用途可以用 foldings 來實作(像 `Enumerable#inject`)。 在 MRI-1.9 啟用 TCO: @@ -420,7 +439,7 @@ module Math end ``` -在遞迴深度不太可能很深的情況下,你仍可以使用: +在遞迴深度不會很深的情況下,仍可以使用: ```Ruby class Node @@ -468,7 +487,7 @@ natural_numbers.take(10) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ``` -現在,試試給 `natural_numbers` 做 `map`,發生什麼事?它不會停止。標準的 enumerable 方法 (`map`, `select` 等)回傳一個陣列,所以在輸入流是無窮大時,無法正常工作。讓我們擴展 Enumerator 類別,比如加入這個惰性的 Enumerator#map: +現在,試試給 `natural_numbers` 做 `map`,發生什麼事?它不會停止。標準的 enumerable 方法 (`map`, `select` 等)回傳一個陣列,所以在輸入流是無窮大時,無法正常工作。讓我們擴展 Enumerator 類別,比如加入這個惰性的 `Enumerator#map`: ```Ruby @@ -490,7 +509,7 @@ natural_numbers.map { |x| 2*x }.take(10) # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] ``` -枚舉器是用來建構惰性行為的區塊的好東西,但你可以使用用懶惰風格,實作了所有 enumerable 方法的函式庫: +枚舉器是用來建構惰性行為的區塊的好東西,但你可以使用用懶惰風格,實作了所有 `Enumerable` 方法的函式庫: https://github.com/yhara/enumerable-lazy @@ -543,15 +562,15 @@ sum #=> 275 1. 更簡潔: 你會撰寫更少的程式碼。函數式程式處理的是表達式,而表達式可以連鎖起來;命令式程式處理的是變量的改動(敘述式),而這不能連鎖。 -2. 更抽象: 你可以爭論我們使用 `select`, `inject`…等等,來隱藏了一大堆程式碼,我很高興你這麼說,因為我們正是這麼幹的。將通用的、可重用的程式碼隱藏起來,這是所有程式設計的重點 –– 但函數式程式設計特別是關於如何撰寫抽象。感到開心不是因為寫了更少的代碼,而是因為藉由認出可重用的模式,簡化了程式碼的複雜性。 +2. 更抽象: 你可以爭論我們使用 `select`, `inject`…等等,來隱藏了一大堆程式碼,我很高興你這麼說,因為我們正是這麼幹的。將通用的、可重用的程式碼隱藏起來,這是所有程式設計的重點 ── 但函數式程式設計特別是關於如何撰寫抽象。感到開心不是因為寫了更少的代碼,而是因為藉由認出可重用的模式,簡化了程式碼的複雜性。 -3. 更有聲明式的味道: 看看命令式的版本,第一眼看起來是一沱無用的程式碼 –– 沒有註解的話 –– 它會做什麼你完全沒有概念。你可能會說:“好吧,從這裡開始讀,草草記下 `n` 與 `sum` 的值,進入某個迴圈,看看 `n` 與 `sum` 的值如何變化,看看最後一次迭代的情形” 等等。函數式版本另一方面是自我解釋的,函數式版本描述、聲明它在幹的事,而不是如何幹這件事。 +3. 更有聲明式的味道: 看看命令式的版本,第一眼看起來是一沱無用的程式碼 ── 沒有註解的話 ── 它會做什麼你完全沒有概念。你可能會說:“好吧,從這裡開始讀,草草記下 `n` 與 `sum` 的值,進入某個迴圈,看看 `n` 與 `sum` 的值如何變化,看看最後一次迭代的情形” 等等。函數式版本另一方面是自我解釋的,函數式版本描述、聲明它在幹的事,而不是如何幹這件事。 “函數式程式設計就像是將你的問題敘述給數學家一樣。命令式程式設計像是給白痴下指令” (arcus 在 Freenode #scheme 頻道所說) ### 結論 -更好的理解函數式程式設計的原理,幫助我們寫出更清晰、重用性更高並更簡潔的程式碼。Ruby 基本上是一個命令式語言,但它也有很大的函數式能力,明白什麼時候用,及如何用(以及何時不該用)這些能力。將這句話當成你的座右銘吧 “狀態是萬惡的根源,盡可能避免它。” +更好的理解函數式程式設計的原理,幫助我們寫出更清晰、重用性更高並更簡潔的程式碼。Ruby 基本上是一個命令式語言,但它也有很大的函數式能力,明白什麼時候用,及如何用(以及何時不該用)這些能力。將這句話當成你的座右銘吧“狀態是萬惡的根源,盡可能避免它。”。 ### 簡報 @@ -575,7 +594,7 @@ http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey ## 授權 -This document is licensed under the CC-By 3.0 License, which encourages you to share these documents. See http://creativecommons.org/licenses/by/3.0/ for more details. +本文授權為 CC-By 3.0,歡迎分享此文件。進一步了解請參考 http://creativecommons.org/licenses/by/3.0/。 CC-By 3.0 License http://creativecommons.org/licenses/by/3.0/