轨道
/
Julia
Julia
/
练习
/
奶酪俱乐部
奶酪俱乐部

奶酪俱乐部

学习练习

简介

严格来说,Higher Order Function就是一个函数,只要它满足以下至少一条:

  • 把函数作为自己的一个实参。
  • 把函数作为结果返回。

在函数式编程的世界里,这个词的用法往往更窄。它通常指 filter、map和reduce这类函数,它们会把传入的函数应用到集合的各个元素上。

对集合进行操作

学到课程体系的这个阶段,我们已经见过多种把某个操作应用到可迭代集合(比如 Vector)所有元素上的方式:

  • 使用循环(自数字计算诞生以来,大多数编程语言都是这么做的)。
  • 使用推导式(Python 风格)。
  • 使用广播(Julia 特有的语法,不过深受 R、Matlab 和 NumPy 的影响)。

本概念将重点介绍高阶函数(在 Haskell 或 F# 这类函数式语言里都能见到)。

其他可能的方式包括:

  • 递归(ML 系语言的做法)。
    • Julia 允许这么做,但没有尾调用优化,有栈溢出的风险。
  • 用宏进行元编程(传统上是 Lisp 的特性)。
    • 在高级 Julia 编程中这被广泛使用,但在大多数情况下要_谨慎使用_。
    • 其他方案通常更容易编写,调试起来也容易得多。

过滤

filter()函数接收一个返回布尔值的传入函数,并把它应用到集合上。只有返回 true的元素才会包含在返回值中,返回值的_基本_类型与输入相同(见下文)。

julia> filter(iseven, 1:6)
3-element Vector{Int64}:
 2
 4
 6

# String is a collection of Chars, so String in -> String out
julia> filter(!isascii, "Hrōðgār")
"ōðā"

# tuple input -> tuple output
julia> filter(iseven, (1, 2, 3, 4, 5))
(2, 4)

对于多维数组,filter会把输入的维度展平,返回一个 Vector:这是“输出类型与输入类型保持一致”这一规则最主要的例外。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> filter(isodd, m)
3-element Vector{Int64}:
 1
 5
 3

上面的例子用的都是内置函数,但在这种场景下,匿名函数用得也非常多。

julia> filter(x -> x % 3 == 0, 1:20)
6-element Vector{Int64}:
  3
  6
  9
 12
 15
 18

还有一个原地版本 filter!(),本概念中的许多函数都有这样的版本。

映射

map()函数通过对每个元素应用一个函数来转换集合。在简单的情况下,这和广播很相似,输出的形状与输入一致。

julia> map(√, [1, 4, 9])
3-element Vector{Float64}:
 1.0
 2.0
 3.0

julia> map(x -> x^2 + 1, 1:4)
4-element Vector{Int64}:
  2
  5
 10
 17

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> map(√, m)
2×3 Matrix{Float64}:
 1.0  1.41421  1.73205
 2.0  2.23607  2.44949

map()也可以对多个集合逐元素操作。

julia> map(*, [1, 2], [3, 4])
2-element Vector{Int64}:
 3
 8

从概念上讲,这相当于先对多个输入集合运行 zip(),再对中间结果的每个元素运行 map()。这只是一个粗略的类比,并不代表实现上也是如此!

和 zip()一样,形状不匹配的集合会被截断到最小者的维度。

有时只需要传入函数的副作用,比如写数据库,或者向数组 push!。这时可以使用高阶函数 foreach(),它总是返回 nothing。

归约

reduce()函数接收一个有两个实参的函数,把它应用到集合上,从而降低维度。

抽象地听起来可能有点绕,但想想 sum()或 prod()这类函数,它们接收一个集合,返回单个值。

julia> sum(1:4) # add
10

julia> prod(1:4) # multiply
24

这些专用函数经过了高度优化,只要能用就应该始终使用。 其他例子包括 maximum()和 minimum()、逻辑函数 all()和 any(),以及许多统计函数。

仅为说明起见,看看用更通用的 reduce()实现同样的功能(回想一下,中缀运算符 +和*在内部其实就是函数)。

julia> reduce(+, 1:4) # add
10

julia> reduce(*, 1:4) # multiply
24

和 sum()及其他聚合函数一样,reduce()可以接收一个可选的关键字参数 dims,用来指定要归约的维度。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> reduce(+, m; dims=1)
1×3 Matrix{Int64}:
 5  7  9

这些例子很简单,因为加法和乘法都满足交换律(1+2 == 2+1)和结合律((1+2)+3 == 1+(2+3))。

但这远不是普遍情况!就连减法和除法这样常见的运算也不满足结合律。

还有一个问题:浮点数误差会在大型集合上累积,所以从左到右的归约结果可能和从右到左略有不同。

Julia 的 reduce函数的方向取决于具体实现,并没有保证。

要显式控制方向,可以用 foldl()和 foldr()函数,名义上分别从“左”和“右”开始(对 Vector 而言,实际上是顶部和底部)。

julia> foldl(-, 1:3) # (1 - 2) - 3
-4

julia> foldr(-, 1:3) # 1 - (2 - 3)
2

注意,这两个函数是为可以当作一维来看待的集合设计的,返回标量结果。foldl和 foldr不支持 dims实参,只有 reduce支持。

MapReduce

把 map操作和 reduce结合起来,在各种编程领域都非常常见。

我们可以依次运行 map,再对中间集合运行 reduce。但这样充其量效率不高,而且随着集合变大,扩展性会非常差。

因此_强烈_建议改用合并后的 mapreduce()函数。它可以实现性能高得多的算法,把 map 和 reduce 操作交织在一起。

第一个实参是用来映射的函数,第二个实参是归约运算符。

julia> mapreduce(x -> x^2 + 1, +, 1:3)
17

# equivalent to (2 + 5 + 10)
julia> sum(map(x -> x^2 + 1, 1:3))
17

如我们所料,对于方向很重要的场景,Julia 也有 mapfoldl()和 mapfoldr()函数。

说明

我们正在创办一个奶酪俱乐部,它将利用机器学习,根据顾客的历史记录和口味,挑选出新的奶酪,提供给热爱奶酪的顾客。

新会员需要填写一份初始调查问卷,以便我们先收集一些基础数据。 通过这份问卷,我们发现有一小部分顾客的评价非常极端,缺少层次。 因为这可能会不可逆地让更精细的算法产生偏差,所以我们单独设置了一套算法来处理他们的需求。 请你提供一些辅助函数,来整理他们的数据。

Note

解决下面这些任务可能有多种方式,但每个任务都可以用一个不同的高阶函数来完成。

1. 对顾客进行分类

评分系统以五星为基准,也就是由整数1:5构成。 极端顾客只会给出1或5的评分,我们想知道某位顾客是否表现出这种行为。

实现all_15(),它接收一个评分向量,如果所有评分都是1或5,就返回true,否则返回 false。

julia> ratings = [2, 3, 4, 4, 1];

julia> all_15(ratings)
false

julia> ratings = [1, 5, 5, 1, 5];

julia> all_15(ratings)
true

2. 分离出极端顾客

我们需要把评价更极端的顾客和其他顾客分开。

实现emphatics(),它接收一个顾客与评分的字典。 返回一个类似的字典,其中只包含那些仅给出1星或5星评分的顾客。

julia> ratings = ([2, 3, 5, 1, 1], [1, 1, 5, 5, 1], [4, 5, 5, 3, 2], [5, 5, 1, 1, 5]);

julia> names = ("c1", "c2", "c3", "c4");

julia> customers = Dict(zip(names, ratings))
Dict{String, Vector{Int64}} with 4 entries:
  "c2" => [1, 1, 5, 5, 1]
  "c1" => [2, 3, 5, 1, 1]
  "c3" => [4, 5, 5, 3, 2]
  "c4" => [5, 5, 1, 1, 5]

julia> emphatics(customers)
Dict{String, Vector{Int64}} with 2 entries:
  "c2" => [1, 1, 5, 5, 1]
  "c4" => [5, 5, 1, 1, 5]

3. 把评分转换为二进制

由于极端顾客只用1和5评分,把这些评分改成0和1在计算上会方便得多。

实现tobinary(),它接收一个极端评分的向量。 返回二进制评分,其中1已被改为0,5已被改为1。

julia> ratings = [1, 1, 5, 5, 1];

julia> tobinary(ratings)
5-element Vector{Int64}:
 0
 0
 1
 1
 0

4. 把评分组成矩阵

我们的算法使用Matrix作为输入,所以需要把数据转换成矩阵。

实现tobinarymatrix(),它接收一个由极端评分向量组成的向量。 返回转换后数据的Matrix,其中每个评分向量都是矩阵中的一行。

julia> customersratings = [[1, 1, 5, 5, 1],[5, 5, 1, 1, 5]];

julia> tobinarymatrix(customersratings)
2×5 Matrix{Int64}:
 0  0  1  1  0
 1  1  0  0  1
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Julia Exercism

准备好开始 奶酪俱乐部 了吗?

注册 Exercism,借助 35 个概念128 个练习 和真人导师指导,学习并掌握 Julia,全部免费。