许多函数式文章讲述的是组合,流水线和高阶函数这样的抽象函数式技术。本文不同,它展示了人们每天编写的命令式,非函数式代码示例,以及将这些示例转换为函数式风格。
文章的第一部分将一些短小的数据转换循环重写成函数式的maps和reduces。第二部分选取长一点的循环,把他们分解成单元,然后把每个单元改成函数式的。第三部分选取一个很长的连续数据转换循环,然后把它分解成函数式流水线。
示例都是用Python写的,因为很多人觉得Python易读。为了证明函数式技术对许多语言来说都相同,许多示例避免使用Python特有的语法:map,reduce,pipeline。
导引
当人们谈论函数式编程,他们会提到非常多的“函数式”特性。提到不可变数据1,第一类对象2以及尾调用优化3。这些是帮助函数式编程的语言特征。提到mapping(映射),reducing(归纳),piplining(管道),recursing(递归),currying4(科里化);以及高阶函数的使用。这些是用来写函数式代码的编程技术。提到并行5,惰性计算6以及确定性。这些是有利于函数式编程的属性。
忽略全部这些。可以用一句话来描述函数式代码的特征:避免副作用。它不会依赖也不会改变当前函数以外的数据。所有其他的“函数式”的东西都源于此。当你学习时把它当做指引。
这是一个非函数式方法:
a = 0 def increment1(): global a a += 1</div>
这是一个函数式的方法:
def increment2(a): return a + 1</div>
不要在lists上迭代。使用map和reduce。
Map(映射)
Map接受一个方法和一个集合作为参数。它创建一个新的空集合,以每一个集合中的元素作为参数调用这个传入的方法,然后把返回值插入到新创建的集合中。最后返回那个新集合。
这是一个简单的map,接受一个存放名字的list,并且返回一个存放名字长度的list:
name_lengths = map(len, ["Mary", "Isla", "Sam"]) print name_lengths # => [4, 4, 3]</div>
接下来这个map将传入的collection中每个元素都做平方操作:
squares = map(lambda x: x * x, [0, 1, 2, 3, 4]) print squares # => [0, 1, 4, 9, 16]</div>
这个map并没有使用一个命名的方法。它是使用了一个匿名并且内联的用lambda定义的方法。lambda的参数定义在冒号左边。方法主体定义在冒号右边。返回值是方法体运行的结果。
下面的非函数式代码接受一个真名列表,然后用随机指定的代号来替换真名。
import random names = ['Mary', 'Isla', 'Sam'] code_names = ['Mr. Pink', 'Mr. Orange', 'Mr. Blonde'] for i in range(len(names)): names[i] = random.choice(code_names) print names # => ['Mr. Blonde', 'Mr. Blonde', 'Mr. Blonde']</div>
(正如你所见的,这个算法可能会给多个密探同一个秘密代号。希望不会在任务中混淆。)
这个可以用map重写:
import random
names = ['Mary', 'Isla', 'Sam']
secret_names = map(lambda x: random.choice(['Mr. Pink',
'Mr. Orange',
'Mr. Blonde']),
names)
</div>
练习1.尝试用map重写下面的代码。它接受由真名组成的list作为参数,然后用一个更加稳定的策略产生一个代号来替换这些名字。
names = ['Mary', 'Isla', 'Sam'] for i in range(len(names)): names[i] = hash(names[i]) print names # => [6306819796133686941, 8135353348168144921, -1228887169324443034]</div>
(希望密探记忆力够好,不要在执行任务时把代号忘记了。)
我的解决方案:
names = ['Mary', 'Isla', 'Sam'] secret_names = map(hash, names)</div>
Reduce(迭代)
Reduce 接受一个方法和一个集合做参数。返回通过这个方法迭代容器中所有元素产生的结果。
这是个简单的reduce。返回集合中所有元素的和。
sum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4]) print sum # => 10</div>
x是迭代的当前元素。a是累加和也就是在之前的元素上执行lambda返回的值。reduce()遍历元素。每次迭代,在当前的a和x上执行lambda然后返回结果作为下一次迭代的a。
第一次迭代的a是什么?在这之前没有迭代结果传进来。reduce() 使用集合中的第一个元素作为第一次迭代的a,然后从第二个元素开始迭代。也就是说,第一个x是第二个元素。
这段代码记'Sam'这个词在字符串列表中出现的频率:
sentences = ['Mary read a story to Sam and Isla.',
'Isla cuddled Sam.',
'Sam chortled.']
sam_count = 0
for sentence in sentences:
sam_count += sentence.count('Sam')
print sam_count
# => 3
</div>
下面这个是用reduce写的:
sentences = ['Mary read a story to Sam and Isla.',
'Isla cuddled Sam.',
'Sam chortled.']
sam_count = reduce(lambda a, x: a + x.count('Sam'),
sentences,
0)
</div>
这段代码如何初始化a?出现‘Sam'的起始点不能是'Mary read a story to Sam and Isla.' 初始的累加和由第三个参数来指定。这样就允许了集合中元素的类型可以与累加器不同。
为什么map和reduce更好?
首先,它们大多是一行代码。
二、迭代中最重要的部分:集合,操作和返回值,在所有的map和reduce中总是在相同的位置。
三、循环中的代码可能会改变之前定义的变量或之后要用到的变量。照例,map和reduce是函数式的。
四、map和reduce是元素操作。每次有人读到for循环,他们都要逐行读懂逻辑。几乎没有什么规律性的结构可以帮助理解代码。相反,map和reduce都是创建代码块来组织复杂的算法,并且读者也能非常快的理解元素并在脑海中抽象出来。“嗯,代码在转换集合中的每一个元素。然后结合处理的数据成一个输出。”
五、map和reduce有许多提供便利的“好朋友”,它们是基本行为的修订版。例如filter,all,any以及find。
练习2。尝试用map,reduce和filter重写下面的代码。Filter接受一个方法和一个集合。返回集合中使方法返回true的元素。
people = [{'name': 'Mary', 'height': 160},
{'name': 'Isla', 'height': 80},
{'name': 'Sam'}]
height_total = 0
height_count = 0
for person in people:
if 'height' in person:
height_total += person['height']
height_count += 1
if height_count > 0:
average_height = height_total / height_count
print average_height
# => 120
</div>
如果这个比较棘手,试着不要考虑数据上的操作。考虑下数据要经过的状态,从people字典列表到平均高度。不要尝试把多个转换捆绑在一起。把每一个放在独立的一行,并且把结果保存在命名良好的变量中。代码可以运行后,立刻凝练。
我的方案:
people = [{'name': 'Mary', 'height': 160},
{'name': 'Isla', 'height': 80},
{'name': 'Sam'}]
heights = map(lambda x: x['height'],
filter(lambda x: 'height' in x, people))
if len(heights) > 0:
from operator import add
average_height = reduce(add, heights) / len(heights)
</div>
写声明式代码,而不是命令式
下面的程序演示三辆车比赛。每次移动时间,每辆车可能移动或者不动。每次移动时间程序会打印到目前为止所有车的路径。五次后,比赛结束。
下面是某一次的输出:
- -- -- -- -- --- --- -- --- ---- --- ---- ---- ---- -----</div>
这是程序:
from random import random
time = 5
car_positions = [1, 1, 1]
while time:
# decrease time
time -= 1
print ''
for i in range(len(car_positions)):
# move car
if random() > 0.3:
car_positions[i] += 1
# draw car
print '-' * car_positions[i]
</div>
代码是命令式的。一个函数式的版本应该是声明式的。应该描述要做什么,而不是怎么做。
使用方法
通过绑定

