Clojure首先是FP, 但是由于基于JVM, 所以不得已需要做出一些妥协, 包含一些OO的编程方式
Scala首先是OO, Java语法过于冗余, 一种比较平庸的语言, Scala首先做的是简化, 以更为简洁的方式来编写OO, 主要利用‘type inference’能推断出来的, 你就不用写, 但如果仅仅这样, 不如用python
所以Scala象其名字一样, “可伸展的语言”, 它是个大的集市, 它积极吸纳其他语言的优秀的特征, 最重要的就是FP, 你可以使用Scala来写OO, 但它推荐使用FP的方式来写Scala; 还包括Erlang里面的actor模型
所以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)
2 数据结构
数组
可变的同类对象序列, 适用于OO场景
val greetStrings = new Array[String](3) //greetStrings为val, 但是内部的数组值是可变的greetStrings(0) = "Hello" //scala用()而非[]greetStrings(1) = ", "greetStrings(2) = "world!\n"
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
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.Setimport scala.collection.mutable.Map
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, 默认将最后一次计算的值返回
不写return是推荐的方式, 因为函数尽量不要有多个出口
class ChecksumAccumulator {
private var sum = 0
def add(b: Byte): Unit = sum += b
def checksum(): Int = ~(sum & 0xFF) + 1
}
其实对于Unit(void), 即没有返回值, 对于FP而言, 就是该function只会产生side effect, 还有另外一种简写的方式
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) }
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))