Reading 26: Map, Filter, Reduce

Reading 26: Map, Filter, Reduce

Safe from bugs.Correct today and correct in the unknown future.
从今天到未来一直改正,免除bug。
Easy to understand .Communicating clearly with future programmers, including future you. 易于理解,与未来的程序员(包括未来的您)进行清晰的沟通。
Ready for change.Designed to accommodate change without rewriting.
准备改变,旨在适应变化而无需重写。

目标

在本阅读中,您将学习一种用于实现对元素序列进行操作的功能的设计模式,并且您将看到如何将函数本身视为我们可以在程序中传递和操作的一流值是一个特别有力的主意。

映射/过滤/缩小
Lambda表达式
功能对象
高阶函数

/**
 * Find all the files in the filesystem subtree rooted at folder.
 * @param folder root of subtree, requires folder.isDirectory() == true
 * @return list of all ordinary files (not folders) that have folder as
 *         their ancestor
 */
public static List<File> allFilesIn(File folder) {
    List<File> files = new ArrayList<>();
    for (File f : folder.listFiles()) {
        if (f.isDirectory()) {
            files.addAll(allFilesIn(f));
        } else if (f.isFile()) {
            files.add(f);
        }
    }
    return files;
}

这就是过滤方法的样子,它将该文件列表限制为仅Java文件(可以像这样调用onlyFilesWithSuffix(files, “.java”))

/**
 * Filter a list of files to those that end with suffix.
 * @param files list of files
 * @param suffix string to test
 * @return a new list consisting of only those files whose names end with
 *         suffix
 */
public static List<File> onlyFilesWithSuffix(List<File> files, String suffix) {
    List<File> result = new ArrayList<>();
    for (File f : files) {
        if (f.getName().endsWith(suffix)) {
            result.add(f);
        }
    }
    return result;
}

→示例的完整Java代码

在本文中,我们讨论了map / filter / reduce,这是一种设计模式,可以大大简化对元素序列进行操作的功能的实现。在此示例中,我们将有很多序列-文件列表;输入流是行序列;作为单词序列的行;频率表是(单词,计数)对的序列。Map / filter / reduce将使我们能够在没有显式控制流的情况下对那些序列进行操作-而不是单个for循环或if语句。

在此过程中,我们还将看到一个重要的重要思想:将函数作为“一流”的数据值,这意味着它们可以存储在变量中,作为参数传递给函数,并像其他值一样动态创建。

在Java中使用一流的函数比较冗长,使用一些不熟悉的语法,并且由于静态类型而具有一些额外的复杂性。因此,要开始使用map / filter / reduce,我们将切换回Python。

抽象控制流

我们已经看到一种设计模式,它从对数据结构进行迭代的细节中抽象出来:迭代器。

迭代器抽象

迭代器为您提供了数据结构中的一系列元素,而无需担心数据结构是集合还是令牌流,列表还是数组,Iterator无论数据结构如何,外观都是相同的。

例如,给定a List files,我们可以使用索引进行迭代:

for (int ii = 0; ii < files.size(); ii++) {
    File f = files.get(ii);
    // ...
   

但是此代码取决于的size和get方法List,在另一个数据结构中可能有所不同。使用迭代器可以抽象出细节:

Iterator<File> iter = files.iterator();
while (iter.hasNext()) {
    File f = iter.next();
    // ...

现在,对于提供的任何类型,循环都是相同的Iterator。实际上,有一个用于此类类型的接口:Iterable。任何Iterable可与Java的使用增强的for语句- for (File f : files)-和引擎盖下,它使用一个迭代器。

映射/过滤/减少抽象

本阅读中的map / filter / reduce模式与Iterator相似,但具有更高的层次:它们将元素的整个序列视为一个单元,因此程序员不必单独命名和使用元素。 。在这种范式中,控制语句消失了:具体地说,我们入门示例中的for语句,if语句和return代码中的语句将消失。我们也将能够摆脱大部分的临时名称(即,局部变量files,f和result)。

顺序

让我们想象一个抽象数据类型Seq,它表示type元素的序列E。

例如,[1, 2, 3, 4]∈ Seq。

任何具有迭代器的数据类型都可以视为一个序列:数组,列表,集合等。字符串也是(字符)序列(尽管Java的字符串不提供迭代器)。Python在这方面更加一致:列表不仅可以迭代,而且字符串,元组(不可变列表)甚至输入流(生成一系列行)也可以迭代。我们首先在Python中看到这些示例,因为您的语法非常易读和熟悉,然后我们将了解它在Java中的工作方式。

我们将对序列进行三种操作:映射,过滤和缩小。让我们依次查看每个对象,然后查看它们如何协同工作。

映射

Map将一元函数应用于序列中的每个元素,并以相同的顺序返回包含结果的新序列:

映射:(E→F)×Seq <‍E>→Seq <‍F>

例如,在Python中:

>>> from math import sqrt
>>> map(sqrt, [1, 4, 9, 16])
[1.0, 2.0, 3.0, 4.0]
>>> map(str.lower, ['A', 'b', 'C'])
['a', 'b', 'c']
map 是内置的,但在Python中也很容易实现:
def map(f, seq):
    result = []
    for elt in seq:
        result.append(f(elt))
    return result

此操作捕获用于序列操作的通用模式:对序列的每个元素执行相同的操作。

作为价值的功能

让我们在这里暂停一秒钟,因为我们正在对函数做一些不寻常的事情。所述map函数接受到的参考函数作为第一个参数-不到该函数的结果。当我们写

map(sqrt, [1, 4, 9, 16])

我们没有打电话 sqrt(就像打电话一样sqrt(25)),而是使用了它的名字。在Python中,函数的名称是对表示该函数的对象的引用。您可以根据需要将该对象分配给另一个变量,但其行为仍类似于sqrt:

>>> mySquareRoot = sqrt
>>> mySquareRoot(25)
5.0
您还可以将对函数对象的引用作为参数传递给另一个函数,这就是我们在这里所做的map。您可以像在Python中使用其他任何数据值(例如数字,字符串或对象)一样使用函数对象。

我们已经看到了如何使用内置库函数作为一等值。我们如何使自己的?一种方法是使用熟悉的函数定义,该定义为函数命名:

>>> def powerOfTwo(k):
...     return 2**k
... 
>>> powerOfTwo(5)                 
32
>>> map(powerOfTwo, [1, 2, 3, 4])
[2, 4, 8, 16]

但是,当您只需要一个位置的函数时(通常是在使用函数进行编程时会出现这种情况),使用lambda表达式更方便:

lambda k: 2**k
此表达式表示一个k返回值2 k的自变量(称为)的函数。您可以在任何曾经使用过的地方使用它powerOfTwo:

>>> (lambda k: 2**k)(5)
32
>>> map(lambda k: 2**k, [1, 2, 3, 4])
[2, 4, 8, 16]

不幸的是,Python lambda表达式在语法上仅限于可以仅用一条return语句(没有if语句,没有for循环,没有局部变量)编写的函数。但是请记住,无论如何,这是我们使用map / filter / reduce的目标,因此它不会成为严重的障碍。

使用地图的更多方法

即使您不关心函数的返回值,Map还是很有用的。例如,当您具有一系列可变对象时,可以在它们上映射一个mutator操作:

map(IOBase.close, streams) # closes each stream on the list
map(Thread.join, threads)  # waits for each thread to finish

某些版本的地图(包括Python的内置map)也支持带有多个参数的映射功能。例如,您可以按元素添加两个数字列表:

>>> import operator
>>> map(operator.add, [1, 2, 3], [4, 5, 6])
[5, 7, 9]

筛选

下一个重要的序列操作是filter,它使用一元谓词测试每个元素。保留满足谓词的元素;那些没有被删除。返回一个新列表;过滤器不会修改其输入列表。

过滤器:(E→布尔值)×Seq <‍E>→Seq <‍E>

Python范例:


>>> filter(str.isalpha, ['x', 'y', '2', '3', 'a']) 
['x', 'y', 'a']
>>> def isOdd(x): return x % 2 == 1
... 
>>> filter(isOdd, [1, 2, 3, 4])
[1, 3]
>>> filter(lambda s: len(s)>0, ['abc', '', 'd'])
['abc', 'd']

我们可以直接定义过滤器:

def filter(f, seq):
    result = []
    for elt in seq:
        if f(elt):
            result.append(elt)
    return result

减少

我们的最终运算符reduce,使用二进制函数将序列的元素组合在一起。除了函数和列表之外,它还采用一个初始值来初始化归约,如果列表为空,则最终成为返回值。

减少:(F×E→F)×Seq <‍E>×F→F

reduce(f, list, init) 从左到右组合列表的元素,如下所示:

结果0 =初始
结果1 = f(结果0,list [0])
结果2 = f(结果1,list [1])

结果n = f(结果n-1,list [n-1])
结果n是n元素列表的最终结果。

加数字可能是最直接的例子:

>>> reduce(lambda x,y: x+y, [1, 2, 3], 0)
6
# --or--
>>> import operator
>>> reduce(operator.add, [1, 2, 3], 0)
6

化简操作中有两种设计选择。首先是是否需要初始值。在Python的reduce函数中,初始值是可选的,如果省略它,则reduce使用列表的第一个元素作为其初始值。因此,您会得到如下所示的行为:

结果0 =未定义(如果列表为空,则减少抛出异常)
结果1 = list [0]
结果2 = f(结果1,列表[1])

结果n = f(结果n-1,列表[ n-1])
这样可以更轻松地使用max没有明确定义的初始值的reducer之类的:

>>> reduce(max, [5, 8, 3, 1])
8

第二个设计选择是元素堆积的顺序。对于喜欢这样的关联运算符add,max这没有什么区别,但是对于其他运算符,它可以。Python的reduce在其他编程语言中也称为fold-left,因为它结合了从左(第一个元素)开始的顺序。 向右折向另一个方向:

右折:(E×F→F)×Seq <‍E>×F→F

其中fold-right(f, list, init)的n元素列表的遵循以下模式:

结果0 =初始
结果1 = f(list [n-1],结果0)
结果2 = f(list [n-2],结果1)

结果n = f(list [0],结果n- 1)
产生结果n作为最终结果。

以下是两种减少方法的图表:从左或从右:

左折:(F×E→F)×Seq <‍E>×F→F

折左(-,[1,2,3],0)= -6
右折:(E×F→F)×Seq <‍E>×F→F
右折(-,[1,2,3],0)= 2
reduce操作的返回类型不必与list元素的类型匹配。例如,我们可以使用reduce将序列粘在一起形成字符串:

>>> reduce(lambda s,x: s+str(x), [1, 2, 3, 4], '') 
'1234'

或将嵌套的子列表展平为一个列表:

>>> reduce(operator.concat, [[1, 2], [3, 4], [], [5]], [])
[1, 2, 3, 4, 5]

这是一个足够有用的序列操作,我们将其定义为flatten,尽管这只是内部的减少步骤:

def flatten(list):
    return reduce(operator.concat, list, [])

更多例子

假设我们有一个表示为系数列表a [0],a [1],…,a [n-1]的多项式,其中a [i]是x i的系数。然后,我们可以使用map进行评估并减少:

def evaluate(a, x):
    xi = map(lambda i: x**i, range(0, len(a))) # [x^0, x^1, x^2, ..., x^(n-1)]
    axi = map(operator.mul, a, xi)             # [a[0]*x^0, a[1]*x^1, ..., a[n-1]*x^(n-1)]
    return reduce(operator.add, axi, 0)        # sum of axi

此代码使用便捷的Python生成器方法range(a,b),该方法生成从a到b-1的整数列表。在map / filter / reduce编程中,这种方法替换了for从a索引到b的循环。

现在,让我们看一个典型的数据库查询示例。假设我们有一个关于数码相机数据库,其中,每个对象的类型的Camera与观察者方法其属性(brand(),pixels(),cost()等等)。整个数据库在一个名为的列表中cameras。然后,我们可以使用map / filter / reduce描述对该数据库的查询:

# What's the highest resolution Nikon sells? 
reduce(max, map(Camera.pixels, filter(lambda c: c.brand() == "Nikon", cameras)))

关系数据库使用map / filter / reduce范式(在这里称为项目/选择/聚合)。 SQL(结构化查询语言)是用于查询关系数据库的事实上的标准语言。典型的SQL查询如下所示:

select max(pixels) from cameras where brand = "Nikon"

cameras是一个序列(行的列表,其中每一行都有一个摄像机的数据)

where brand = "Nikon"是一个过滤器

pixels是一张地图(仅从行中提取像素字段)

max是减少

返回介绍示例

回到开始的示例,我们想在我们的项目中查找Java文件中的所有单词,让我们尝试创建一个有用的抽象来通过后缀过滤文件:

def fileEndsWith(suffix):
    return lambda file: file.getName().endsWith(suffix)

fileEndsWith返回可用作过滤器的函数:它采用类似文件名的后缀,.java并动态生成一个可与过滤器一起使用以测试该后缀的函数:

filter(fileEndsWith(".java"), files)

fileEndsWith是与我们通常的功能不同的野兽。这是一个高阶函数,意味着它是一个将另一个函数作为参数或返回另一个函数作为结果的函数。高阶函数是对函数数据类型的操作;在这种情况下,fileEndsWith是函数的创建者。

现在,让我们使用map,filter和flatten(我们在上面使用reduce定义的)​​来递归遍历文件夹树:

def allFilesIn(folder):
    children = folder.listFiles()
    subfolders = filter(File.isDirectory, children)
    descendants = flatten(map(allFilesIn, subfolders))
    return descendants + filter(File.isFile, children)

第一行获取该文件夹的所有子项,如下所示:

["src/client", "src/server", "src/Main.java", ...]

接下来的两行是遍历的本质:仅过滤属于文件夹(也称为目录)的子项,然后allFilesIn针对此子文件夹列表进行递归映射。结果可能如下所示:

[["src/client/MyClient.java", ...], ["src/server/MyServer.java", ...], ...]

因此,我们必须将其展平以删除嵌套结构。然后,我们添加直接文件(不是文件夹)的直接子级,这就是我们的结果。

我们还可以使用map / filter / reduce解决其他问题。有了要从中提取单词的文件列表后,就可以加载它们的内容了。我们可以使用map来获取它们的路径名作为字符串,打开它们,然后以文件列表的形式读取每个文件:

pathnames = map(File.getPath, files)
streams = map(open, pathnames)
lines = map(list, streams)

实际上,这看起来像是单个映射操作,我们希望将三个函数应用于元素,因此让我们暂停以创建另一个有用的高阶函数:将函数组合在一起。

def compose(f, g):
    """Requires that f and g are functions, f:A->B and g:B->C.
    Returns a function A->C by composing f with g.""" 
    return lambda x: g(f(x))

现在我们可以使用一个地图:

lines = map(compose(compose(File.getPath, open), list), files)

更好的是,因为我们已经有三个要应用的功能,所以让我们设计一种组合任意功能链的方法:

def chain(funcs):
    """Requires funcs is a list of functions [A->B, B->C, ..., Y->Z]. 
    Returns a fn A->Z that is the left-to-right composition of funcs."""
    return reduce(compose, funcs)

这样地图操作就变成了:

lines = map(chain([File.getPath, open, list]), files)

现在,我们看到了更多的一流功能。我们可以将函数放入数据结构中,并对这些数据结构使用操作,例如对函数本身进行映射,缩小和过滤!

由于此映射将生成行列表(每个文件一个行列表),因此我们将其展平以得到单个行列表,而忽略文件边界:

allLines = flatten(map(chain([File.getPath, open, list]), files))

然后,我们将每一行类似地分成单词:

allLines = flatten(map(chain([File.getPath, open, list]), files))

然后,我们将每一行类似地分成单词:

words = flatten(map(str.split, allLines))

至此,我们已经在项目的Java文件中列出了所有单词!如所承诺的,控制语句已消失。

抽出控制权的好处

Map / filter / reduce通常可以使代码更短,更简单,并使程序员可以专注于计算的核心,而不是循环,分支和控制流的细节。
通过按照映射,过滤和归约方式安排程序,尤其是尽可能使用不变的数据类型和纯函数(不会使数据发生突变的函数),我们创造了更多的安全并发机会。使用不可变数据类型上的纯函数的映射和过滤器可立即并行化-在序列的不同元素上调用函数可以在不同的线程,不同的处理器,甚至不同的机器上运行,结果仍然是相同的。MapReduce是一种以这种方式并行化大型计算的模式。

Java中的功能接口

Java提供了一些标准功能接口,我们可以使用它们来以map / filter / reduce模式编写代码,例如:

Function<T,R>代表从T到的一元函数R
BiFunction<T,U,R>表示从T×U到的二进制函数R
Predicate表示从T到布尔的函数
因此,我们可以在Java中实现Map,如下所示:

/**
 * Apply a function to every element of a list.
 * @param f function to apply
 * @param list list to iterate over
 * @return [f(list[0]), f(list[1]), ..., f(list[n-1])]
 */
public static <T,R> List<R> map(Function<T,R> f, List<T> list) {
    List<R> result = new ArrayList<>();
    for (T t : list) {
        result.add(f.apply(t));
    }
    return result;
}

这是一个使用地图的例子;首先,我们将使用熟悉的语法编写它:

// anonymous classes like this one are effectively lambda expressions
Function<String,String> toLowerCase = new Function<>() {
    public String apply(String s) { return s.toLowerCase(); }
};
map(toLowerCase, Arrays.asList(new String[] {"A", "b", "C"}));

并带有lambda表达式:

map(s -> s.toLowerCase(), Arrays.asList(new String[] {"A", "b", "C"}));
// --or--
map((s) -> s.toLowerCase(), Arrays.asList(new String[] {"A", "b", "C"}));
// --or--
map((s) -> { return s.toLowerCase(); }, Arrays.asList(new String[] {"A", "b", "C"}));

在该例子中,λ表达式只是包裹到一个呼叫String的toLowerCase。我们可以使用方法引用来避免编写语法为的lambda ::。我们所引用的方法的签名必须与功能接口所需的签名相匹配,才能满足静态类型的要求:

map(String::toLowerCase, Arrays.asList(new String[] {"A", "b", "C"}));

如果需要详细信息,可以在Java教程中阅读有关方法引用的更多信息。

在Java中使用方法引用(相对于调用)具有与在Python中按名称引用功能(相对于调用函数)相同的目的。

Java中的Map / filter / reduce

所述抽象序列类型我们定义在上述的Java形式存在Stream,其定义map,filter,reduce,以及许多其他操作。

集合类型,如List并Set提供一个stream()操作返回一个Stream收集,并且有一个Arrays.stream用于创建函数Stream从数组。

这是allFilesInJava中带有map和filter的一种实现:

public class Words {
    static Stream<File> allFilesIn(File folder) {
        File[] children = folder.listFiles();
        Stream<File> descendants = Arrays.stream(children)
                                         .filter(File::isDirectory)
                                         .flatMap(Words::allFilesIn);
        return Stream.concat(descendants,
                             Arrays.stream(children).filter(File::isFile));
    }

映射和展平模式是如此普遍,以至于Java提供了一种flatMap操作来做到这一点,我们已经使用了它而不是定义它flatten。

这里是endsWith:

    static Predicate<File> endsWith(String suffix) {
        return f -> f.getPath().endsWith(suffix);
    }

给定一个Stream files,我们现在可以编写例如files.filter(endsWith(".java"))以获取一个新的过滤流。

查看此示例的修订后的Java代码。

您可以比较所有三个版本:熟悉的Java实现,带有map / filter / reduce的Python和带有map / filter / reduce的Java。

Java中的高阶函数

Map / filter / reduce当然是高阶函数;所以是endsWith上方。让我们再看看之前看到的两个:compose和chain。

该Function接口提供了compose—但是实现非常简单。特别是,一旦您获得了参数的类型并正确返回了值,Java的静态类型将使错误的方法体变得几乎是不可能的:

/**
 * Compose two functions.
 * @param f function A->B
 * @param g function B->C
 * @return new function A->C formed by composing f with g
 */
public static <A,B,C> Function<A,C> compose(Function<A,B> f,
                                            Function<B,C> g) {
    return t -> g.apply(f.apply(t));
    // --or--
    // return new Function<A,C>() {
    //     public C apply(A t) { return g.apply(f.apply(t)); }
    // };
}

事实证明,由于s(和其他集合)必须是同质的,所以我们不能chain使用强类型的Java编写List-我们可以指定一个列表,其元素均为type Function<A,B>,但不能指定第一个元素为a Function<A,B>,第二个为a的列表。Function<B,C>, 等等。

但是这里是chain针对相同输入/输出类型的功能的:

/**
 * Compose a chain of functions.
 * @param funcs list of functions A->A to compose
 * @return function A->A made by composing list[0] ... list[n-1]
 */
public static <A> Function<A,A> chain(List<Function<A,A>> funcs) {
    return funcs.stream().reduce(Function.identity(), Function::compose);
}

我们的Python版本未在中使用初始值reduce,它需要一个非空的函数列表。在Java中,我们提供了标识函数(即f(t)= t)作为约简的标识值。

概括

本文内容是关于问题的建模和使用不可变数据和操作的系统的实现,这些数据和操作实现的是纯函数,而可变数据和操作则具有副作用。 函数式编程就是这种编程风格的名称。

当您具有使用语言的一流函数时,可以轻松进行函数式编程,并且可以构建高阶函数来抽象出控制流代码。

某些语言(Haskell,Scala,OCaml)与函数式编程紧密相关。许多其他语言(JavaScript,Swift,几种 .NET 语言,Ruby等)或多或少地使用了函数式编程。借助Java最近添加的功能语言功能,如果您继续使用Java进行编程,那么您应该期望在那里也能看到更多的功能编程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值