scala函数式编程副作用如何理解

为什么选择Scala,它在大数据处理方面有何优势?
时间: 21:15:29
&&&& 阅读:9811
&&&& 评论:
&&&& 收藏:0
标签:近年来,关于大数据讨论已然是热火朝天,虽不说是家喻户晓,那至少对于业界来说也是引起了轩然大波。作为学生党的我,最近也在研究关于大数据的东东。作为一个技术迷,总是会想尝试一些新鲜的东西。前一段时间学习了Hadoop之后,又想看看Spark是什么东东。那么在这里有必要八卦一下Spark了。
Spark是发源于美国加州大学伯克利分校AMPLab的集群计算平台。它立足于内存计算,从多迭代批量处理出发,兼收并蓄数据仓库、流处理和图计算等多种计算范式,是罕见的全能选手。就大数据集而言,对典型的迭代机器 学习、即席查询(ad-hoc query)、图计算等应用,Spark版本比基于MapReduce、Hive和Pregel的实现快上十倍到百倍。其中内存计算、数据本地性 (locality)和传输优化、调度优化等该居首功,也与设计伊始即秉持的轻量理念不无关系。
那么,天下武功,唯快不破,看到这里当然是以一种很激动的心情想要去学习它了。那么问题也来了,通过百度等各种小道消息打听到,Spark是采用Scala语言设计的,要想学好Spark,Scala这一关必须是要过的,并且像Twitter、Linkedin等这些公司都在用。于是,还能怎么办,学呗。。。
于是,就愉快的开始了Scala之旅,嘿嘿,然后就没有然后了。看了Scala前面的内容还好,看到后面真的是想吐血了,简直是受不了这种编写方式,不仅编译速度慢,而且编写代码过于随意、灵活,完全无法驾驭。于是,进行了内心的各种挣扎,并且还被实验室的几个研究生学长踏雪了一番,我也不能坐以待毙了,因此,我再一次选择了强大的网络,打开搜索引擎,然后查看各种八卦与新闻。以下是搜索到的各种观点。
来自知乎的小道消息:
我想大部分应用开发程序员,最关键是看有什么类库合适的方便特定领域的应用开发。就像ruby有rails做web开发,你可以去论证ruby优缺点,但实际上应用开发效率提升很大程度上依靠类库。
现在Spark是大数据领域的杀手级应用框架,BAT,我们现在几个领域巨头的客户(有保密协议不方便透露)都全面使用Spark了,这个时候再谈Scala适不适合大数据开发其实意义不大。因为大家比的不只是编程语言,而是构建在这个编程语言之上的类库、社区和生态圈(包括文档和数据、衍生类库、商业技术支持、成熟产品等等)。
那么反过来问,为什么Spark会选择Scala可能更有意义一点。Spark主创Matei在不同场合回答两次这个问题,思考的点稍微不一样,但重点是一样的,很适合回答题主的问题。总结来说最主要有三点:
1. API能做得优雅; 这是框架设计师第一个要考虑的问题,框架的用户是应用开发程序员,API是否优雅直接影响用户体验。
2. 能融合到Hadoop生态圈,要用JVM语言; Hadoop现在是大数据事实标准,Spark并不是要取代Hadoop,而是要完善Hadoop生态。JVM语言大部分可能会想到Java,但Java做出来的API太丑,或者想实现一个优雅的API太费劲。
3. 速度要快; Scala是静态编译的,所以和JRuby,Groovy比起来速度会快很多,非常接近Java。
关于Scala性能的问题,主要分两种情况,
1. Scala的基准性能很接近Java,但确实没有Java好。但很多任务的单次执行的,性能损失在毫秒级不是什么问题;
2. 在大数据计算次数很多的情况下,我们全部写成命令式,而且还要考虑GC,JIT等基于JVM特性的优化。
Scala很难是个很含糊的问题,关键是要看你想达到什么目的。
我们培训客户做Spark开发,基本上一两个星期就可以独立工作了。
当然师傅领进门,修行靠个人,一两个星期能独立工作不代表能马上成为Scala或Spark专家。
这里回答主要针对大数据产品应用开发,不是大数据分析。大数据分析是个更泛的话题,包括大数据分析实验和大数据分析产品等。实验关心建模和快速试不同方式,产品关心稳定、可拓展性。大数据分析实验首选R(SAS),python和Matlab, 通常只拿真实数据的一小部分,在一个性能很好的单机上试各种想法。Scala目前在大数据分析实验上没有太多优势,不过现在有人在做R语言的Scala实现,可以无缝和Spark等大数据平台做衔接。当然现在也已经有SparkR了,可能用R和Spark做交互。
来自InfoQ的小道消息:
Scala是一门现代的多范式编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。Scala允许用户使用命令和函数范式编写代码。Scala运行在Java虚拟机之上,可以直接调用Java类库。对于新手来说,Scala相对比较复杂,其看起来灵活的语法并不容易掌握,但是对于熟悉Scala的用户来说,Scala是一把利器,它提供了许多独特的语言机制,可以以库的形式轻易无缝添加新的语言结构。近日,Spotify的软件工程师Neville Li发表了一篇题为《数据工程师应该学习Scala的三个理由》的文章,他认为现在的编程语言种类非常多,每种语言都各有优缺点,并且它们的适用的场景也不同,比如Scala就非常适合用于数据处理和机器学习。
在大数据和机器学习领域,很多开发者都有Python/R/Matlab语言的背景,相比与Java或者C++,Scala的语法更容易掌握。从以往的经验来看,只要掌握基本的集合API以及lambda,一个没有经验的新员工就可以快速上手处理数据。像Breeze、ScalaLab和BIDMach这样的类库都通过操作符重写模仿了一些流行工具的语法以及其它的一些语法糖,简单并且容易使用。另外,Scala的性能比传统的Python或者R语言更好。
由于Scala运行于Java平台(Java虚拟机),并兼容现有的Java程序,所以Scala可以和大数据相关的基于JVM的系统很好的集成,比如基于JVM类库的框架Scalding(Cascading)、Summingbird(Scalding和Storm)、Scrunch(Crunch)、Flink(Java编写并有Scala的API),本身使用Scale开发的系统Spark、Kafka。另外,很多数据存储解决方案都支持JVM语言,比如Cassandra、HBase、Voldemort和Datomic。
函数编程范式更适合用于Map/Reduce和大数据模型,它摒弃了数据与状态的计算模型,着眼于函数本身,而非执行的过程的数据和状态的处理。函数范式逻辑清晰、简单,非常适合用于处理基于不变数据的批量处理工作,这些工作基本都是通过map和reduce操作转换数据后,生成新的数据副本,然后再进行处理。而大多数的Scala数据框架都能够把Scala数据集合API和抽象数据类型相统一,比如Scalding中的TypedPipe与Spark中的RDD都有相同的方法,包括map、flatMap、filter、reduce、fold和groupBy,这样使用Scala来处理就更为方便。开发者只需要学习标准集合就可以迅速上手其它工具包。另外,很多的类库都参考了范畴论中的一些设计,它们通过使用semigroup、monoid、group标识来保证分布式操作的正确性。
来自内存溢出的小道消息:
有不少人问过我这个问题:你为什么要学习Scala?
在最开始,我也是被各种宣传忽悠了,以为Scala是一门简单的,对Java有很多改进的,甚至可能很快就成为下一代Java的语言。当时正打算做一个网站,于是就用它边学边做。几个月后,实在无法忍受它的编译速度、各种类库的缺失、以及各种各样的编译错误,放弃了它。
但是当时创建的那个Scala群里,却有非常好的交流氛围。很多人由于对Scala很有兴趣,在群里讨论各种新鲜好玩的技术,让我大开眼界,发现Java原来只是一个小世界。另外在群里认识了杨云,也因此加入了TW,现在他是我的sponsor。
来到我们公司之后,居然有两次参与Scala项目的机会。发现我们的客户居然都喜欢用Scala,西安办公室里也有越来越多的人主动或者被迫学习Scala。由于现在有了更多学习的时间,所以我又把它捡了起来,同时惊喜地发现之前一直没搞懂的问题,现在竟然差不多理解了。
我认为我现在学习Scala的原因是:它为我打开了编程世界的一扇门,让我看到了与之前完全不同的世界。通过对它的学习,我可以强迫自己学习更多编程知识,提高自己的能力,从而有机会跟更多牛人交流。另外,我个人也看好Scala的未来,所以认为越早掌握它对自己越有利。
来自知乎的小道消息:
Scala正如其名,一门可扩展的语言,它就是一个含有精美工具的工具箱,里面有静态类型, OOP, FP, 宏等工具。
我想这是马丁设计的初衷,也是他写的书上所说的,Scala不是在单纯的混合面向对象和函数式,它是一门可被无限扩展的语言,每次添加新特性都是在丰富工具箱,所以有人说Scala太复杂了,也有人说Scala满足了我对编程语言的所有幻想。
来自segmentfault的小道消息:
个人觉得比较运行速度其实没啥意义, 因为两种语言都是生成 JVM 的字节码, 依赖 JVM 这个虚拟平台来跑代码. 除非 Scalac (scala的编译器) 有重大 bug, 生成的字节码执行让人无法接受, 否则基本上不会相差太多. 再说, scala 都到大版本2了, 这种概率实在是不大.
相比较与 Java, 在下觉得 Scala 最主要的有以下两点优势:
FP 泛型支持
如果用多了 Spring 中大量的 template 接口, 你就会觉得 FP 其实还是蛮好用的.
而这仅仅是 FP 好处的冰山一角.
函数其实就是一个 input -& output (scala 也是这么表示一个函数的), 没有任何副作用, 与状态无关, 由于这种特性, 所以函数式的编程范式在分布式领域有很多好处
对于函数式编程,我的知识实在是皮毛, 但可以这么说, FP 相对与 OO 有哪些优势, Scala 对于 Java 差不多就有哪些优势.
正因为 FP 有如此多的优势, 所以 Java8 才引入了 FP. 从某种程度上来说, Java 认可了 Scala 的做法.
类型系统支持
如果说 Java 是一种类型安全的语言, 那么毫无疑问, Scala 的类型更加安全, 从某种程度上说, Scala 的类型是图灵完备的, 而 Java 不是. 我的一位好朋友在这方面研究的比较深(
), 而我对与 Scala 的类型系统的理解, 也还是皮毛.
正是以上这两点大优势, 造成了 Scala 比 Java 更加安全, 同时又具备灵活性, 想象力.
其他语言层面上的优势
在 Java 中, 你是否有时很想继承多个 AbstractClass 呢? 对不起, Java 只支持单继承
在 Scala 中, 你可以进行 mixin (Java 8 也开始引入 default method 了呢)
在 Java 中, 想要一个 singleton ? 要么在 static block 中做, 要么利用 Enum 的单例特性完成, 或者其他更纠结的方法.
在 Scala 中, 只要声明为 object, 即为单例.
在 Java 中, 想要延迟加载一个单例? double check吧
在 Scala 中, 只要在 object 中将变量修饰为 lazy 即可
在 Java 中, 想要对集合进行一些操作? 使用一层一层的 for 循环吧
在 Scala 中, 使用 collection 的一些集合操作, 即可获得如写SQL般的享受.
在 Java 中, 在并发中想对Future进行回调? 对不起, Future 不是 Listenable (无法支持回调), 除非你使用额外的工具(如 guava, spring)
在 Scala 中, 本来就主张异步编程, future 和 promise 的配合让人非常愉快.
在 Java 中, 要透明扩展一个第三方库的类怎么办? 包装, 再加一层.
在 Scala 中, 有强大的 implicit 机制让你更优雅的做到这一点, 同时还能保证类型安全(比起 Ruby 的 monkey patch, 要安全得多)
Scala 的表达力很强, 相同功能的代码, 用 Java 和 Scala 的行数不可同日而语.
这些单单是语言层面上的优势, 除此之外, Scala 还能无缝结合 Java
尽管罗列了如此多的好处, 但 Scala 有如下劣势:
语法复杂, 学习曲线非常高
国内 Scala 程序员很难找 (目前 Scala 的影响力也在缓慢扩大, 比如 Scala 社区中的明星 Spark 的流行也在慢慢拉动 Scala 的流行, 如同 rails 之于 ruby)
社区, 生态还比较小, Scala 风格的库还非常少(但可以和 Java 很容易的斜街很多时候弥补了这一点)
对于程序员来说: Scala 很难学, 但值得学
对于企业来说: Scala 是过滤优秀(好学)程序员(Geek)的好滤斗。
。。。。。
好吧,看了这么多观点,也算是接受了一下思想的洗礼,一千个读者就有一千个哈姆雷特,用于不用各有时,爱与不爱都是伤害。那么,最为大三党的我,我想还是先放放,作为自己也后的一个学习方向,现在先掌握好一些基础知识,准备即将到来的校招,go for it。
标签:原文地址:http://blog.csdn.net/scgaliguodong123_/article/details/
&&国之画&&&& &&&&chrome插件
版权所有 京ICP备号-2
迷上了代码!Scala函数论 - 简书
Scala函数论
桃花落,闲池阁,山盟虽在,锦书难托,莫,莫,莫!
「函数(Function)」是函数式编程的基本单元。本文将重点讨论函数式理论中几个容易混淆的概念,最后通过ScalaHamcrest的实战,加深对这些概念的理解和运用。
部分应用函数
实战ScalaHamcrest
在Scala中,函数可以使用「函数字面值(Function Literal)」直接定义。有时候,函数字面值也常常称为「函数」,或「函数值」。
(s: String) =& s.toLowerCase
事实上,「函数字面值」本质上是一个「匿名函数(Anonymous Function)」。在Scala里,函数被转换为FunctionN的实例。上例等价于:
new Function1[String, String] {
def apply(s: String): String = s.toLowerCase
其中,Function1[String, String]可以简写为String =& String,因此它又等价于:
new (String =& String) {
def apply(s: String): String = s.toLowerCase
也就是说,「函数字面值」可以看做「匿名函数对象」的一个「语法糖」。
综上述,函数实际上是FunctionN[A1, A2, ..., An, R]类型的一个实例而已。例如,(s: String) =& s.toLowerCase是Function1[String, String]类型的一个实例。
在函数式编程中,函数做为一等公民,函数值可以被自由地传递和存储。例如,它可以赋予一个变量lower。
val lower: String =& String = _.toLowerCase
假如存在一个map的柯里化的函数。
def map[A, B](a: A)(f: A =& B): B = f(a)
函数值可以作为参数传递给map函数。
map("HORANCE") { _.toLowerCase }
相对于「匿名函数」,如果将「函数值」赋予def,或者val,此时函数常称为「有名函数」(Named Function)。
val lower: String =& String = _.toLowerCase
def lower: String =& String = _.toLowerCase
两者之间存在微妙的差异。前者使用val定义的变量直接持有函数值,多次使用lower将返回同一个函数值;后者使用def定义函数,每次调用lower将得到不同的函数值。
在Scala里,「函数调用」实际上等价于在FunctionN实例上调用apply方法。例如,存在一个有名函数lower。
val lower: String =& String = _.toLowerCase
当发生如下函数调用时:
lower("HORANCE")
它等价于在Function1[String, String]类型的实例上调用apply方法。
lower.apply("HORANCE")
一般地,函数字面值具有如下形式:
(a1: A1, a2: A2, ..., an: An) =& E: R
其中,E是一个具有类型为R的表达式。它的类型为:
(A1, A2, ..., An) =& R
或者表示为:
FunctionN[A1, A2, ..., An, R]
其中,函数字面值等价于匿名函数对象的定义:
new FunctionN[A1, A2, ..., An, R] {
def apply(a1: A1, a2: A2, ..., an: An): R = E
其中,FunctionN类型定义为
trait FunctionN[-A1, -A2, ..., -An, +R] {
def apply(a1: A1, a2: A2, ..., an: An): R
接受函数作为参数,或返回函数的函数常常称为「高阶函数(Hign Order Function)」。高阶函数是组合式设计的基础,它极大地提高了代码的可复用性。
例如,starts, ends, contains是三个高阶函数,它们都返回String =& Boolean类型的谓词,用于判断某个字符串的特征。
type StringMatcher = String =& String =& Boolean
def starts: StringMatcher = prefix =&
_ startsWith prefix
def ends: StringMatcher = suffix =&
_ endsWith suffix
def contains: StringMatcher = substr =&
_ contains substr
同样地,高阶函数也可以接受函数类型作为参数。例如,如果要忽略大小写,并复用上面三个函数,可以定义ignoringCase的高阶函数,用于修饰既有的字符串匹配器。
def ignoringCase(matcher: StringMatcher): StringMatcher = substr =&
str =& matcher(substr.toLowerCase)(str.toLowerCase)
可以如下方式使用ignoringCase。
assertThat("Horance, Liu", ignoringCase(starts)("horance"))
其中,assertThat也是一个高阶函数。
def assertThat[A](actual: A, matcher: A =& Boolean) =
assert(matcher(actual))
部分应用函数
所谓「部分应用函数」(Partially Applied Function),即至少还有一个参数还未确定的函数。例如,times用于判断m是不是n的整数倍。
def times: Int =& Int =& Boolean = n =& m =& m % n == 0
对于常见的倍数关系,可以先确定参数n的值。例如:
= times(2)
val triple = times(3)
此时,twice, triple是一个Int =& Boolean的函数,可以被再次利用。
首先,将原来的times进行一下变换。
def times(n: Int)(m: Int): Boolean = m % n == 0
此时,如果采用如下的方式确定twice, triple,将发生错误。
= times(2)
val triple = times(3)
因为,times(2)的真实类型为方法(Method),且拥有(Int)Boolean的类型。它不是函数,且没有函数值。
可以通过在「方法」的后面手动地添加「一个空格和一个_」,将方法转换为一个「部分应用函数」,这种转换常称为「η扩展」。例如:
= times(2) _
val triple = times(3) _
事实上,times(2) _将生成一个匿名的函数对象。它等价于:
val twice = new Function1[Int, Boolean] {
def apply(m: Int): Boolwan = this.times(2)(m)
// 在this对象调用`times`方法
如果上下文已经标明函数的类型,编译器会自动实施η扩展。例如:
def atom(matcher: Int =& Boolean, action: Int =& String): Int =& String =
m =& if (matcher(m)) action(m) else ""
因为,atom的matcher参数已经标明了函数原型。因此,当传递times(3), to("Fizz")给atom时,编译器会自动实施η扩展,将times(3), to("Fizz")转变为一个部分应用函数。
val r_n1 = atom(times(3), to("Fizz"))
val r_n1 = atom(times(3) _, to("Fizz") _)
其中,to的实现为:
def to(s: String): Int =& String = _ =& s
JSONObject是一个JSON的一个子类,它持有bindings: Map[String, JSON]。toString迭代地处理Map中元组,并通过调用_1, _2方法分别获取当前迭代元组的关键字和值。
case class JSONObject(bindings: Map[String, JSON]) extends JSON {
override def toString: String = {
val contents = bindings map { t =&
"\\\\"" + t._1 + ":" + t._2.toString + "\\\\""
"{" + (contents mkString ",") + "}"
可以使用「模式匹配」直接获取Map中当前迭代的元组。
bindings map { {
case (key, value) =& "\\\\"" + key + ":" + value + "\\\\""
其中,map的大括号(或小括号)常常被省略。
bindings map {
case (key, value) =& "\\\\"" + key + ":" + value + "\\\\""
其中,{ case (key, value) =& ??? }表达式是一个整体,包括外面的大括号。此外,该表达式是有类型的,它的类型为PartialFunction[(String, JSON), String]。
val f: PartialFunction[(String, JSON), String] = {
case (key, value) =& "\\\\"" + key + ":" + value + "\\\\""
也就是说,{ case (key, value) =& ??? }是「偏函数」(Partial Function)的一个字面值。
偏函数的定义
一般地,对于函数A =& B,它对A类型的所有值都有意义;而PartialFunction[A, B]仅对A类型的部分值有意义。
也就是说,偏函数是一个残缺的函数,它只处理模式匹配成功的值,而对模式匹配失败的值抛出MatchError异常。例如,positive就是一个偏函数,它仅对大于0的整数有意义。
val positive: PartialFunction[Int, Int] = { case x if x & 0 =& x }
当传递负数或0值时,将在运行时抛出MatchError异常。
positive(1)
positive(-1)
// MatchError
剖析偏函数
偏函数是一个一元函数。
trait PartialFunction[-A, +B] extends (A =& B) {
def isDefinedAt(x: A): Boolean
def applyOrElse[A1 &: A, B1 &: B](x: A1)(default: A1 =& B1): B1 =
if (isDefinedAt(x)) apply(x) else default(x)
对于偏函数positive
val positive: PartialFunction[Int, Int] = { case x if x & 0 =& x }
为了方便理解,上述偏函数实现类似于如下的匿名函数定义:
val positive = new PartialFunction[Int, Int] {
def isDefinedAt(x: A): Boolean = x & 0
def apply(x: Int): Int = x
追溯MatchError
当调用positive(-1)时,将抛出MatchError异常。追本溯源,它实际调用的是AbstractPartialFunction的apply方法。
AbstractPartialFunction的存在,避免了偏函数的字面值产生大量的匿名类定义。
abstract class AbstractPartialFunction[-T, +R] extends PartialFunction[T, R] {
def apply(x: T): R = applyOrElse(x, PartialFunction.empty)
其中,empty定义在PartialFunction的伴生对象中。
object PartialFunction {
val empty = new PartialFunction[Any, Nothing] {
def isDefinedAt(x: Any) = false
def apply(x: Any) = throw new MatchError(x)
当调用positive(-1)时,AbstractPartialFunction的apply方法被调用,它委托给了PartialFunction的applyOrElse方法。
因为此时传入的是负数,isDefinedAt返回false,因此最终偏函数的返回值为PartialFunction.empty,而调用empty.apply将抛出MatchError异常。
如果对偏函数进行提升(lift),会将偏函数升级为一个普通的一元函数。例如,对于偏函数positive:
val positive: PartialFunction[Int, Int] = { case x if x & 0 =& x }
通过lift,可以将positive提升为Int =& Option[Int]的一元函数。
val positiveOption = positive.lift
当positiveOption传入的是正数x时,将返回Some(x),否则返回None。
如果对一元函数positiveOption调用Function.unlift,将使得该一元函数降级为一个偏函数。例如:
val positive = Function.unlift(positiveOption)
// positive: PartialFunction[Int, Int]
其中,unlift实现在单键对象Function中,它的函数原型为:
def unlift[T, R](f: T =& Option[R]): PartialFunction[T, R] = ???
在Scala中,可能存在多种风格类似的函数定义(广义的函数定义)。首先,可以以方法(Method)的形式存在。
def lower(s: String): String = s.toLowerCase
特殊地,虽然以方法的形式存在,但它返回了函数类型。因此,也常常成为方法。
def lower: String =& String = _.toLowerCase
def lower: Function1[String, String] = _.toLowerCase
另外,也可以使用val持有函数值。
val lower: String =& String = _.toLowerCase
val lower: Function1[String, String] = _.toLowerCase
val lower = (s: String) =& s.toLowerCase
假如存在一个map的柯里化的函数。
def map[A, B](a: A)(f: A =& B): B = f(a)
它接受上述6种风格的lower定义。
map("HORANCE")(lower)
当然,map也可以直接接受匿名函数(函数字面值)。
map("HORANCE") { _.toUpperCase }
虽然map接受多种不同的lower表示,但三种表示形式存在微妙的差异。
方法与函数
def lower(s: String): String = s.toLowerCase
def lower: String =& String = _.toLowerCase
从严格意义上讲,两者都是使用了def定义的「方法」(Method)。但是,后者返回了一个「函数类型」,并具有函数调用的语义。
因此按照习惯,前者常称为「方法」,而后者则常常称为「函数」;前者代表了面向对象的风格,后者代表了函数式的风格。
在Scala中,方法与函数是两个不同的概念,并存在微妙的差异,并代表了两种不同的编程范式。但幸运的是,它们的差异性对于99%的用户是无感知的。
方法不是函数
在Scala中,方法并不是函数。例如
def lower(s: String): String = s.toLowerCase
lower是一个方法(Method),而方法是没有值的,因此它不能赋予给变量。
val f = lower
// 错误,lower是一个方法,它没有值
可以对方法lower实施η扩展,将其转换为部分应用函数。例如:
val f = lower _
// f: String =& String = &function1&
事实上,lower _将生成一个匿名的函数对象。它等价于:
val f = new Function1[String, String] {
def apply(s: String): String = this.lower(s)
// 在this对象调用`lower`方法
特殊地,当上下文需要一个「函数类型」时,此时编译器可以自动完成「方法」的η扩展。例如,map的第二个参数刚好是一个函数类型。此时,可以直接将lower方法进行传递,编译器自动完成η扩展,将其转换为一个函数对象。
map("HORANCE")(lower)
// map期待一个函数类型,编译器自动完成`η`扩展
它等价于:
map("HORANCE")(lower _)
两阶段调用
def lower: String =& String = _.toLowerCase
事实上,此处的lower是一个使用def定义的方法。但是特殊地,它返回了函数类型。当发生如下调用时。
lower("HORANCE")
实际上,它做了两件事情:
在this对象上调用lower方法,并返回String =& String类型的函数对象;
在该函数对象上调用apply方法;
也就是说,上例函数调用等价于:
val f = this.lower
// f: String =& String = &function1&
f.apply("HORANCE")
事实上,lower("HORANCE")的调用具有特殊意义。其一,lower在this对象上调用刚好返回函数类型;其二,FunctionN上的apply是一个特殊的方法,其具有更贴切的函数调用语义。
按照习惯,即使lower是一个方法定义,但也被常常称为函数定义。
如下两种方式,定义了类似的有名函数,但两者之间存在微妙的差异。
val lower: String =& String = _.toLowerCase
def lower: String =& String = _.toLowerCase
使用val定义函数
前者使用val的变量直接持有函数值,多次使用lower将返回同一个函数值。也就是说,如下表达式求值为真。
lower eq lower
使用def定义函数
后者使用def定义函数,每次调用lower将得到不同的函数值。也就是说,如下表达式求值为假。
lower eq lower
「柯里化(Currying)」是函数式理论的一个重要概念。例如,之前的map就是一个柯里化的方法定义。
def map[A, B](a: A)(f: A =& B): B = f(a)
柯里化类型的自动推演,及其控制结构的抽象等应用场景扮演重要角色。
假如,map是一个普通的方法定义,如果没有进行柯里化。
def map[A, B](a: A, f: A =& B): B = f(a)
当直接传递匿名函数时,如果没有显式地标注参数类型,类型推演将发生错误。
map("HORANCE", s =& s.toLowerCase)
它必须显式地声明匿名函数的入参类型。
map("HORANCE", (s: String) =& s.toLowerCase)
如果对map进行柯里化,让其拥有两个柯里化的参数列表。
def map[A, B](a: A)(f: A =& B): B = f(a)
当传递匿名函数时,它便可以自动完成匿名函数参数的类型推演。
map("HORANCE", s =& s.toLowerCase)
借助于柯里化,可以实现控制结构的抽象。例如,可以设计一个指令式的until的函数,它将循环调用blok,直至条件cond为真为止(与while相反)。
@annotation.tailrec
def until(cond: =& Boolean)(block: =& Unit) {
if (!cond) {
until(cond)(block)
当调用until时,最后一个柯里化参数列表的调用可以使用大括号代替小括号。例如,
var n = 10
until (n == 0) {
事实上,它等价于
(0 to 10).sum
前者为指令式的代码风格,后者为函数式的指令风格。
一般地,具有多个参数列表,并且每个参数列表中有且仅有一个参数的「柯里化」常称为「完全柯里化」。假设存在一个完全柯里化的方法定义,它具有如下的形式:
def f(a1: A1)(a2: A2)...(an: An):R = E
其中,n & 1。它等价于:
def f(a1: A1)(a2: A2)...(an_1: An_1) = an =& E
递归这个过程,可以推出最终的等价形式:
def f = (a1 =& (a2 =& ...(an =& E)...))
虽然,后者也是一个使用def定义的方法,但它返回了一个「完全柯里化的函数类型」。按照惯例,后者也常称为「完全柯里化的函数」。
柯里化方法与函数
def concat(s1: String)(s2: String): String = s1 + s2
def concat: String =& String =& String = s1 =& s2 =& s1 + s2
按照上述形式化的描述,前者进行2次变换可将其转变为后者,两者功能等价。但是,它们两者之间是存在着微妙的差异,前者是一个方法,它没有值;而后者是一个函数,且拥有函数值。
柯里化方法
首先,对于柯里化的方法,它首先是一个方法(Method)。
def concat(s1: String)(s2: String): String = s1 + s2
因此,它没有值。如下语句将发生编译错误。
val f = concat
但是,可以应用η扩展将该柯里化的方法转变为柯里化的函数值。
val f = concat _
// f: String =& (String =& String) = &function1&
如果固定第一个参数的值,必须再实施η扩展,才能得到部分应用函数。
val f = concat("horance") _
// f: String =& String = &function1&
柯里化函数
而对于柯里化的函数,它是一个函数(Function)。
def concat: String =& String =& String = s1 =& s2 =& s1 + s2
无需η扩展,它便可以直接获取到函数值。
val f = concat
// f: String =& (String =& String) = &function1&
如果固定第一个参数的值,也不用实施η扩展,便直接可得到的部分应用函数。
val g = concat("horance")
// g: String =& String = &function1&
当然,如果存在一个高阶函数twice,它接受String =& String =& String的函数类型。
def twice[A](x: A)(f: A =& A =& A): A = f(x)(x)
对于柯里化的方法concat。
def concat(s1: String)(s2: String): String = s1 + s2
可以直接传递concat给twice,编译器自动完成η扩展。
twice("horance")(concat)
事实上,它等价于:
twice("horance")(concat _)
FunctionN.curried(N &= 2)
一般地,完全柯里化的函数具有如下的类型。
(A1 =& (A2 =& ...(An =& E)...))
可以通过调用FunctionN.curried方法,将一个「非完全柯里化的函数」转化为等价的「完全柯里化的函数」。例如,如果concat是一个普通的方法定义。
def concat(s1: String, s2: String): String = s1 + s2
首先,应用η扩展,将其转变为部分应用函数;此时等到的「部分应用函数」还不是一个完全被柯里化的函数。
val f = concat _
// f: (String, String) =& String = &function2&
可以在这个实例对象上通过调用curried将其转变为「完全柯里化」的等价形式。
val g = f.curried
// g: String =& (String =& String) = &function1&
其中,curried是一个高阶函数,它返回一个完全柯里化的函数。
trait Function2[-T1, -T2, +R] {
def apply(v1: T1, v2: T2): R
def curried: T1 =& T2 =& R =
x1 =& x2 =& apply(x1, x2)
Function.uncurried
可以对g应用Function.uncurried的逆向操作,将其转变为「非柯里化」的函数。
val f = Function.uncurried(g)
// f: (String, String) =& String = &function2&
其中,Function.uncurried是这样定义的。
object Function {
def uncurried[A1, A2, B](f: A1 =& A2 =& B): (A1, A2) =& B =
(x1, x2) =& f(x1)(x2)
FunctionN.tupled(N &= 2)
假设存在一个二元组。
val names = ("horance", "liu")
如果要调用如下的函数,完成字符串的连接。
def concat(s1: String, s2: String): String = s1 + s2
它需要依次取出元组中的元素并进行传递。
val fullName = concat(names._1, names._2)
// fullName: String = horanceliu
可以应用Function2.tupled改善设计。首先,应用η扩展将该方法变换为部分应用函数,然后再在该函数对象上调用tupled,并直接传递二元组。
val fullName = (concat _).tupled(names)
// fullName: String = horanceliu
其中,tupled是一个高阶函数,它返回(String, String) =& String类型的函数。
trait Function2[-T1, -T2, +R] {
def apply(v1: T1, v2: T2): R
def tupled: Tuple2[T1, T2] =& R = {
case Tuple2(x1, x2) =& apply(x1, x2)
Function.untupled
可以对g应用Function.untupled的逆向操作,将接受元组类型参数的函数转变为接受参数列表的函数。
val g = (concat _).tupled
// g: ((String, String)) =& String = &function1&
val f = Function.untupled(g)
// f: (String, String) =& String = &function2&
其中,Function.untupled是这样定义的。
object Function {
def untupled[A1, A2, B](f: (A1, A2) =& B): (A1, A2) =& B =
(x1, x2) =& f((x1, x2))
实战ScalaHamcrest
对于任意的类型A,A =& Boolean常常称为「谓词」;如果该谓词用于匹配类型A的某个值,也常常称该谓词为「匹配器」。
ScalaHamcrest是一个使用函数式设计思维实现的Matcher集合库。通过实战ScalaHamcrest,以便加深理解Scala函数的理论基础。
原子匹配器
可以定义特定的原子匹配器。例如:
def always: Any =& Boolean = _ =& true
def never:
Any =& Boolean = _ =& false
也可以定义equalTo的原子匹配器,用于比较对象间的相等性。
def equalTo[T](expected: T): T =& Boolean =
expected == _
与equalTo类似,可以定义原子匹配器same,用于比较对象间的一致性。
def same[T &: AnyRef](t: T): T =& Boolean = t eq _
其中,T &: AnyRef类型对T进行界定,排除AnyVal的子类误操作same。类似于类型上界,也可以使用其他的类型界定形式;例如,可以定义instanceOf,对类型T进行上下文界定,用于匹配某个实例的类型。
def instanceOf[T : ClassTag]: Any =& Boolean = x =&
case _: T =& true
有时候,基于既有的原子可以很方便地构造出新的原子。
= equalTo[AnyRef](null)
def empty = equalTo("")
组合匹配器
也可以将各个原子或者组合器进行组装,形成威力更为强大的组合器。
def allOf[T](matchers: (T =& Boolean)*): T =& Boolean =
actual =& matchers.forall(_(actual))
def anyOf[T](matchers: (T =& Boolean)*): T =& Boolean =
actual =& matchers.exists(_(actual))
特殊地,基于anyof,可以构造很多特定的匹配器。
def blank: String =& Boolean = """\\\\s*""".r.pattern.matcher(_).matches
def emptyOrNil = anyOf(nil, equalTo(""))
def blankOrNil = anyOf(nil, blank)
修饰匹配器
修饰也是一种特殊的组合行为,用于完成既有功能的增强和补充。
def not[T](matcher: T =& Boolean): T =& Boolean =
!matcher(_)
def is[T](matcher: T =& Boolean): T =& Boolean =
其中,not, is是两个普遍的修饰器,可以修饰任意的匹配器。此外,可以通过定义语法糖,提升用户感受。例如,可以使用not替换not(equalTo),is替代is(equalTo),不仅减轻用户的负担,而且还能提高表达力。
def not[T](expected: T): T =& Boolean = not(equalTo(expected))
def is[T](expected: T):
T =& Boolean = is(equalTo(expected))
至此,还不知道ScalaHamcrest如何使用呢?可以定义一个实用方法assertThat。
def assertThat[A](actual: A, matcher: A =& Boolean) =
assert(matcher(actual))
其中,assert定义于Predef之中。例如存在如下一个测试用例。
assertThat(2, allOf(always, instanceOf[Int], is(2), equalTo(2)))
遗留一个很有意思的问题,能否使用&&直接连接多个匹配器形成调用链,替代allOf匹配器呢?
assertThat("horance", equalTo("horance") && ignoringCase(equalTo)("HORANCE"))
可以定义个一个implicit Matcher,并添加了&&, ||, !的基本操作,用于模拟谓词的基本功能。
implicit class Matcher[-A](pred: A =& Boolean) extends (A =& Boolean) {
def &&[A1 &: A](that: A1 =& Boolean): A1 =& Boolean =
x =& self(x) && that(x)
def ||[A1 &: A](that: A1 =& Boolean): A1 =& Boolean =
x =& self(x) || that(x)
def unary_![A1 &: A]: A1 =& Boolean =
def apply(x: A): Boolean = pred(x)
本文重点叙述了Scala函数式编程的基础,重点讨论了几个容易混淆的概念。包括「方法」与「函数」,「偏函数」与「部分应用函数」,「柯里化」,「高阶函数」等,并通过实战ScalaHamcrest,深入理解Scala函数的基本概念。
接下来,将重点讨论函数式编程的应用。
Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, Waldemar Celes 云风译www.codingnow.com Copyright(C) 2006 Lua.org, PUC-Rio....
函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且可以在任何地方、任何时候调用执行。在javascript中,函数即对象,程序可以随意操控它们。函数可以嵌套在其他函数中定义,这样他们就可以访问它们被定义时所处的...
原文链接:https://github.com/EasyKotlin 值就是函数,函数就是值。所有函数都消费函数,所有函数都生产函数。 &函数式编程&, 又称泛函编程, 是一种&编程范式&(programming paradigm),也就是如何编写程序的方法论。它的基础是 ...
前言 把《C++ Primer》读薄系列笔记全集。 目录 第I部分:C++基础 开始学习C++ 变量和基本类型 字符串、向量和数组 表达式 语句 函数 类 第II部分:C++标准库 IO库 顺序容器 范型算法 关联容器 动态内存 第III部分:类设计者的工具 拷贝控制 重载...
第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型和基本包装类型 用类型的值(对象)是引用类型的-个实例。在ECMAScript中,引用类型是--种数据结构, 1 I用于将数据和功能组织在一起。它也常被称为类,...
作为一个全球化的联赛,我们都有幸在NBA赛季中见证一些史诗级扣篮和关键投篮,但超级巨星或许才是NBA全球推广的最大优势。 像詹姆斯和库里在美国本土确实是家喻户晓的球员,但真正做到将篮球带到中国的球员并不是这些所谓的美国本土超级巨星。 2002年,火箭以状元签摘下拥有2.26...
家里的孩子不喜欢吃饭,不喜欢吃水果,不喜欢吃蔬菜,只喜欢喝饮料,吃小零食……宝妈宝爸困惑吗?来找我,我为宝妈宝爸解烦忧! 双色饺子,胡萝卜+青椒汁和面!馅是胡萝卜+青椒渣+木耳!是不是很有食欲! 绿色的肉夹馍,是不是很想吃!那么自己动手做吧!很健康哟! 大家吃鸡翅都要怎么吃...
以创办博客平台与微博客平台(推特)而掀起了两次新媒体革命的埃文o威廉姆斯刚刚(周三,22日)说:【媒体机器坏了,亲,我们一起来修理。请快交每月5美元成为 Medium 创始会员。注意,现在只有符合特定条件的人才有资格获邀请交钱成为会员。】 这不是恶搞的玩笑,这是埃文o威廉姆...
图片发自简书App 付出的收获总会珍惜索取的利益奢侈鄙弃 没有价值敷衍了事没有意义容易忘记 昂贵代价付出精力意义重大铭记在心 放弃忘记的憧憬铭记的 活得太累那是你发现了索取索取的最容易忘记所以越容易就越累 观世间人来人往走得快来得也快进去了出来了进进出出让人眼花缭乱 图片发...
寂静的深夜,黑暗的屋子里,当你坐在电脑前观看恐怖电影时,突然有一只手拍了一下你的肩膀,顿时心里一颤,壮着胆子回头一看,阴森的白骨,裸露的牙齿,漆黑的眼眶,都在挑战心脏的极限。 这样的场景是我们记忆中骷髅的出场方式,But换成五彩斑斓的背景,有些灵动的眼珠,穿着时髦,身体零部...}

我要回帖

更多关于 scala函数式编程 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信