第12步 用map方法和for-yield变换
在指令式编程风格中,可以当场改变数据结构直到达成算法的目标。而在函数式编程风格中,需要把不可变的数据结构变换成新的数据结构来达成目标。
不可变集合的一个重要的用于实现函数式变换的方法是map。与foreach方法相同,map方法也接收一个函数作为参数。与foreach方法不同的是,foreach方法用传入的函数对每个元素执行副作用,而map方法用传入的函数将每个元素变换成新的值。举例来说,如下字符串的列表:
可以像这样将它变换成由新的字符串组成的新列表:
另一种执行这个变换的方式是使用for表达式。可以通过yield关键字(而不是do)来引入需要执行的代码体:
for-yield生成的结果与map方法生成的结果完全一样,因为编译器会把for-yield表达式变换成map方法调用。[11]由于map方法返回的列表包含了由传入的函数生成的值,因此返回的列表的元素类型将会是该函数的结果类型。在前一例中,传入的函数返回的是字符串,因此map方法返回List[String]。如果传入map方法的函数返回其他类型,则map方法返回的List也会以相应的类型作为其元素类型。例如,在下面的map方法调用中,传入的函数将字符串变换成整数来表示字符串元素的长度。因此,map方法调用的结果就是包含了这些长度的List[Int]:
像以前一样,你也可以用带有yield的for表达式来完成同样的变换:
很多类型都可以使用map方法,不仅仅是List。这让我们可以在很多类型上使用for表达式。比如说Vector,这是一个对所有它支持的操作提供“实效常量时间”(effectively constant time)性能的不可变序列。由于Vector具备带有正确签名的map方法,因此可以对Vector执行像List一样的函数式变换,可以直接调用map方法,也可以使用for-yield。例如:
请注意,当你对List执行map操作时,得到的返回值是一个新的List;而当你对Vector执行map操作时,得到的返回值是一个新的Vector。你会发现绝大多数定义了map方法的类型都具备这个模式。
最后再看一个例子,Scala的Option类型。Scala用Option表示可选的值,而不使用像Java一样用null表达此含义的传统技法。[12]Option要么是一个Some,表示值存在;要么是一个None,表示没有值。
作为一个展示Option实际使用的案例,我们可以考查一下find方法。所有的Scala集合类型,包括List和Vector,都具备find方法,其作用是查找满足给定前提的元素,这个前提是一个接收元素类型的参数并返回布尔值的函数。find方法的结果类型是Option[E],其中,E是集合的元素类型。find方法会逐个遍历集合的元素,将元素传递给前提。如果前提返回了true,find就停止遍历,并将当前元素包装在Some中返回。如果find遍历了所有元素都没有找到能通过前提判断的元素,就会返回None。下面是一些结果类型均为Option[String]的示例:
尽管Option不是一个集合,它也提供了map方法。[13]如果Option是一个Some,可被称为“已定义”的可选值,则map方法将返回一个新的包含了将原始Some元素传入map方法后得到返回值的新Option。下面的示例对startsW进行了变换,而它本来是一个包含字符串"Who"的Some:
与List和Vetcor相同,可以通过对Option执行for-yield来完成这个变换:
如果对None执行map,None意味着这是一个“未定义”的可选值,将得到一个None。下面是一个展示对startsH(一个None值)执行map操作的示例:
用for-yield完成同样的变换操作:
还可以用map方法和for-yield对其他许多类型进行变换,但就目前而言足够了。这一步的主要目的是让你对如何编写典型的Scala代码有一个直观的认识:对不可变数据结构进行函数式变换。