许多函数式编程文章都讲授抽象的函数技术。即,合成,流水线,高阶函数。这是不同的。它显示了人们每天编写的命令性,非功能性代码示例,并将这些示例转换为功能性样式。

本文的第一部分采用了简短的数据转换循环,并将其转换为功能映射并进行了简化。第二部分采用更长的循环,将其分解为多个单元并使每个单元起作用。第三部分介绍了一个循环,该循环是一系列连续的数据转换,并将其分解为功能流水线。

这些示例使用Python,因为许多人发现Python易于阅读。为了说明许多语言共有的功能技术,许多示例避免了Python性:映射,归约,流水线。所有示例都在Python 2中。

导向绳

当人们谈论函数式编程时,他们提到了令人眼花number乱的“函数式”特性。他们提到了不可变数据1,一流的功能2和尾部调用优化3。这些是有助于功能编程的语言功能。他们提到了映射,归约,流水线,递归,循环4以及使用高阶函数。这些是用于编写功能代码的编程技术。他们提到并行化5,惰性评估6和确定性7。这些是功能程序的有利属性。

忽略所有这些。功能代码的特点是一件事:没有副作用。它不依赖当前函数外部的数据,也不更改当前函数外部的数据。其他所有“功能性”事物都可以从此属性派生。在学习时将其用作引导绳。

这是一个不起作用的功能:

a = 0
def increment():
    global a
    a += 1 
1
2
3
4

这是一个功能函数:

def increment(a):
    return a + 1 
1
2

不要遍历列表。使用地图并减少。

地图

地图具有功能和项目集合。它创建一个新的空集合,在原始集合中的每个项目上运行该函数,并将每个返回值插入到新集合中。它返回新的集合。

这是一个简单的映射,它包含一个名称列表并返回这些名称的长度列表:

name_lengths = map(len, ["Mary", "Isla", "Sam"])

print name_lengths
# => [4, 4, 3] 
1
2
3
4

这是一张将传递的集合中的每个数字平方的地图:

squares = map(lambda x: x * x, [0, 1, 2, 3, 4])

print squares
# => [0, 1, 4, 9, 16] 
1
2
3
4

该映射没有命名函数。它需要使用定义的匿名内联函数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'] 
1
2
3
4
5
6
7
8
9
10

(如您所见,该算法可以为多个秘密特工分配相同的秘密代码名称。希望这不会在秘密任务期间造成混乱。)

可以将其重写为地图:

import random

names = ['Mary', 'Isla', 'Sam']

secret_names = map(lambda x: random.choice(['Mr. Pink',
                                            'Mr. Orange',
                                            'Mr. Blonde']),
                   names) 
1
2
3
4
5
6
7
8

练习1。尝试将以下代码重写为地图。它获取一个实名列表,并将其替换为使用更可靠策略生成的代码名。

names = ['Mary', 'Isla', 'Sam']

for i in range(len(names)):
    names[i] = hash(names[i])

print names
# => [6306819796133686941, 8135353348168144921, -1228887169324443034] 
1
2
3
4
5
6
7

(希望秘密特工会留下美好的回忆,并且在秘密任务期间不会忘记彼此的秘密代号。)

我的解决方案:

names = ['Mary', 'Isla', 'Sam']

secret_names = map(hash, names) 
1
2
3

减少

Reduce具有功能和项目集合。它返回通过组合项目创建的值。

这是一个简单的减少。它返回集合中所有项目的总和。

sum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4])

print sum
# => 10 
1
2
3
4

x是正在迭代的当前项目。a是累加器。它是对上一项执行lambda返回的值。reduce()遍历所有项目。对于每一个,它运行在当前的拉姆达ax,并返回结果作为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 
1
2
3
4
5
6
7
8
9
10

这是与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) 
1
2
3
4
5
6
7

该代码如何以其首字母开头a?的发生次数的起点'Sam'不能为'Mary read a story to Sam and Isla.'。初始累加器由的第三个参数指定reduce()。这允许使用与集合中的项目不同类型的值。

为什么贴图和缩小效果更好?

首先,它们通常是单线的。

其次,迭代的重要部分-集合,操作和返回值-始终在每个映射中都位于相同的位置,并减少。

第三,循环中的代码可能会影响在其之前定义的变量或在其之后运行的代码。按照惯例,映射和归约是功能性的。

第四,map和reduce是基本操作。每当一个人阅读一个for循环时,他们就必须逐行地遍历逻辑。他们可以使用很少的结构规则来创建一个脚手架,以使其对代码的理解得以保留。相反,map和reduce既是构建块,可以组合成复杂的算法,而且代码阅读器可以立即在他们的脑海中理解和抽象化元素。“啊,这段代码正在改变这个集合中的每个项目。它抛弃了一些转变。它将其余部分合并为一个输出。”

第五,制图和化简有很多朋友,他们提供了基本行为的有用的,经过调整的版本。例如:filterallanyfind

练习2。尝试使用map,reduce和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 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如果这看起来很棘手,请尝试不要考虑对数据进行的操作。考虑一下数据将经过的状态,从人员字典列表到平均身高。不要尝试将多个转换捆绑在一起。将每个变量放在单独的行上,然后将结果分配给一个描述性名称的变量。代码工作后,将其压缩。

我的解决方案:

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) 
1
2
3
4
5
6
7
8
9
10

声明式而非强制性地写

以下程序在三辆汽车之间进行比赛。在每个时间步长处,每辆汽车可能会向前行驶或失速。在每个时间步,程序都会打印出到目前为止的汽车路径。经过五个时间步骤,比赛结束了。

这是一些示例输出:

-
--
--

--
--
---

---
--
---

----
---
----

----
----
----- 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这是程序:

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] 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

该代码是强制性编写的。功能版本将是声明性的。它会描述做什么,而不是如何做。

使用功能

通过将代码片段捆绑到函数中,可以使程序更具声明性。

from random import random

def move_cars():
    for i, _ in enumerate(car_positions):
        if random() > 0.3:
            car_positions[i] += 1

def draw_car(car_position):
    print '-' * car_position

def run_step_of_race():
    global time
    time -= 1
    move_cars()

def draw():
    print ''
    for car_position in car_positions:
        draw_car(car_position)

time = 5
car_positions = [1, 1, 1]

while time:
    run_step_of_race()
    draw() 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

为了理解该程序,读者只需阅读主循环即可。“如果还有时间,请参加比赛并抽签。再次检查时间。” 如果读者想更多地了解进行比赛或绘画的含义,则可以阅读这些函数中的代码。

没有更多评论了。该代码描述了自己。

将代码拆分为功能是一种很棒的,低脑力的方法,可以使代码更具可读性。

该技术使用函数,但是将它们用作子例程。他们打包代码。该代码在引导绳方面不起作用。代码中的函数使用未作为参数传递的状态。它们通过更改外部变量而不是通过返回值来影响其周围的代码。要检查某个功能的真正作用,读者必须仔细阅读每一行。如果找到外部变量,则必须找到其来源。他们必须查看其他哪些函数可以更改该变量。

移除状态

这是赛车代码的功能版本:

from random import random

def move_cars(car_positions):
    return map(lambda x: x + 1 if random() > 0.3 else x,
               car_positions)

def output_car(car_position):
    return '-' * car_position

def run_step_of_race(state):
    return {'time': state['time'] - 1,
            'car_positions': move_cars(state['car_positions'])}

def draw(state):
    print ''
    print '\n'.join(map(output_car, state['car_positions']))

def race(state):
    draw(state)
    if state['time']:
        race(run_step_of_race(state))

race({'time': 5,
      'car_positions': [1, 1, 1]}) 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

代码仍然被拆分为功能,但是功能是功能性的。这有三个迹象。首先,不再有任何共享变量。timecar_positions直接通过race()。其次,函数采用参数。第三,在函数内部没有实例化任何变量。所有数据更改都使用返回值完成。race()递归3的结果run_step_of_race()。每次步骤生成新状态时,都会立即将其传递到下一步。

现在,这是两个函数,zero()one()

def zero(s):
    if s[0] == "0":
        return s[1:]

def one(s):
    if s[0] == "1":
        return s[1:] 
1
2
3
4
5
6
7

zero()需要一个字符串s。如果第一个字符是'0',则返回字符串的其余部分。如果不是,它将返回NonePython函数的默认返回值。one()的功能相同,但第一个字符为'1'

想象一个名为的函数rule_sequence()。它采用字符串和形式为zero()和的规则函数列表one()。它在字符串上调用第一个规则。除非None返回,否则它将使用返回值并在其上调用第二条规则。除非None返回,否则它将获取返回值并在其上调用第三条规则。依此类推。如果有任何规则返回None,则rule_sequence()停止并返回None。否则,它将返回最终规则的返回值。

这是一些示例输入和输出:

print rule_sequence('0101', [zero, one, zero])
# => 1

print rule_sequence('0101', [zero, zero])
# => None 
1
2
3
4
5

这是的命令性版本rule_sequence()

def rule_sequence(s, rules):
    for rule in rules:
        s = rule(s)
        if s == None:
            break

    return s 
1
2
3
4
5
6
7

练习3。上面的代码使用循环来完成其工作。通过将其重写为递归使其更具声明性。

我的解决方案:

def rule_sequence(s, rules):
    if s == None or not rules:
        return s
    else:
        return rule_sequence(rules[0](s), rules[1:]) 
1
2
3
4
5

使用管道

在上一节中,一些命令性循环被重写为调用辅助函数的递归。在本节中,将使用称为管道的技术来重写另一种命令式循环。

下面的循环对包含名称,不正确的原产国和某些乐队的活跃状态的词典进行转换。

bands = [{'name': 'sunset rubdown', 'country': 'UK', 'active': False},
         {'name': 'women', 'country': 'Germany', 'active': False},
         {'name': 'a silver mt. zion', 'country': 'Spain', 'active': True}]

def format_bands(bands):
    for band in bands:
        band['country'] = 'Canada'
        band['name'] = band['name'].replace('.', '')
        band['name'] = band['name'].title()

format_bands(bands)

print bands
# => [{'name': 'Sunset Rubdown', 'active': False, 'country': 'Canada'},
#     {'name': 'Women', 'active': False, 'country': 'Canada' },
#     {'name': 'A Silver Mt Zion', 'active': True, 'country': 'Canada'}] 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

函数名称激起了人们的担忧。“格式”非常模糊。在仔细检查代码后,这些担忧开始消除。同一循环中会发生三件事。该'country'键被设置成'Canada'。标点从乐队名称中删除。乐队名称大写。很难说出代码打算做什么,也很难说出它是否按照它的意图去做。该代码难以重用,难以测试且难以并行化。

与此比较:

print pipeline_each(bands, [set_canada_as_country,
                            strip_punctuation_from_name,
                            capitalize_names]) 
1
2
3

这段代码很容易理解。它给人的印象是辅助功能是有功能的,因为它们似乎被链接在一起。前一个的输出包括下一个的输入。如果它们起作用,则易于验证。它们还易于重用,易于测试和易于并行化。

的工作pipeline_each()是一次将频段传递给转换函数,例如set_canada_as_country()。将功能应用于所有频段后,pipeline_each()将转换后的频段捆绑在一起。然后,它将每个传递给下一个功能。

让我们看一下转换函数。

def assoc(_d, key, value):
    from copy import deepcopy
    d = deepcopy(_d)
    d[key] = value
    return d

def set_canada_as_country(band):
    return assoc(band, 'country', "Canada")

def strip_punctuation_from_name(band):
    return assoc(band, 'name', band['name'].replace('.', ''))

def capitalize_names(band):
    return assoc(band, 'name', band['name'].title()) 
1
2
3
4
5
6
7
8
9
10
11
12
13
14

每个人都将频段上的键与新值相关联。没有改变原始频段的简单方法。assoc()通过使用deepcopy()生成传递的字典的副本来解决此问题。每个转换函数都会对副本进行修改并返回该副本。

一切似乎都很好。当键与新值相关联时,可以防止带字典的原始内容发生变异。但是上面的代码还有另外两个潜在的突变。在中strip_punctuation_from_name(),未标点名称是通过调用replace()原始名称生成的。在中capitalize_names(),大写名称是通过调用title()原始名称生成的。如果replace()title()不起作用,strip_punctuation_from_name()并且capitalize_names()不起作用。

幸运的是,replace()title()没有发生变异,他们在工作中的字符串。这是因为字符串在Python中是不可变的。例如,当replace()对乐队名称字符串进行操作时,原始乐队名称将被复制并replace()在副本上被调用。ew

Python中字符串和字典的可变性之间的这种对比说明了Clojure之类的语言的吸引力。程序员无需考虑是否正在变异数据。他们不是。

练习4。尝试编写pipeline_each函数。考虑一下操作顺序。数组中的波段一次传递一个波段到第一个变换函数。将结果数组中的波段一次传递给第二个转换函数。依此类推。

我的解决方案:

def pipeline_each(data, fns):
    return reduce(lambda a, x: map(x, a),
                  fns,
                  data) 
1
2
3
4

这三个变换函数都归结为对通过的频段上的特定字段进行更改。call()可以用来抽象它。它需要一个函数来应用,并应用值的键。

set_canada_as_country = call(lambda x: 'Canada', 'country')
strip_punctuation_from_name = call(lambda x: x.replace('.', ''), 'name')
capitalize_names = call(str.title, 'name')

print pipeline_each(bands, [set_canada_as_country,
                            strip_punctuation_from_name,
                            capitalize_names]) 
1
2
3
4
5
6
7

或者,如果我们为简洁而愿意牺牲可读性,则:

print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
                            call(lambda x: x.replace('.', ''), 'name'),
                            call(str.title, 'name')]) 
1
2
3

的代码call()

def assoc(_d, key, value):
    from copy import deepcopy
    d = deepcopy(_d)
    d[key] = value
    return d

def call(fn, key):
    def apply_fn(record):
        return assoc(record, key, fn(record.get(key)))
    return apply_fn 
1
2
3
4
5
6
7
8
9
10

这里有很多事情。让我们一步一步来。

之一。call()是一个高阶函数。高阶函数将一个函数作为参数,或者返回一个函数。或者,就像call(),两者都可以。

二。apply_fn()看起来与三个转换函数非常相似。它需要一个记录(一个乐队)。它查找处的值record[key]。它要求fn该值。它将结果分配回记录的副本。它返回副本。

三。call()没有任何实际工作。apply_fn(),当被调用时,将完成工作。在上述使用示例中pipeline_each(),的一个实例apply_fn()将在通过的频段上设置'country''Canada'。另一个实例将大写通过的乐队的名称。

四。当一个apply_fn()实例上运行,fnkey不会在范围之内。它们既不是参数apply_fn(),也不是其内部的局部变量。但是它们仍然可以访问。定义函数后,它将保存对它关闭的变量的引用:那些在函数外部的作用域中定义且在函数内部使用的变量。当函数运行并且其代码引用了变量时,Python在本地变量和参数中查找变量。如果在该处找不到它,它将在已保存的引用中查找封闭变量。这是它将找到fn和的地方key

五。call()代码中没有提到频段。那是因为call()可以用来为任何程序生成管道功能,而与主题无关。函数式编程部分是关于建立通用,可重用,可组合函数的库。

做得好。闭包,高阶函数和变量范围都在几段中涵盖。喝一杯柠檬水。

还有一个波段处理要做。那就是除去名字和国家以外的所有东西。extract_name_and_country()可以提取该信息:

def extract_name_and_country(band):
    plucked_band = {}
    plucked_band['name'] = band['name']
    plucked_band['country'] = band['country']
    return plucked_band

print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
                            call(lambda x: x.replace('.', ''), 'name'),
                            call(str.title, 'name'),
                            extract_name_and_country])

# => [{'name': 'Sunset Rubdown', 'country': 'Canada'},
#     {'name': 'Women', 'country': 'Canada'},
#     {'name': 'A Silver Mt Zion', 'country': 'Canada'}] 
1
2
3
4
5
6
7
8
9
10
11
12
13
14

extract_name_and_country()本来可以写成称为的通用函数pluck()pluck()将被这样使用:

print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
                            call(lambda x: x.replace('.', ''), 'name'),
                            call(str.title, 'name'),
                            pluck(['name', 'country'])]) 
1
2
3
4

练习5pluck()提取要从每个记录中提取的键的列表。尝试并编写它。它需要是一个高阶函数。

我的解决方案:

def pluck(keys):
    def pluck_fn(record):
        return reduce(lambda a, x: assoc(a, x, record[x]),
                      keys,
                      {})
    return pluck_fn 
1
2
3
4
5
6

现在怎么办?

功能代码可以与其他样式的代码很好地共存。本文中的转换可以应用于任何语言的任何代码库。尝试将它们应用于您自己的代码。

想想玛丽,艾斯拉和山姆。将列表的迭代转化为地图并进行简化。

想想种族。将代码分解为函数。使这些功能起作用。将重复一个过程的循环变成递归。

想想乐队。将一系列操作转换为管道。

1不变的数据是无法更改的数据。某些语言(例如Clojure)默认使所有值不可变。任何“变异”操作都将复制该值,对其进行更改,然后将更改后的副本传回。这样可以消除由于程序员的不完整模型(可能导致他们的程序进入的状态)引起的错误。

2支持一流功能的语言使功能可以像其他任何值一样对待。这意味着它们可以被创建,传递给函数,从函数返回并存储在数据结构内部。

3尾叫优化是一种编程语言功能。每次函数递归时,都会创建一个新的堆栈框架。堆栈框架用于存储当前函数调用的参数和局部值。如果函数重复多次,则解释器或编译器可能会用完内存。具有尾部调用优化功能的语言将其整个递归调用序列重用相同的堆栈框架。像Python这样的没有尾部调用优化的语言通常会将函数递归的次数限制为数千个。对于该race()功能,只有五个时间步长,因此很安全。

4 Currying意味着将接受多个参数的函数分解为接受第一个参数的函数,并返回接受下一个参数的函数,以此类推。

5并行化是指在不同步的情况下同时运行相同的代码。这些并发进程通常在多个处理器上运行。

6惰性求值是一种编译器技术,可避免在需要结果之前运行代码。

7如果重复每次都产生相同的结果,则该过程是确定性的。