[关闭]
@spiritnotes 2016-03-18T07:25:54.000000Z 字数 11516 阅读 1617

Scala 指南

Scala


原文地址

http://zh.scala-tour.com/

基础

几乎所有语言元素都是表达式,可以通过val定义一个常量,亦可以通过var定义一个变量

函数是一等公民
可以使用def来定义一个函数。函数体是一个表达式。
使用Block表达式的时候,默认最后一行的返回是返回值,无需显式指定。
函数还可以像值一样,赋值给var或val。因此函数也可以作为参数传给另一个函数

借贷模式

由于函数可以像值一样作为参数传递,所以可以方便的实现借贷模式。
这个例子是从/proc/self/stat文件中读取当前进程的pid。
withScanner封装了try-finally块,所以调用者不用再close。

  1. import scala.reflect.io.File
  2. import java.util.Scanner
  3. def withScanner(f: File, op: Scanner => Unit) = {
  4. val scanner = new Scanner(f.bufferedReader)
  5. try {
  6. op(scanner)
  7. } finally {
  8. scanner.close()
  9. }
  10. }
  11. withScanner(File("/proc/self/stat"),
  12. scanner => println("pid is " + scanner.next()))

按名称传递参数

参数是按名称传递,参数会等到实际使用的时候才会计算。按名称传递参数可以减少不必要的计算和异常。

  1. def log(msg: => String)

定义类

可以用class关键字来定义类。并通过new来创建类。
在定义类时可以定义字段,如firstName,lastName。这样做还可以自动生成构造函数。
可以在类中通过def定义函数。var和val定义字段。
函数名是任何字符如+,-,*,/

鸭子类型

使用{ def close(): Unit }作为参数类型。因此任何含有close()的函数的类都可以作为参数。
不必使用继承这种不够灵活的特性。

  1. def withClose(closeAble: { def close(): Unit },
  2. op: { def close(): Unit } => Unit) {
  3. try {
  4. op(closeAble)
  5. } finally {
  6. closeAble.close()
  7. }
  8. }
  9. class Connection {
  10. def close() = println("close Connection")
  11. }
  12. val conn: Connection = new Connection()
  13. withClose(conn, conn =>
  14. println("do something with Connection"))

柯里化

柯里化(Currying)技术。
def add(x:Int, y:Int) = x + y是普通的函数
def add(x:Int) = (y:Int) => x + y是柯里化后的函数,相当于返回一个匿名函数表达式。
def add(x:Int)(y:Int) = x + y是简化写法
柯里化可以让我们构造出更像原生语言提供的功能的代码

  1. def withClose(closeAble: { def close(): Unit })
  2. (op: { def close(): Unit } => Unit) {
  3. try {
  4. op(closeAble)
  5. } finally {
  6. closeAble.close()
  7. }
  8. }
  9. class Connection {
  10. def close() = println("close Connection")
  11. }
  12. val conn: Connection = new Connection()
  13. withClose(conn){conn =>
  14. println("do something with Connection")}

范型

  1. def withClose[A <: { def close(): Unit }, B](closeAble: A)
  2. (f: A => B): B =
  3. try {
  4. f(closeAble)
  5. } finally {
  6. closeAble.close()
  7. }
  8. class Connection {
  9. def close() = println("close Connection")
  10. }
  11. val conn: Connection = new Connection()
  12. val msg = withClose(conn) { conn =>
  13. {
  14. println("do something with Connection")
  15. "123456"
  16. }
  17. }
  18. println(msg)

Traits

Traits就像是有函数体的Interface。使用with关键字来混入。
这个例子是给java.util.ArrayList添加了foreach的功能。
试着再在with ForEachAble[Int]后面加上with JsonAble给list添加toJson的能力

  1. trait ForEachAble[A] {
  2. def iterator: java.util.Iterator[A]
  3. def foreach(f: A => Unit) = {
  4. val iter = iterator
  5. while (iter.hasNext)
  6. f(iter.next)
  7. }
  8. }
  9. trait JsonAble {
  10. def toJson() =
  11. scala.util.parsing.json.JSONFormat.defaultFormatter(this)
  12. }
  13. val list = new java.util.ArrayList[Int]() with ForEachAble[Int] with JsonAble
  14. list.add(1); list.add(2)
  15. println("For each: "); list.foreach(println(_))
  16. println("Json: " + list.toJson())

函数式

模式匹配

模式匹配是类似switch-case特性,但更加灵活;也类似if-else,但更加简约

  1. def fibonacci(in: Any): Int = in match {
  2. case 0 => 0
  3. case 1 => 1
  4. case n:Int if (n>1) => fibonacci(n - 1) + fibonacci(n - 2)
  5. case n:String => fibonacci(n.toInt)
  6. case _ => 0
  7. }
  8. println(fibonacci(3))
  9. println(fibonacci(-3))
  10. println(fibonacci("3"))

Case Class

case class 顾名思义就是为case语句专门设计的类, 在普通类的基础上添加了和类名一直的工厂方法, 还添加了hashcode,equals和toString等方法。

  1. abstract class Expr
  2. case class FibonacciExpr(n: Int) extends Expr {
  3. require(n >= 0)
  4. }
  5. case class SumExpr(a: Expr, b: Expr) extends Expr
  6. def value(in: Expr): Int = in match {
  7. case FibonacciExpr(0) => 0
  8. case FibonacciExpr(1) => 1
  9. case FibonacciExpr(n) =>
  10. value(SumExpr(FibonacciExpr(n - 1), FibonacciExpr(n - 2)))
  11. case SumExpr(a, b) => value(a) + value(b)
  12. case _ => 0
  13. }
  14. println(value(FibonacciExpr(3)))

函数式的威力

这个例子是用指令式编程判断一个List中是否含有奇数
试着将containsOdd(list)替换为list.exists((x: Int) => x % 2 ==1)通过将函数作为参数,可以使程序更简洁。还可以再简化为list.exists(_ % 2 == 1)可以用_替代参数

  1. val list = List(1, 2, 3, 4)
  2. def containsOdd(list: List[Int]): Boolean = {
  3. for (i <- list) {
  4. if (i % 2 == 1)
  5. return true;
  6. }
  7. return false;
  8. }
  9. println("list contains Odd ? " + containsOdd(list))

函数式真正的威力

函数式除了能简化代码外,更重要的是他关注的是Input和Output,函数本身没有副作用。就是Unix pipeline一样,简单的命令组合在一起威力无穷。

  1. val file = List("warn 2013 msg", "warn 2012 msg",
  2. "error 2013 msg", "warn 2013 msg")
  3. println("cat file | grep 'warn' | grep '2013' | wc : "
  4. + file.filter(_.contains("warn")).filter(_.contains("2013")).size)

Word Count

  1. val file = List("warn 2013 msg", "warn 2012 msg",
  2. "error 2013 msg", "warn 2013 msg")
  3. def wordcount(str: String): Int = str.split(" ").count("msg" == _)
  4. val num = file.map(wordcount).reduceLeft(_ + _)
  5. println("wordcount:" + num)

尾递归

尾递归是递归的一种,特点在于会在函数的最末调用自身。尾递归是函数式编程的常见写法。尾递归会在编译期优化,因此不用担心递归造成的栈溢出问题。

  1. def foldLeft(list: List[Int])(init: Int)(f: (Int, Int) => Int): Int = {
  2. list match {
  3. case List() => init
  4. case head :: tail => foldLeft(tail)(f(init, head))(f)
  5. }
  6. }
  7. val num = foldLeft(file.map(wordcount))(0)(_ + _)

更强大的For循环

循环语句是指令式编程的常见语句,Scala对其加以改进,成为适应函数式风格的利器。
For循环也是有返回值的,返回的是一个List。在每一轮迭代中加入yield,yield后的值可以加入到List中。

  1. val counts =
  2. for (line <- file)
  3. yield wordcount(line)

Option

Scala提供了Option机制来解决,代码中不断检查null的问题。
这个例子包装了getProperty方法,使其返回一个Option。 这样就可以不再漫无目的地null检查。只要Option类型的值即可。
使用pattern match来检查是常见做法。也可以使用getOrElse来提供当为None时的默认值。
给力的是Option还可以看作是最大长度为1的List,List的强大功能都可以使用。再见 NullException

  1. def getProperty(name: String): Option[String] = {
  2. val value = System.getProperty(name)
  3. if (value != null) Some(value) else None
  4. }
  5. val osName = getProperty("os.name")
  6. osName match {
  7. case Some(value) => println(value)
  8. case _ => println("none")
  9. }
  10. println(osName.getOrElse("none"))
  11. osName.foreach(print _)

Lazy初始化

Lazy可以延迟初始化字段。加上lazy的字段会在第一次访问的时候初始化,而不是类初始化的时候初始化。
Lazy非常适合于初始化非常耗时的场景

  1. class ScalaCurrentVersion(val url: String) {
  2. lazy val source= {
  3. println("fetching from url...")
  4. scala.io.Source.fromURL(url).getLines().toList
  5. }
  6. lazy val majorVersion = source.find(_.contains("version.major"))
  7. lazy val minorVersion = source.find(_.contains("version.minor"))
  8. }
  9. val version = new ScalaCurrentVersion(
  10. "https://raw.github.com/scala/scala/master/build.number")
  11. println("get scala version from " + version.url)
  12. version.majorVersion.foreach(println _)
  13. version.minorVersion.foreach(println _)

并发

使用Actor

Actor是Scala的并发模型。在2.10之后的版本中,使用Akka作为其推荐Actor实现。
Actor是类似线程的实体,有一个邮箱。 Actor可以通过system.actorOf来创建,receive获取邮箱消息,!向邮箱发送消息。

  1. import akka.actor.{ Actor, ActorSystem, Props }
  2. val system = ActorSystem()
  3. class EchoServer extends Actor {
  4. def receive = {
  5. case msg: String => println("echo " + msg)
  6. }
  7. }
  8. val echoServer = system.actorOf(Props[EchoServer])
  9. echoServer ! "hi"
  10. system.shutdown

Actor更简化的用法

可以通过更简化的办法声明Actor。
通过akka.actor.ActorDSL中的actor函数。这个函数可以接受一个Actor的构造器Act,启动并返回Actor。

  1. import akka.actor.ActorDSL._
  2. import akka.actor.ActorSystem
  3. implicit val system = ActorSystem()
  4. val echoServer = actor(new Act {
  5. become {
  6. case msg => println("echo " + msg)
  7. }
  8. })
  9. echoServer ! "hi"
  10. system.shutdown

Actor原理

Actor比线程轻量。在Scala中可以创建数以百万级的Actor。奥秘在于Actor直接可以复用线程。
Actor和线程是不同的抽象,他们的对应关系是由Dispatcher决定的。
这个例子创建4个Actor,每次调用的时候打印自身线程名称。
可以发现Actor和线程之间没有一对一的对应关系。一个Actor可以使用多个线程,一个线程也会被多个Actor复用。

  1. import akka.actor.{ Actor, Props, ActorSystem }
  2. import akka.testkit.CallingThreadDispatcher
  3. implicit val system = ActorSystem()
  4. class EchoServer(name: String) extends Actor {
  5. def receive = {
  6. case msg => println("server" + name + " echo " + msg +
  7. " by " + Thread.currentThread().getName())
  8. }
  9. }
  10. val echoServers = (1 to 10).map(x =>
  11. system.actorOf(Props(new EchoServer(x.toString))
  12. .withDispatcher(CallingThreadDispatcher.Id)))
  13. (1 to 10).foreach(msg =>
  14. echoServers(scala.util.Random.nextInt(10)) ! msg.toString)
  15. system.shutdown

同步返回

Actor非常适合于较耗时的操作。比如获取网络资源。
这个例子通过调用ask函数来获取一个Future。
在Actor内部通过 sender ! 传递结果。
Future像Option一样有很多高阶方法,可以使用foreach查看结果。

  1. import akka.actor.ActorDSL._
  2. import akka.pattern.ask
  3. implicit val ec = scala.concurrent.ExecutionContext.Implicits.global
  4. implicit val system = akka.actor.ActorSystem()
  5. val versionUrl = "https://raw.github.com/scala/scala/master/starr.number"
  6. val fromURL = actor(new Act {
  7. become {
  8. case url: String => sender ! scala.io.Source.fromURL(url)
  9. .getLines().mkString("\n")
  10. }
  11. })
  12. val version = fromURL.ask(versionUrl)(akka.util.Timeout(5 * 1000))
  13. version.foreach(println _)
  14. system.shutdown

异步返回

异步操作可以最大发挥效能。Scala的Futrue很强大,可以异步返回。
可以实现Futrue的onComplete方法。当Futrue结束的时候就会回调。
在调用ask的时候,可以设定超时。

  1. import akka.actor.ActorDSL._
  2. import akka.pattern.ask
  3. implicit val ec = scala.concurrent.ExecutionContext.Implicits.global
  4. implicit val system = akka.actor.ActorSystem()
  5. val versionUrl = "https://raw.github.com/scala/scala/master/starr.number"
  6. val fromURL = actor(new Act {
  7. become {
  8. case url: String => sender ! scala.io.Source.fromURL(url)
  9. .getLines().mkString("\n")
  10. }
  11. })
  12. val version = fromURL.ask(versionUrl)(akka.util.Timeout(5 * 1000))
  13. version onComplete {
  14. case msg => println(msg); system.shutdown
  15. }

并行集合

这个例子是访问若干URL,并记录时间。如果能并行访问,就可以大幅提高性能。

尝试将urls.map修改为urls.par.map这样每个map中的函数都可以并发执行。

当函数式和并发结合,就会这样让人兴奋

  1. val urls = List("http://scala-lang.org",
  2. "https://github.com/yankay/scala-tour")
  3. def fromURL(url: String) = scala.io.Source.fromURL(url)
  4. .getLines().mkString("\n")
  5. val t = System.currentTimeMillis()
  6. urls.map(fromURL(_))
  7. println("time: " + (System.currentTimeMillis - t) + "ms")

并行wordcount

这个例子是访问若干URL,并记录时间。
并行集合支持大部分集合的功能。
在前面有一个wordcount例子,也可以用并行集合加以实现。
不增加程序复杂性,却能大幅提高利用多核的能力。

  1. val file = List("warn 2013 msg", "warn 2012 msg",
  2. "error 2013 msg", "warn 2013 msg")
  3. def wordcount(str: String): Int = str.split(" ").count("msg" == _)
  4. val num = file.par.map(wordcount).par.reduceLeft(_ + _)
  5. println("wordcount:" + num)

远程Actor

Actor是并发模型,也使用于分布式。
这个例子创建一个Echo服务器,通过actorOf来注册自己。
然后再创建一个client,通过akka url来寻址。
除了是通过url创建的,其他使用的方法和普通Actor一样。

  1. import akka.actor.{ Actor, ActorSystem, Props }
  2. import com.typesafe.config.ConfigFactory
  3. implicit val system = akka.actor.ActorSystem("RemoteSystem",
  4. ConfigFactory.load.getConfig("remote"))
  5. class EchoServer extends Actor {
  6. def receive = {
  7. case msg: String => println("echo " + msg)
  8. }
  9. }
  10. val server = system.actorOf(Props[EchoServer], name = "echoServer")
  11. val echoClient = system
  12. .actorFor("akka://RemoteSystem@127.0.0.1:2552/user/echoServer")
  13. echoClient ! "Hi Remote"
  14. system.shutdown

实践

使用Java

  1. import org.apache.commons.beanutils.BeanUtils
  2. import scala.beans.BeanProperty
  3. class SimpleBean(var name: String) {
  4. }
  5. @BeanProperty val bean = new SimpleBean("foo")
  6. println(BeanUtils.describe(bean))

相等性

在Scala中==等效于equals,这一点和Java不同。更自然一些。
写一个完全正确的equal函数并不容易,这个例子也有子类会不对称的Bug。
尝试将class修改为case class,并删除equals函数。
case类会自动生成正确的equals函数。

  1. class Person(val name: String) {
  2. override def equals(other: Any) = other match {
  3. case that: Person => name.equals(that.name)
  4. case _ => false
  5. }
  6. }
  7. println(new Person("Black") == new Person("Black"))

抽取器

抽取器可以帮助模式匹配进行解构。
这个例子是构建一个Email抽取器,只要实现unapply函数就可以了。
Scala的正则表达式会自带抽取器,可以抽取出一个List。List里的元素是匹配()里的表达式。
抽取器很有用,短短的例子里就有两处使用抽取器:
case user :: domain :: Nil解构List
case Email(user, domain)解构Email。

  1. import scala.util.matching.Regex
  2. object Email {
  3. def unapply(str: String) = new Regex("""(.*)@(.*)""")
  4. .unapplySeq(str).get match {
  5. case user :: domain :: Nil => Some(user, domain)
  6. case _ => None
  7. }
  8. }
  9. "user@domain.com" match {
  10. case Email(user, domain) => println(user + "@" + domain)
  11. }

记忆模式

记忆模式可以解决手动编写存取cache代码的麻烦。
这个例子中,memo可以将一个不含cache函数,包装成一个含有cache功能的。
还是斐波那契的例子,通过cache可以使性能提高。
尝试将fibonacci_(n - 1) + fibonacci_(n - 2)修改为memo(fibonacci_)(n - 1) + memo(fibonacci_)(n - 2)可以提高更多性能。

  1. import scala.collection.mutable.WeakHashMap
  2. val cache = new WeakHashMap[Int, Int]
  3. def memo(f: Int => Int) = (x: Int) => cache.getOrElseUpdate(x, f(x))
  4. def fibonacci_(in: Int): Int = in match {
  5. case 0 => 0;
  6. case 1 => 1;
  7. case n: Int => fibonacci_(n - 1) + fibonacci_(n - 2)
  8. }
  9. val fibonacci: Int => Int = memo(fibonacci_)
  10. val t1 = System.currentTimeMillis()
  11. println(fibonacci(40))
  12. println("it takes " + (System.currentTimeMillis() - t1) + "ms")
  13. val t2 = System.currentTimeMillis()
  14. println(fibonacci(40))
  15. println("it takes " + (System.currentTimeMillis() - t2) + "ms")

隐式转换

implicit可以定义一个转换函数,可以在使用相应类型的时候自动转换。
这个例子可以将String自动转换为Date类型。隐式转换时实现DSL的重要工具。

  1. import java.text.SimpleDateFormat
  2. implicit def strToDate(str: String) =
  3. new SimpleDateFormat("yyyy-MM-dd").parse(str)
  4. println("2013-01-01 unix time: " + "2013-01-01".getTime()/1000l)

DSL

DSL是Scala最强大武器,可以使一些描述性代码变得极为简单。

这个例子是使用DSL生成JSON。Scala很多看似是语言级的特性也是用DSL做到的。
自己编写DSL有点复杂,但使用起来非常方便。

  1. import org.json4s._
  2. import org.json4s.JsonDSL._
  3. import org.json4s.jackson.JsonMethods._
  4. import java.util.Date
  5. case class Twitter(id: Long, text: String, publishedAt: Option[java.util.Date])
  6. var twitters = Twitter(1, "hello scala", Some(new Date())) ::
  7. Twitter(2, "I like scala tour", None) :: Nil
  8. var json = ("twitters"
  9. -> twitters.map(
  10. t => ("id" -> t.id)
  11. ~ ("text" -> t.text)
  12. ~ ("published_at" -> t.publishedAt.toString())))
  13. println(pretty(render(json)))

测试

Scala可以使用Spec2,ScalaTest来测试, DSL可以使测试更方便。
这个例子是测试一个阶乘函数。使用should/in来建立测试用例。
测试是默认并发执行的。

  1. import org.specs2.mutable._
  2. class FactorialSpec extends Specification {
  3. args.report(color = false)
  4. def factorial(n: Int) = (1 to n).reduce(_ - _)
  5. "The 'Hello world' string" should {
  6. "factorial 3 must be 6" in {
  7. factorial(3) mustEqual 6
  8. }
  9. "factorial 4 must greaterThan 6" in {
  10. factorial(4) must greaterThan(6)
  11. }
  12. }
  13. }
  14. specs2.run(new FactorialSpec)

Simple Build Tool

SBT是Scala的最佳编译工具,在他的帮助下,
你甚至不需要安装除JRE外的任何东西,来开发Scala。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注