12.3.3 平面映射(flattening projection)

728 篇文章 1 订阅
16 篇文章 0 订阅
12.3.3 平面映射(flattening projection)



    平面映射,可以为来自源集合的每个元素的元素生成一个序列,合并所有返回的序列。我们很快就会看到,这是一个基本操作,可以用来定义其他处理操作,包括映射和筛选。有关平面映射唯一的事情就是,为每个输入元素生成多个输出元素。



注意



    在 LINQ 库中,这个操作被称为 SelectMany。在查询表达式中,用多个 from 子句表示。名字反映事实,它类似于 Select 操作,除了能够为源中的每个项目返回多个元素之外。F# 库的等效函数是 Seq.collect。在这里,其名字暗示实现,就像调用 Seq.map 函数来生成多个序列的序列,然后,调用 Seq.concat,将它们串联起来。



    我们首先来看一个需要平面映射的例子,这意味着,我们不可能只用高阶函数来写这个示例,像前一节一样。先看一下使用 F# 序列表达式的实现,然后,再逐步修改代码,使用映射。



序列表达式中的平面映射



    假设我们有一个元组列表,每个元组包含一个城市的名字和它所在的国家,另外,我们还有一个列表,是用户所选的城市。这种情况下,我们可以像这样表示示例数据:



let cities = [ ("New York", "USA"); ("London", "UK");
                  ("Cambridge", "UK"); ("Cambridge", "USA") ]
let entered = [ "London"; "Cambridge" ]



    现在,假设我们要查找选定城市的国家。我们可以在 cities 列表中遍历所选的城市,以找到国家。你可能可能已经看出了这种方法的问题:有一个名为 Cambridge 的城市,在英国和美国都有,所以,对于一个城市,需要能够返回多条记录。在清单 12.10 中,可以看到如何来写,在序列表达式中使用两个嵌套的 for 循环。



Listing 12.10 Joining collections using sequence expressions (F# Interactive)



> seq { for name in entered do
              for (n, c) in cities do
                if (n = name) then
                  yield sprintf "%s (%s)" n c };;
val it : seq<string> =
seq [ "London (UK)"; "Cambridge (UK)"; "Cambridge (USA)" ]



    外部的 for 循环遍历 entered 中的名字,嵌套的 for 循环用于循环遍历 cities 列表。这意味着,在嵌套的循环体内部,我们会得到一个机会,来比较每个输入城市的名字,是否等于每个已知城市的名字。如果名字相同的话,嵌套在两个循环内的代码使用 yield 语句,生成一个项目。如果名字不一样,它不产生任何元素。

    在数据库术语中,这个操作可能会被解释为联接(jion)。输入名字的列表与包含城市信息的列表防意如联接,使用城市名字作为键。使用序列表达式来写这个代码是很容易的,它是 F# 中联接编码的首选方式。



    我们提到过,任何序列表达式可以使用平面映射操作进行编码,因此,我们看一下,可以如何重写前一示例,显式使用 Seq.collect。在实践中,你不会这样做,但这将是非常宝贵的,当我们研究定义自己的、类似于序列表达式的替代工作流。



直接使用平面映射



    首先,让我们看看平面映射的样子。通常,理解函数如何工作的最初一步,就是检查它的类型签名。图 12.2 比较了 Seq.map (普通映射) 和 Seq.collect (平面映射)的类型签名。







图 12.2 映射,为每个输入元素返回一个元素,而平面映射,可以返回元素的任意集合。



    提醒一下,在描述映射函数的类型签名的部分的 # ,传递给 collect,表示该函数的返回类型不必一定是 seq <'b> 类型。在前一章中,我们讨论过使用 # 的类型声明,#seq<'b> 位置上所使用的实际类型可以是实现了 seq<'b> 接口的任何类型。这意味着,我们可以返回一个序列,也可以是 F# 列表、数组,或者甚至是我们自己定义的集合类型。

    现在,让我们看一下如何重写前面的示例,使用 Seq.collect 函数。一般规则是,在序列表达式内部,可以用 Seq.collect 的调用替换每个 for 循环的使用 。这正是 F# 编译器在早期版本中已编译序列表达式所做的。因为我们的示例中,有两个嵌套的循环,要做两步的转换。在清单 12.11 中,我们先替换外部循环。



Listing 12.11 Replacing the outer loop with flattening projection (F# Interactive)



> entered |> Seq.collect (fun name –>
     seq { for (n, c) in cities do  
              if (n = name) then
                yield sprintf "%s (%s)" n c });;
val it : seq<string> =
  seq [ "London (UK)"; "Cambridge (UK)"; "Cambridge (USA)" ]



    我们用平面映射替换了外部循环,因此,清单 12.11 调用 Seq.collect,用户的输入的城市列表作为输入传递给它。我们提供的这个 Lambda 函数,取一个城市的名字,然后,遍历所有已知城市的集合,以找到包含该城市的国家。搜索的实现使用序列表达式,来自清单 12.10,只是删除了外部循环。我们使用的 lambda 函数返回一个序列,有关于指定城市的信息,Seq.collect 函数将所有这些连接在一起,返回单独的结果序列。

    现在,我们有了函数调用和序列表达式的组合,让我们再来看一下如何替换内部 for 循环,来完成转换。我们可以实现嵌套部分,用  Seq.filter 和 Seq.map,或者,甚至更好,用 Seq.choose,哪一个让我们两个操作组合成一个。我们将展示编译器所做的,它会遵循这样的规则,用平面映射替换每一个 for 循环。清单 12.12 再次显示了同样的处理代码,但只使用了 Seq.collect 调用。



Listing 12.12 Replacing both loops with flattening projection (F# Interactive)



> entered |> Seq.collect (fun name –>  
     cities |> Seq.collect (fun (n, c) –>
       if (n = name) then  
         [ sprintf "%s (%s)" n c ]
       else [] ));;
val it : seq<string> =
  seq [ "London (UK)"; "Cambridge (UK)"; "Cambridge (USA)" ]



    外部调用与清单 12.11 中的相同,但是,在 lambda 函数内部,我们现在执行另一个对 Seq.collect 的调用。嵌套的调用遍历所有的城市,对于每个城市,如果城市与输入的名称不匹配,则返回空列表;如果匹配,则返回包含一个元素的列表。如你所见,我们已经用返回一个包含单个元素列表的代码,替换 yield 的使用。如果代码包含多个 yield,可能会返回一个更长的列表。还有一点值得注意的,必须添加返回一个空列表的 else 子句;在序列表达式的内部,这是隐式的。

    虽然 Seq.collect 函数有时是有用的,比如,用高阶函数写处理序列的代码,但是,实际的重要性是,它可用于将任意序列表达式转换成函数调用。我们很快将看到,序列表达式是一个更为通用的 F# 结构的特例,平面映射是定义序列表达式如何工作的原始操作。我们还将看到,在这一节中演示的转换,与那些可以自定义值的其他计算,工作方式是类似的。

    我们前面提到过,可以利用映射和筛选实现嵌套循环,但是,正如你所看到的,序列表达式中的 for 循环,足以表达实现映射、筛选和联接,我们在这一节已经看到。现在,让我们看一下在 C# 中的相同操作。



在 C# 中使用平面映射



    LINQ 运算符类似于 collect 函数,称为 SelectMany。这两个版本之间存在着差异,因为 LINQ 有不同的要求。而 F# 序列表达式只可以使用 collect 函数表示,LINQ 查询使用许多其它运算符,所以,对于序列操作,它们需要不同的方式。

    让我们再次开始看一下普通的语法,然后,检查它如何使用扩展方法,被转换成显式语法的。我们将使用在前面的 F# 示例中的相同数据。与有关国家的信息的城市列表中包含 CityInfo 类的实例,有两个属性,输入名字的列表只包含字符串。清单  12.13 显示,我们可以写 LINQ 查询来查找输入城市的国家。



Listing 12.13 Searching for country of entered cities using a query (C#)



var q =
  from e in entered
  from known in cities
  where known.City == e
  select string.Format("{0} ({1})", known.City, known.Country);



    这个查询表达了完全相同的想法,像我们在前面的实现中所做的。它遍历两个数据源 (entered 和 cities),使我们交叉联接两个集合,然后,由用户输入的城市名,与在"已知的城市"列表中相对应的城市的记录;最后,格式化输出。

    在 C# 查询表达式语法中,我们也可以使用 jion 子句,它直接指定来自两个数据源的键 (在我们的例子中,是值 e 和 known.City 值)。这稍有不同:join 子句可能更有效,但是,多个 from 子句更灵活。特别是,我们生成的第二个序列可以取决于第一个序列的那个项目,我们当前看到的。

    正如我们刚才所说的,查询表达式被转换为正常的成员调用。在查询表达式中,第一个 from 子句之后的任何 from 子句,都被转换成一个对 SelectMany 的调用。清单 12.14 显示了由 C# 编译器执行的转换。



Listing 12.14 Query translated to explicit operator calls (C#)



var q = entered
  .SelectMany(
    e => cities,
    (e, known) => new { e, known })
  .Where(tmp => tmp.known.City == tmp.e)
  .Select(tmp => String.Format("{0} ({1})",
    tmp.known.City, tmp.known.Country));



    不像在 F # 中,if 条件被嵌套在两个 for 循环中 (平面映射),在 C# 中的操作组成在序列中而不需要嵌套。处理用 SelectMany 运算符开始实现联接;筛选和映射的执行,在序列的末尾用 Where 和 Select。

    第一个 lambda 函数指定一个集合,我们要为源列表中的每个项目生成的。这个参数对应于所提供的函数,作为一个给 F# 的 collect 函数的参数值。在查询中,我们返回所有已知的城市,所以,这个操作只执行联接,而没有任何筛选,或进一步地处理。第二个参数指定如何构建结果,基于原始序列的元素,和由该函数所返回的新生成的序列中的元素。在我们的示例中,构建了一个匿名类型,包含这两个项目,这样,我们可以在以后的查询运算符使用它们。

    在 F# 中,所有的处理是在筛选映射内部做的,这样,我们只返回最终的结果。在 C# 中,大多数处理在后面完成,所以,我们需要返回两个元素组合成一个值 (使用匿名类型),使它们能在以后访问。通常,第一个 from 子句指定查询的主要来源,如果我们添加更多的 from 子句,它们使用 SelectMany 操作,被联接到原始源中。任何进一步的运算符,比如,where 和 select 被附加到结尾,处理联接的数据源。这不同于 F# 转换,因为,在 F# 中,筛选和映射都嵌套在最里面,调用 Seq.collect。

    理解转换如何工作并不是那么重要,但是,下一节中,我们还是需要有点了解。我们将会看到, F# 序列表达式表示更普遍的想法,也可以在一定程度上使用 LINQ 查询来表示。我们已经看到的平面映射发挥关键作用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值