Clojure首先是FP, 但是由于基于JVM, 所以不得已需要做出一些妥协, 包含一些OO的编程方式
Scala首先是OO, Java语法过于冗余, 一种比较平庸的语言, Scala首先做的是简化, 以更为简洁的方式来编写OO, 主要利用‘type inference’能推断出来的, 你就不用写, 但如果仅仅这样, 不如用python
所以Scala象其名字一样, “可伸展的语言”, 它是个大的集市, 它积极吸纳其他语言的优秀的特征, 最重要的就是FP, 你可以使用Scala来写OO, 但它推荐使用FP的方式来写Scala; 还包括Erlang里面的actor模型
所以Scala并不容易学, 因为比较繁杂
我主要对Scala programing做个笔记, 下面这个哥们整理的更好
http://qiujj.com/static/Scala-Handbook.htm
http://docs.scala-lang.org/ , Scala官方文档
http://www.scala-lang.org/api/current/#package, 参考手册
http://twitter.github.io/effectivescala/index-cn.html, Effective Scala
0
Scala interpreter
scala> 1 + 2
res0: Int = 3
scala> res0 * 3
res1: Int = 9
scala> println("Hello, world!")
Hello, world!
代码段落
scala中;常常可以省略, 但如果一行有多条语句, 则必须加上
val s = "hello"; println(s)
如果一条语句, 要用多行写
x
+ y
这样会当作2个语句, 两种方法解决,
(x
+ y) //加括号x +
y +
z //把操作符写在前一行, 暗示这句没完
1 基本语法
基本类型
Rich wrappers, 为基本类型提供更多的操作
变量定义
val, 不可变变量, 常量, 适用于FP
var, 可变变量, 适用于OO
scala> val msg = "Hello, world!" msg: java.lang.String = Hello, world! scala> msg = "Goodbye cruel world!" <console>:5: error: reassignment to val msg = "Goodbye cruel world!"
函数定义
可以简写为, 返回值类型不需要写, 可以推断出, 只有一条语句, 所以{}可以省略
scala> def max2(x: Int, y: Int) = if (x > y) x else y max2: (Int,Int)Int
简单的funciton, 返回值为Unit, 类似Void(区别在于void为无返回值, 而scala都有返回值, 只是返回的为Unit, ())
scala> def greet() = println("Hello, world!")
greet: ()Unit
scala> greet() == ()
Boolean = true
函数参数不可变
def add(b: Byte): Unit = {
b = 1 // This won’t compile, because b is a val
sum += b
}
重复参数, 最后的*表示可变参数列表, 可传入多个string
scala> def echo(args: String*) = for (arg <- args) println(arg)
scala> echo("hello", "world!")
hello
world!
Function literal
如何翻译...
Scala FP的基础, function作为first class, 以function literal的形式作为参数被传递
args.foreach((arg: String) => println(arg))
args.foreach(arg => println(arg)) //省略类型
args.foreach(println) //其实连参赛列表也可以省略
可以看到scala在省略代码量上可以说下足功夫, 只要能推断出来的你都可以不写, 这也是对于静态类型系统的一种形式的弥补
对于oo程序员, 可能比较难理解, 其实等于
for (arg <args)
println(arg)
控制结构
由于scala是偏向于FP的, 所以所有控制结构都有返回值, 这样便于FP编程
If, 可以返回值
val filename = if (!args.isEmpty) args(0) else "default.txt"
While, 在FP里面不推荐使用循环, 应该用递归,尽量避免
For, 没有python和clojure的好用或简洁
for ( file <- filesHere //generator,用于遍历,每次file都会被从新初始化 if file.isFile; //过滤条件, 多个间需要用; if file.getName.endsWith(".scala"); //第二个过滤 line <- fileLines(file) //嵌套for trimmed = line.trim //Mid-stream variable bindings, val类型,类似clojure let if trimmed.matches(pattern) ) println(file +": "+ trimmed)
//for默认不会产生新的集合, 必须使用yield def scalaFiles = for { file <- filesHere if file.getName.endsWith(".scala") } yield file //yield产生新的集合,类似python
match, switch-case
可以返回值, FP风格, 这样只需要最后println一次
默认会break, 不需要每次自己加
val firstArg = if (!args.isEmpty) args(0) else "" val friend = firstArg match { case "salt" => "pepper" case "chips" => "salsa" case "eggs" => "bacon" case _ => "huh?" //default } println(friend)
Option and Either
参考, http://www.ibm.com/developerworks/cn/java/j-ft13/index.html
Option和Either都是用来让返回值可以有两个选择
而Option是比较简单的版本, 两个选择, 一定是成功Some, 和失败None
Option意味着可能有值some(x), 也可能没有值(用None对象, 表示缺失), 典型的例子就是从字典里取值
val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo") def show(x: Option[String]) = x match { //Option类型, 可选的String case Some(s) => s case None => "?" } scala> show(capitals get "France") res24: String = Paris scala> show(capitals get "North Pole") res25: String = ?
以前的方式, 比如Java, 通过null来表示没有取到值, 但是有的时候null可能作为合法值出现, 就需要特殊处理, 很麻烦
而Scala提供option来比较优雅的解决这个问题
Either, 更为通用一些, 可用自己定义两种选择, 直接看个spark源码中的例子,
对于PutResult中的data, 有可能是ByteBuffer或者Iterator
而使用的时候, 使用Left和Right来选择到底用哪一个
private[spark] case class PutResult(size: Long, data: Either[Iterator[_], ByteBuffer])
PutResult(sizeEstimate, Left(values.iterator)) PutResult(bytes.limit(), Right(bytes.duplicate()))
这里无论option或either都提高了极好的灵活性, 在Java中如果要返回一个有两种可能性的值就比较不那么优雅了, 参考上面的链接
Implicit Conversions, 隐式转换
Scala支持这种隐式转换,
implicit def intToString(x: Int) = x.toString //定义转换函数后, 编译器会自动做转换
比如x op y无法通过类型检查, int x 不支持op操作, 而string支持, 编译器就会自动调用上面的转换函数, 将int x转化为string x
隐式参数
用法也比较诡异, 看例子
object Greeter { def greet(name: String)(implicit prompt: PreferredPrompt) {//声明2个参数, 第二个是可以隐式的,当然你也可以显式的写 println("Welcome, "+ name +". The system is ready.") println(prompt.preference) } } object JoesPrefs { implicit val prompt = new PreferredPrompt("Yes, master> ") //声明成implicit, 可用作补充 } import JoesPrefs._ scala> Greeter.greet("Joe") //编译器会自动补充成, greet("Joe")(prompt) Welcome, Joe. The system is ready. Yes, master>
2 数据结构
数组
可变的同类对象序列, 适用于OO场景
val greetStrings = new Array[String](3) //greetStrings为val, 但是内部的数组值是可变的
greetStrings(0) = "Hello" //scala用()而非[]
greetStrings(1) = ", "
greetStrings(2) = "world! "
for (i <- 0 to 2)
print(greetStrings(i))
Scala 操作符等价于方法, 所以任意方法都可以以操作符的形式使用
1 + 2 //(1).+(2), 在只有一个参数的情况下, 可以省略.和()
0 to 2 //(0).to(2)
greetStrings(0) //greetStrings.apply(0),这也是为什么scala使用(), 而非[]
greetStrings(0) = "Hello" //greetStrings.update(0, "Hello")
简化的array初始化
val numNames = Array("zero", "one", "two") //Array.apply("zero", "one", "two")
List
相对于array, List为不可变对象序列, 适用于FP场景
val oneTwo = List(1, 2)
val threeFour = List(3, 4)val zeroOneTwo = 0 :: oneTwo //::
val oneTwoThreeFour = oneTwo ::: threeFour
对于List最常用的操作符为::, cons, 把新的elem放到list最前端
:::, 两个list的合并
右操作数, ::
普通情况下, 都是左操作数, 比如, a * b => a.*(b)
但是当方法名为:结尾时, 为右操作数
1 :: twoThree => twoThree.::(1)
不支持append
原因是, 这个操作的耗时会随着list的长度变长而线性增长, 所以不支持, 只支持前端cons, 实在需要append可以考虑ListBuffer
支持模式
scala> val List(a, b, c) = fruit a: String = apples b: String = oranges c: String = pears scala> val a :: b :: rest = fruit a: String = apples b: String = oranges rest: List[String] = List(pears)
list zip,啮合
scala> abcde.indices zip abcde //indices返回所有有效索引值 res14: List[(Int, Char)] = List((0,a), (1,b), (2,c), (3,d),(4,e)) scala> val zipped = abcde zip List(1, 2, 3) zipped: List[(Char, Int)] = List((a,1), (b,2), (c,3)) //会自动截断
Folding lists: /: and :
折叠操作, 比较奇怪的操作, 看例子
(z /: List(a, b, c)) (op) equals op(op(op(z, a), b), c) scala> def sum(xs: List[Int]): Int = (0 /: xs) (_ + _) //实现列表元素叠加 (List(a, b, c) : z) (op) equals op(a, op(b, op(c, z))) def flattenLeft[T](xss: List[List[T]]) = (List[T]() /: xss) (_ ::: _) //两种的区别 def flattenRight[T](xss: List[List[T]]) = (xss : List[T]()) (_ ::: _)
Queues
import scala.collection.immutable.Queue //不可变Queue
val empty = new Queue[Int] val has1 = empty.enqueue(1) //添加单个元素 val has123 = has1.enqueue(List(2, 3)) //添加多个元素 val (element, has23) = has123.dequeue //取出头元素,返回两个值, 头元素和剩下的queue element: Int = 1 has23: scala.collection.immutable.Queue[Int] = Queue(2,3)
import scala.collection.mutable.Queue //可变Queue val queue = new Queue[String] queue += "a" //添加单个 queue ++= List("b", "c") //添加多个 queue.dequeue //取出头元素, 只返回一个值 res22: String = a scala> queue res23: scala.collection.mutable.Queue[String] = Queue(b, c)
Stacks
import scala.collection.mutable.Stack val stack = new Stack[Int] stack.push(1) stack.push(2) scala> stack.top res8: Int = 2 scala> stack.pop res10: Int = 2 scala> stack res11: scala.collection.mutable.Stack[Int] = Stack(1)
Tuple
tuple和list一样是不可变的, 不同是, list中的elem必须是同一种类型, 但tuple中可以包含不同类型的elem
val pair = (99, "Luftballons") //自动推断出类型为,Tuple2[Int, String] println(pair._1) //从1开始,而不是0,依照Haskell and ML的传统 println(pair._2) //elem访问方式不同于list, 由于元组中elem类型不同
Set和Map
var jetSet = Set("Boeing", "Airbus")
jetSet += "Lear"
println(jetSet.contains("Cessna"))
val treasureMap = Map[Int, String]() treasureMap += (1 -> "Go to island.") treasureMap += (2 -> "Find big X on ground.") println(treasureMap(2))
val romanNumeral = Map(1 -> "I", 2 -> "II", 3 -> "III" ) //简写
Scala需要兼顾OO和FP, 所以需要提供mutable和immutable版本
这里默认是Immutable, 如果需要使用mutable版本, 需要在使用前显示的引用...
import scala.collection.mutable.Set
import scala.collection.mutable.Map
Sorted Set and Map
和Java一样, Scala也提供基于红黑树实现的有序的Set和Map
import scala.collection.immutable.TreeSet scala> val ts = TreeSet(9, 3, 1, 8, 0, 2, 7, 4, 6, 5) ts: scala.collection.immutable.SortedSet[Int] = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) scala> val cs = TreeSet('f', 'u', 'n') cs: scala.collection.immutable.SortedSet[Char] = Set(f, n, u) import scala.collection.immutable.TreeMap scala> var tm = TreeMap(3 ->'x', 1 ->'x', 4 ->'x') scala> tm += (2 ->'x') scala> tm res38: scala.collection.immutable.SortedMap[Int,Char] = Map(1 ->x, 2 ->x, 3 ->x, 4 ->x)
3 面向对象-OO
类和对象
相对于Java定义比较简单, 默认public
class ChecksumAccumulator {
private var sum = 0
def add(b: Byte): Unit = {
sum += b
}def checksum(): Int = {
return ~(sum & 0xFF) + 1
}
}
不写return是推荐的方式, 因为函数尽量不要有多个出口
class ChecksumAccumulator {
private var sum = 0
def add(b: Byte): Unit = sum += b
def checksum(): Int = ~(sum & 0xFF) + 1
}
class ChecksumAccumulator {
private var sum = 0
def add(b: Byte) { sum += b } //对于Unit返回的, 另一种简写, 用{}来表示无返回, 所以前面的就不用写了
def checksum(): Int = ~(sum & 0xFF) + 1
}
实例化
val acc = new ChecksumAccumulator
val csa = new ChecksumAccumulator
acc.sum = 3
Singleton对象
Scala不能定义静态成员, 所以用Singleton对象来达到同样的目的
import scala.collection.mutable.Map object ChecksumAccumulator { //用object代替class private val cache = Map[String, Int]() def calculate(s: String): Int = if (cache.contains(s)) cache(s) else { val acc = new ChecksumAccumulator for (c <s) acc.add(c.toByte) val cs = acc.checksum() cache += (s -> cs) cs } }
最常见的场景就是, 作为scala程序的入口,
To run a Scala program, you must supply the name of a standalone singleton object with a main method that takes one parameter(Array[String]), and has a result type of Unit.
import ChecksumAccumulator.calculate object Summer { def main(args: Array[String]) { for (arg <args) println(arg +": "+ calculate(arg)) } }
不可变对象
适用于FP场景的对象, 所以也叫做Functional Objects.
好处, 消除可变带来的复杂性, 可以放心的当参数传递, 多线程下使用啊...
下面以定义有理数类为例
class Rational(n: Int, d: Int) //极简方式,没有类主体
和上面的定义比, 不需定义成员变量, 而只是通过参数, 因为根本没有定义成员变量, 所以无处可变.
class Rational(n: Int, d: Int) { require(d != 0) //Precondition, 如果require返回false会抛出IllegalArgumentException,阻止初始化 private val g = gcd(n.abs, d.abs) val numer = n / g //添加不可变成员字段,便于引用 val denom = d / g def this(n: Int) = this(n, 1) //辅助构造函数 def + (that: Rational): Rational = //定义操作符 new Rational( numer * that.denom + that.numer * denom, denom * that.denom //也可以使用this.number引用成员 ) def + (i: Int): Rational = //典型的成员函数重载 new Rational(numer + i * denom, denom) override def toString = numer +"/"+ denom //override, 方法重载 private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) }
可变对象
可变Objects本身没啥好说的, 说说Scala getter和setter规则
every var that is a non-private member of some object implicitly defines a getter and a setter method with it.
The getter of a var x is just named “x”, while its setter is named “x_=”.
每个非私有的var都会隐含的自动定义getter和setter, 如下面的例子
class Time { var hour = 12 var minute = 0 } //等同于 class Time { private[this] var h = 12 private[this] var m = 0 def hour: Int = h def hour_=(x: Int) { h = x } def minute: Int = m def minute_=(x: Int) { m = x } }
所以在Scala中比较有趣的是, 其实你可以不真正定义这个成员, 而只需要定义getter和setter就可以
如下面的例子, 并没有真正定义华氏温度, 而只是定义了getter和setter, 更简洁
class Thermometer { var celsius: Float = _ def fahrenheit = celsius * 9 / 5 + 32 def fahrenheit_= (f: Float) { celsius = (f 32) * 5 / 9 } override def toString = fahrenheit +"F/"+ celsius +"C" }
组合和继承
抽象类
abstract class Element { //abstract表示抽象类 def contents: Array[String] //没有函数体表示声明抽象方法 }
无参函数和属性, 统一访问模式
客户可以用同样的方式来访问函数width或属性width, 这样的好处是width改变不同的实现方式时, 不会影响客户代码, 实现统一访问模式
函数和属性的区别, 函数节省空间而需要每次计算, 属性是初始化时算好的, 比较快, 但需要额外的空间, 所以可用在不同场景下选择不同实现
abstract class Element { def contents: Array[String] //组合Array[] def height: Int = contents.length //无参函数 def Int = if (height == 0) 0 else contents(0).length } //等同于 abstract class Element { def contents: Array[String] val height = contents.length //属性 val width = if (height == 0) 0 else contents(0).length }
Scala为了实现这种统一的访问模式, 一般对于无参函数在调用时可以省掉(), 但前提是该无参函数是没有副作用的, 否则就会比较confusing
"hello".length // no () because no sideeffect println() // better to not drop the (),因为有副作用
继承
超类的私有成员不会被子类继承.
抽象成员, 需要被子类实现(implement)
一般成员, 可用被子类重写(override)
override关键字的使用,
重写时, 必须显式加override, 防止意外的重写
实现时, 是否写override可选
其他情况禁止使用
class ArrayElement(conts: Array[String]) extends Element { def contents: Array[String] = conts //实现抽象函数 }
这个图, 反映出继承和组合, 其中Element继承自AnyRef, 而ArrayElement组合Array[]
重写方法和属性
这是scala比较特别的地方, 方法和属性在同一个命名空间, 所以方法和属性不能同名
并且支持, 在override时, 方法和属性间可用互相转换
前面将contents声明成方法, 是否觉得有些别扭, 看上去更像属性, 是的, 你可用在子类中实现或重写成属性
在scala中, 无副作用的无参函数和属性是可以简单的互相转换
class ArrayElement(conts: Array[String]) extends Element { val contents: Array[String] = conts //将方法contents实现成属性 }
Java和Scala的命名空间对比
Java’s four namespaces are fields, methods, types, and packages.
By contrast, Scala’s two namespaces are:
• values (fields, methods, packages, and singleton objects)
• types (class and trait names)
参数化属性
一种scala的简化,
是否觉得, 定义参数, 再将参数赋值给属性, 很麻烦, ok, scala可用简化
class ArrayElement( val contents: Array[String] ) extends Element //实现属性
另一个例子,
class Cat { val dangerous = false } class Tiger( override val dangerous: Boolean, //重写属性 private var age: Int //增加新的属性 ) extends Cat
超类构造器
和C++或Pythno一样, 需要在子类中显式的构造超类, 比如这里需要先构造ArrayElement
只不过Scala提供一种简写的方式, 而不用在代码中另写一行
class LineElement(s: String) extends ArrayElement(Array(s)) { override def width = s.length override def height = 1 }
final成员
class ArrayElement extends Element { final override def demo() { //该成员函数禁止被任何子类override println("ArrayElement's implementation invoked") } } final class ArrayElement extends Element {//ArrayElement禁止被任何子类继承 override def demo() { println("ArrayElement's implementation invoked") } }
工厂对象实现
object Element { //用伴生对象(Singleton)来做工厂对象 def elem(contents: Array[String]): Element = new ArrayElement(contents) def elem(chr: Char, Int, height: Int): Element = new UniformElement(chr, width, height) def elem(line: String): Element = new LineElement(line) }
Scala的类层次
At the top of the hierarchy is class Any.
Any声明如下接口, 故所有类都支持这些接口,
final def ==(that: Any): Boolean final def !=(that: Any): Boolean def equals(that: Any): Boolean def hashCode: Int def toString: String
Any有两个子类, AnyVal和AnyRef
AnyVal是所有built-in value class的父类, 除了常见的基本类型外, 还有Unit
AnyRef是所有reference classes的父类, 其实就是java.lang.Object
Scala还定义, Nothing和Null class
Nothing是所有Any的子类, 而Null是所有AnyRef的子类
干吗用的?用于以统一的方式来处理边界形式或异常返回, 因为他们是所有类的子类, 所有可用返回给任意返回类型
例子,
def error(message: String): Nothing = throw new RuntimeException(message) def divide(x: Int, y: Int): Int = //返回类型为Int if (y != 0) x / y else error("can't divide by zero") //异常时返回Nothing
Package管理
支持package嵌套和通过{}指定范围
package launch { class Booster3 } package bobsrockets { package navigation { class Navigator } package launch { class Booster { // No need to say bobsrockets.navigation.Navigator val nav = new navigation.Navigator val booster3 = new _root_.launch.Booster3 //_root_特指顶层包 } } }
Import的用法
import bobsdelights.Fruit import bobsdelights._ //import all import Fruits.{Apple, Orange} //指定import import Fruits.{Apple => McIntosh, Orange} //别名 import Fruits.{Apple => _, _} //排除Apple
访问修饰符
Private
基本和Java一致,但是在对inner class上表现出更好的一致性
Java允许外部类访问内部类的private成员,而scala不行,如下例子中的error case
class Outer { class Inner { private def f() { println("f") } class InnerMost { f() // OK } } (new Inner).f() // error: f is not accessible }
Protected
同样比Java更严格一些,仅允许在子类中被访问
而Java允许同一个包中的其他类访问该类的protected成员,而scala中不行,如下面的error case
package p { class Super { protected def f() { println("f") } } class Sub extends Super { f() } class Other { (new Super).f() // error: f is not accessible } }
修饰符作用域
scala中的修饰符可以加上作用域,非常灵活
private[X] or protected[X] means that access is private or protected “up to” X, where X designates some enclosing package, class or singleton object.
加上作用域的意思,除了原来private和protect表示的可见范围外,将可见范围扩展到X,X可以是package,class,singleton对象
package bobsrockets { package navigation { private[bobsrockets] class Navigator { //使Navigator类在bobsrockets package中可见,所以在package launch中可以new Navigator protected[navigation] def useStarChart() {} //在Navigator和子类,以及package Navigation中可以被访问(等同于Java) class LegOfJourney { private[Navigator] val distance = 100 //是私有变量distance,在外部类Navigtor中可见(等同于Java) } private[this] var speed = 200 //仅仅在相同对象中可以访问,比private的相同类访问还严格,如下例 val other = new Navigator other.speed // this line would not compile } } package launch { import navigation._ object Vehicle { private[launch] val guide = new Navigator } } }
伴生对象
A class shares all its access rights with its companion object and vice versa.
Trait
概念上类似Java的interface, 但是更强大, 可以有方法实现, 属性...
其实几乎等同于class, 除了两点,
a. trait不能有任何参数
b. trait中super是动态绑定的, 因为定义的时候根本就不知道super是谁
trait Philosophical {
def philosophize() {
println("I consume memory, therefore I am!")
}
}
mix in有两种方式, extend和with
extend, implicitly inherit the trait’s superclass
所以下面的例子, Frog会继承Philosophical的超类AnyRef
class Frog extends Philosophical { //mix in Philosophical trait override def toString = "green" }
with, 需要显式的指明超类
class Animal trait HasLegs class Frog extends Animal with Philosophical with HasLegs {//继承Animal,并mix in两个traits override def toString = "green" }
Trait真正的作用在于, 模块封装, 可以把相对独立的功能封装在trait中, 并在需要的时候进行mix in, 如下面的例子
//传统的例子 class Rational(n: Int, d: Int) { // ... def < (that: Rational) = this.numer * that.denom > that.numer * this.denom def > (that: Rational) = that < this def <= (that: Rational) = (this < that) || (this == that) def >= (that: Rational) = (this > that) || (this == that) } //将比较接口和实现封装在Ordered Trait中,这里仅仅需要mix in和实现compare class Rational(n: Int, d: Int) extends Ordered[Rational] { // ... def compare(that: Rational) = (this.numer * that.denom) ( that.numer * this.denom) }
Traits as stackable modifications
trait的强大还体现在可以同时mix in多个traits, 并以类似stackable形式的线性调用, 即多个trait会效果叠加
而传统的多重继承, 只有一个实现会有效
abstract class IntQueue { def get(): Int def put(x: Int) } import scala.collection.mutable.ArrayBuffer class BasicIntQueue extends IntQueue { private val buf = new ArrayBuffer[Int] def get() = buf.remove(0) def put(x: Int) { buf += x } } trait Doubling extends IntQueue { //trait继承自IntQueue, 说明该trait只能mix in IntQueue(或子类)中 //注意这里super的使用(动态绑定), 和abstract override(仅用于这种场景) //表示trait must be mixed into some class that has a concrete definition of the method in question abstract override def put(x: Int) { super.put(2 * x) } } trait Incrementing extends IntQueue { abstract override def put(x: Int) { super.put(x + 1) } } trait Filtering extends IntQueue { abstract override def put(x: Int) { if (x >= 0) super.put(x) } }
下面来看看, 怎么样进行stackable的调用
//同时mix in了Incrementing和Filtering //注意调用顺序是, 从右到左(stackable) //这个例子, 先调用Filtering, 再调用Incrementing, 顺序不同会带来不同的结果 scala> val queue = (new BasicIntQueue with Incrementing with Filtering) queue: BasicIntQueue with Incrementing with Filtering... scala> queue.put(-1); queue.put(0); queue.put(1) scala> queue.get() res15: Int = 1 scala> queue.get() res16: Int = 2
To trait, or not to trait
If the behavior will not be reused, then make it a concrete class.
If it might be reused in multiple, unrelated classes, make it a trait.
If you want to inherit from it in Java code, use an abstract class.
If you plan to distribute it in compiled form, and you expect outside groups to write classes inheriting from it, you might lean towards using an abstract class.
If efficiency is very important, lean towards using a class.
4 函数编程-FP
函数和闭包
成员函数, OO的方式
内部函数, 需要切分功能, 又不想污染外部的命名空间
First-class function, unnamed function literal
function象变量一样, 可以被赋值和当参数传递, 但在scala需要以function literal的形式, 在运行期的时候会实例化为函数值(function value)
scala> var increase = (x: Int) => x + 1 scala> increase(10)
Partially applied functions
scala> def sum(a: Int, b: Int, c: Int) = a + b + c scala> val a = sum _ //用占位符代替整个参数列表 scala> a(1, 2, 3) //a.apply(1, 2, 3) res13: Int = 6 scala> val b = sum(1, _: Int, 3) //partial function, 用占位符代替一个参数 scala> b(2) res15: Int = 6
Closures
关于闭包的解释,
对于通常的function, (x: Int) => x + 1, 称为closed term
而对于(x: Int) => x + more, 称为open term
所以对于开放的, 必须在定义的时候对里面的自由变量more动态进行绑定, 所以上下文中必须要有对more的定义, 这种关闭open term过程产生了closure
scala> var more = 1 scala> val addMore = (x: Int) => x + more //产生闭包,绑定more scala> addMore(10) res19: Int = 11 scala> more = 9999 scala> addMore(10) res21: Int = 10009 //可见闭包绑定的不是value,而是变量本身
刚看到有些惊讶, 去clojure里面试一下, 也是这样的, 绑定的变量本身, 闭包会取最新的值
当然一般不会这样使用闭包.
下面这个例子, 是较常用的case, 其中闭合了函数的参数
如何在闭包调用时, 可以访问到已经不存在的变量? 当产生闭包时, 编译器会将这个变量从堆栈放到堆里面, 所以函数结束后还能访问
def makeIncreaser(more: Int) = (x: Int) => x + more
scala> val inc1 = makeIncreaser(1)
scala> val inc9999 = makeIncreaser(9999)
scala> inc1(10)
res24: Int = 11
scala> inc9999(10)
res25: Int = 10009
Currying
先提供sum的两个版本的比较,
scala> def plainOldSum(x: Int, y: Int) = x + y
scala> plainOldSum(1, 2)
res4: Int = 3
scala> def curriedSum(x: Int)(y: Int) = x + y //currying版本的sum
curriedSum: (Int)(Int)Int
scala> curriedSum(1)(2)
res5: Int = 3
其实currying, 等同于调用两次function, first会返回第二个函数的函数值, 其中closure了x
scala> def first(x: Int) = (y: Int) => x + y first: (Int)(Int) => Int
取出函数值, 效果是减少了参数个数, 第一个函数的参数已经closure在第二个函数中了, 和partially有些类似(区别)
scala> val onePlus = curriedSum(1)_ onePlus: (Int) => Int = <function> scala> onePlus(2) res7: Int = 3
有什么用? 用于创建更像built-in的控制结构
如下, 使用{}更像built-in, 但{}有个限制是, 只有单个参数的参数列表可以用{}替换(), 所以这个时候需要用currying来降低参赛个数
scala> println("Hello, world!") //象方法调用 scala> println { "Hello, world!" } //更像built-in的控制结构,比如if
对于FP, 相对于OO使用继承和多态, 使用函数作为参数来实现代码重用, 希望可以将函数值放在{}, 显得更象built-in
比如下面, 每次打开文件, 操作, 关闭文件, 固定模式, 所以实现withPrintWriter, 每次传入不同的op就可以进行不同的操作, 而不用考虑文件开关
如果是oo实现, 就需要传入基类对象, 利用多态实现, 明显使用函数更轻量级一些
def withPrintWriter(file: File, op: PrintWriter => Unit) { val writer = new PrintWriter(file) try { op(writer) } finally { writer.close() } } //以调用方法的方式使用 withPrintWriter( new File("date.txt"), writer => writer.println(new java.util.Date) )
通过currying减少了参数, 所以就可以使用{}
def withPrintWriter(file: File)(op: PrintWriter => Unit) {......} //currying版本 val file = new File("date.txt") withPrintWriter(file) { writer => writer.println(new java.util.Date) } //将函数值放在{}, 很像built-in
尾递归,Tail recursion
前面说了, FP尽量避免使用循环, 而应该使用递归
但是递归效率有问题, 不停的压栈, 也很容易爆堆栈
所以对于某种递归, 尾递归, 编译器会自动优化成循环执行, 避免多次使用堆栈
局限是, 不是什么情况都能写成尾递归, 其实只有循环可以...
比clojuer好, 编译器会自动进行优化
//while, 循环版本,oo def approximateLoop(initialGuess: Double): Double = { var guess = initialGuess while (!isGoodEnough(guess)) guess = improve(guess) guess } //递归版本,FP def approximate(guess: Double): Double = if (isGoodEnough(guess)) guess else approximate(improve(guess))
Pattern Matching
pattern matching (模式匹配是FP里面比较高级的特性), 可以参考一下clojure的Multimethods
那么Scala要如何来支持pattern matching?
Case Class
首先case class是用于让你更加简洁的使用pattern matching, 如果你想对一个class进行pattern matching, 最好在前面加上case, 如下面的例子(string和double的一元或二元操作)
abstract class Expr case class Var(name: String) extends Expr case class Number(num: Double) extends Expr case class UnOp(operator: String, arg: Expr) extends Expr case class BinOp(operator: String, left: Expr, right: Expr) extends Expr
那么成为case class, 如何就能便于pattern matching?
1. 添加与类名一样的工厂方法, 即创建类对象时不需要new, 更简洁
scala> val v = Var("x") //替代new Var("x")
scala> val op = BinOp("+", Number(1), v)//new BinOp("+", new Number(1), v)
2. all arguments in the parameter list of a case class implicitly get a val prefix, so they are maintained as fields. 参数会默认隐含的加上val前缀, 所以可以当作field来用
scala> v.name res0: String = x scala> op.left res1: Expr = Number(1.0)
3. the compiler adds “natural” implementations of methods toString, hashCode, and equals to your class. 意味着可以用于println和"=="
scala> println(op) BinOp(+,Number(1.0),Var(x)) scala> op.right == Var("x") res3: Boolean = true
case class带来的副作用就是你的类会比原来的大一些
Pattern matching
下面给个例子来说明, 实现方式非常类似switch…case
match关键字表明匹配前面的expr
每个case, case开头, 跟着需要匹配的模式, =>后面加上执行语句
下面的例子的意思, 简化expr, 当出现两个-号, +0, 或*1的情况下, 就直接返回变量e就可以
def simplifyTop(expr: Expr): Expr = expr match { case UnOp("-", UnOp("-", e)) => e //UnOp构造器匹配(匹配类型和参数), "-"常量匹配(使用==), e变量匹配(任意值,并方便后面引用) case BinOp("+", e, Number(0)) => e case BinOp("*", e, Number(1)) => e // Multiplying by one case _ => expr //-通配符匹配, 匹配任意值,但无法引用 }
和switch case的区别
1. match是scala中的expression, 所以一定会返回值
2. 不用加break, 匹配即返回
3. 如果没有匹配上, 会抛MatchError异常, 所以你必须考虑到所有case, 或加上默认case
Pattern种类
Wildcard patterns, 通配符模式
catch all, 用于忽略你不关心的部分
expr match { case BinOp(_, _, _) => println(expr +"is a binary operation") case _ => println("It's something else") }
Constant patterns, 常量模式
Any literal may be used as a constant. For example, 5, true, and "hello" are all constant patterns.
Also, any val or singleton object can be used as a constant.
def describe(x: Any) = x match { case 5 => "five" case true => "truth" case "hello" => "hi!" case Nil => "the empty list" //单例对象Nil匹配空列表 case _ => "something else" }
Variable patterns, 变量模式
变量模式, 和通配符一样是catch all, 但是区别就在于, 后面可以通过变量引用
expr match { case 0 => "zero" case somethingElse => "not zero: "+ somethingElse }
变量模式和常量模式的区别
scala> import Math.{E, Pi} import Math.{E, Pi} scala> E match { case Pi => "strange math? Pi = "+ Pi //Pi是常量而非变量 case _ => "OK" } res10: java.lang.String = OK所有小写字母开头的为变量模式, 其他的都是常量模式
如果要用小写字母开头表示变量, 两种方法, 加前缀this.pi, 加反引号`pi`
Constructor patterns, 构造器模式
这个模式使得scala的模式匹配显得格外强大, 因为这样可以deep match
比如下面的例子, 可以匹配BinOP类, 类参数+, e, Number类, Number类的参数0, 可以达到3层的deep match
expr match { case BinOp("+", e, Number(0)) => println("a deep match") case _ => }
Sequence patterns, 序列模式
expr match { case List(0, _, _) => println("found it") //第一个为0的length为3的List case _ => } expr match { case List(0, _*) => println("found it") //第一个为0,可变长的List case _ => }
Tuple patterns, 元组模式
特别之处在于, 可以用tuple给多个变量赋值
def tupleDemo(expr: Any) = expr match { case (a, b, c) => println("matched "+ a + b + c) case _ => }
Typed patterns, 类型模式
需要注意的是, 在类型模式下用s替换x
原因在于, x的类型是Any, 所以你无法调用x.length, 所以必须使用string s来替换x
def generalSize(x: Any) = x match { case s: String => s.length case m: Map[_, _] => m.size //匹配任意Map case _ => -1 }
可以看到这里很方便, 如果你对比一下使用传统的isInstanceOf and asInstanceOf的方式
Type erasure, 类型擦除
可以看到运行时, 编译器是无法check Map中数据的类型的, 因为Scala和Java一样使用了erasure model of generics(泛型的擦除模式), 参数类型信息没有保留到运行期
scala> def isIntIntMap(x: Any) = x match { case m: Map[Int, Int] => true //会报unchecked warning, 因为无法check Map中是否为Int case _ => false } scala> isIntIntMap(Map(1 >1)) res17: Boolean = true scala> isIntIntMap(Map("abc" >"abc")) res18: Boolean = true //可以看出, 其实是无法区分Map中数据类型的
但是, 数组是唯一的意外, 因为他们被特殊实现, 在运行期会保留参数类型信息,
scala> def isStringArray(x: Any) = x match { case a: Array[String] => "yes" case _ => "no" } scala> val ai = Array(1, 2, 3) scala> isStringArray(ai) res20: java.lang.String = no //对于数组可以识别出
Variable binding, 变量绑定
通过@将UnOp("abs", _)绑定到变量e上
expr match { case UnOp("abs", e @ UnOp("abs", _)) => e case _ => }
Pattern guards
其实就是在模式后面加上一定的条件判断, 通过了才会执行后面的语句
比如下面的例子, 判断x是否等于y,
scala> def simplifyAdd(e: Expr) = e match { case BinOp("+", x, y) if x == y => BinOp("*", x, Number(2)) //加上条件判断, 称为pattern guard case _ => e }
Pattern overlaps
Scala支持多个Pattern重叠, 比如下面的例子, 匹配第一个的, 一定也会匹配第二个, 第三个
但顺序不能反, 必须先具体的, 后general的, 否则编译器会报错
def simplifyAll(expr: Expr): Expr = expr match { case UnOp("-",UnOp("-",e)) => simplifyAll(e) // ‘-’is its own inverse case UnOp(op, e) => UnOp(op, simplifyAll(e)) case _ => expr }
Sealed classes
Sealed意味着只能在这个文件定义子类, 这样就不能随便在其他地方增加case类
这样做的原因是, scala要求模式匹配时需要考虑到所有的情况, 所以程序员都需要知道到底有哪些case class
通过Sealed class, 不但程序员可以在该文件中找到所有的case class, 而且scala编译器也会找到, 并在编译时做检查,
sealed abstract class Expr //sealed case class Var(name: String) extends Expr case class Number(num: Double) extends Expr case class UnOp(operator: String, arg: Expr) extends Expr case class BinOp(operator: String, left: Expr, right: Expr) extends Expr
def describe(e: Expr): String = (e: @unchecked) match { //@unchecked告诉编译器不用check这个match的case case Number(_) => "a number" case Var(_) => "a variable" }
5 Actors and Concurrency
在处理并发上, Scala采取了和Clojure完全不同的思路, 采取actor模式尽量避免并发冲突的可能性, 用于补充Java传统的并发模式
Actors and message passing
Actor模式就不解释了, 这里actor就是一个线程
下面定义的echoActor, 会不停的读inbox, 并打印出收到的message
val echoActor = actor { while (true) { receive { case msg => println("received message: "+ msg) } } }
然后, 如何给actor发消息? 很简单, 如下
scala> echoActor ! "hi there" received message: hi there //echoActor线程收到message, 并打印出来
并且这里receive只会接收匹配的message(即匹配receive中的case的), 对于不匹配的message不会去处理
将普通线程当作actor
在scala中也可以将native线程当作actor来用, 并且通过Actor.self来当前线程当作actor看
import scala.actors.Actor._ scala> self ! "hello" //给当前线程actor发送消息 scala> self.receive { case x => x } //当前线程receive res6: Any = hello
这里需要先发message, 再recieve, 因为receive是block函数, 没有message不会返回
当然也可以使用receiveWithin, 来设置timeout
scala> self.receiveWithin(1000) { case x => x } // wait a sec! res7: Any = TIMEOUT
高效的react方法
actor模式问题是, 每个actor都需要一个线程, 所以如果需要定义大量的actor效率会比较低
JVM只能创建几千个线程, 并且线程间的切换也是比较低效的
所以使用react方法可以共享线程以提高效率...
原理在于, Scala actor模块会维护一个线程池, 用于分配线程给每个线程
对于receive方法, 分配的线程会一直被actor占住, 因为receive会返回值需要保持当前的调用栈
而react方法, 不会返回, 不需要保持调用栈, 所以执行完这个线程就可以被释放, 用于其他的actor
object NameResolver extends Actor { import java.net.{InetAddress, UnknownHostException} def act() { react { case (name: String, actor: Actor) => actor ! getIp(name) act() case "EXIT" => println("Name resolver exiting.")// quit case msg => println("Unhandled message: "+ msg) act() } } def getIp(name: String): Option[InetAddress] = { try { Some(InetAddress.getByName(name)) } catch { case _:UnknownHostException => None } } }
How react works
A return type of Nothing indicates a function will never return normally, but instead will always complete abruptly with an exception. And indeed, this is true of react.
react的返回值是Nothing, 意思是不会正常返回, 即以异常作为react的结束
The actual implementation of react is not as simple as the following description, and subject to change, but conceptually you can think of react as working like this:
When you call start on an actor, the start method will in some way arrange things such that some thread will eventually call act on that actor. If that act method invokes react, the react method will look in the actor’s mailbox for a message the passed partial function can handle. (It does this the same way as receive, by passing candidate messages to the partial function’s isDefinedAt method.)
前面都是和receive一样的, 线程调用act, act调用react从actor的mailbox里面check是否有需要处理的message
If it finds a message that can be handled, react will schedule the handling of that message for later execution and throw an exception.
If it doesn’t find one, it will place the actor in “cold storage,” to be resurrected if and when it gets more messages in its mailbox, and throw an exception.
In either case, react will complete abruptly with this exception, and so will act. The thread that invoked act will catch the exception, forget about the actor, and move on to other duties.
如果找到需要处理的message, react就会处理这条message, 最终抛出exception, 如果找不到合适的message, 会将这个actor暂时休眠, 并抛出exception
可以看到, 这里的关键在于, react一定会以exception结束, 最终调用act的线程会catch到这个exception, 知道react已经完成, 于是接着处理其他的事情, 所以达到线程重用
This is why if you want react to handle more than the first message, you’ll need to call act again from inside your partial function, or use some other means to get react invoked again.
使用Actor.Loop来反复执行消息处理, 而不能用while
def act() { loop { react { case (name: String, actor: Actor) => actor ! getIp(name) case msg => println("Unhandled message: " + msg) } } }
Actors should not block
主Actor不应被阻塞, 因为这样他就不能及时的响应其他消息
处理的方法, 就是阻塞逻辑放到一个新的actor里面执行, 因为这个actor不会接收其他消息, 所以被阻塞是没有影响的
val sillyActor2 = actor { def emoteLater() { val mainActor = self actor { //使用新的actor Thread.sleep(1000) //阻塞逻辑 mainActor ! "Emote" //将结果返回给主actor } } var emoted = 0 emoteLater() loop { react { case "Emote" => println("I'm acting!") emoted += 1 if (emoted < 5) emoteLater() //这里没有直接执行阻塞逻辑, 而是调用新的actor来执行 case msg => println("Received: "+ msg) } } }
实际运行结果, 可见发送给sillyActor2的消息立即会得到处理, 不会被阻塞
scala> sillyActor2 ! "hi there"
scala> Received: hi there
I'm acting!
I'm acting!
I'm acting!
Make messages self-contained
通过case class来定义消息格式
否则如果只是简单的, lookerUpper ! ("www.scalalang.org", self), 其他人很难理解你的消息
import scala.actors.Actor._ import java.net.{InetAddress, UnknownHostException} case class LookupIP(name: String, respondTo: Actor) //通过case class来定义消息格式, 来便于理解 case class LookupResult( name: String, address: Option[InetAddress] ) object NameResolver2 extends Actor { def act() { loop { react { case LookupIP(name, actor) => actor ! LookupResult(name, getIp(name)) } } } def getIp(name: String): Option[InetAddress] = { // As before (in Listing 30.3) } }