另外
最后更新于:2022-04-01 03:05:28
通过这些链接您可以了解更多:
* **[Effective Scala](http://twitter.github.io/effectivescala/index-cn.html)** Twitter的Scala最佳实践。对理解Twitter的代码风格非常有用。
* **[Scala 官网文档](http://docs.scala-lang.org/)** 包含教程,手册,API参考,书籍等...
* **[Scala API文档](http://www.scala-lang.org/api/)**
Searchbird
最后更新于:2022-04-01 03:05:26
我们要使用Scala和先前介绍的 [Finagle](http://github.com/twitter/finagle) 框架构建一个简单的分布式搜索引擎。
[TOC=3,3]
### 设计目标:大图景
从广义上讲,我们的设计目标包括 *抽象* (abstraction:在不知道其内部的所有细节的前提下,利用该系统功能的能力)、 *模块化* (modularity:把系统分解为小而简单的片段,从而更容易被理解和/或被更换的能力)和 *扩展性* (scalability:用简单直接的方法给系统扩容的能力)。
我们要描述的系统有三个部分: (1) *客户端* 发出请求,(2) *服务端* 接收请求并应答,和(3) *传送* 机制来这些通信包装起来。通常情况下,客户端和服务器位于不同的机器上,通过网络上的一个特定的 [*端口*](http://en.wikipedia.org/wiki/Port_(computer_networking)) 进行通信,但在这个例子中,它们将运行在同一台机器上(而且仍然使用端口进行通信) 。在我们的例子中,客户端和服务器将用Scala编写,传送协议将使用 [Thrift](http://thrift.apache.org/) 处理。本教程的主要目的是展示一个简单的具有良好可扩展性的服务器和客户端。
### 探索默认的引导程序项目
首先,使用 [scala-bootstrapper](https://github.com/twitter/scala-bootstrapper) 创建一个骨架项目( “ Searchbird ” )。这将创建一个简单的基于 [Finagle](http://twitter.github.com/finagle/) 和key-value内存存储的Scala服务。我们将扩展这个工程以支持搜索值,并进而支持多进程多个内存存储的搜索。
~~~
$ mkdir searchbird ; cd searchbird
$ scala-bootstrapper searchbird
writing build.sbt
writing config/development.scala
writing config/production.scala
writing config/staging.scala
writing config/test.scala
writing console
writing Gemfile
writing project/plugins.sbt
writing README.md
writing sbt
writing src/main/scala/com/twitter/searchbird/SearchbirdConsoleClient.scala
writing src/main/scala/com/twitter/searchbird/SearchbirdServiceImpl.scala
writing src/main/scala/com/twitter/searchbird/config/SearchbirdServiceConfig.scala
writing src/main/scala/com/twitter/searchbird/Main.scala
writing src/main/thrift/searchbird.thrift
writing src/scripts/searchbird.sh
writing src/scripts/config.sh
writing src/scripts/devel.sh
writing src/scripts/server.sh
writing src/scripts/service.sh
writing src/test/scala/com/twitter/searchbird/AbstractSpec.scala
writing src/test/scala/com/twitter/searchbird/SearchbirdServiceSpec.scala
writing TUTORIAL.md
~~~
首先,来看下 `scala-bootstrapper` 为我们创建的默认项目。这是一个模板。虽然最终将替换它的大部分内容,不过作为支架它还是很方便的。它定义了一个简单(但完整)的key-value存储,并包含了配置、thrift接口、统计输出和日志记录。
在我们看代码之前,先运行一个客户端和服务器,看看它是如何工作的。这里是我们构建的:
![Searchbird implementation, revision 1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-07_55ed22e39de52.svg "Searchbird implementation, revision 1")
这里是我们的服务输出的接口。由于Searchbird服务是一个 [Thrift](http://thrift.apache.org/) 服务(和我们大部分服务一样),因而其外部接口使用Thrift IDL(“接口描述语言”)定义。
##### src/main/thrift/searchbird.thrift
~~~
service SearchbirdService {
string get(1: string key) throws(1: SearchbirdException ex)
void put(1: string key, 2: string value)
}
~~~
这是非常直观的:我们的服务 `SearchbirdService` 输出两个RPC方法 `get` 和 `put` 。他们组成了一个到key-value存储的简单接口。
现在,让我们运行默认的服务,启动客户端连接到这个服务,并通过这个接口来探索他们。打开两个窗口,一个用于服务器,一个用于客户端。
在第一个窗口中,用交互模式启动SBT(在命令行中运行 `./sbt`[1](http://twitter.github.io/scala_school/zh_cn/searchbird.html#fn1)),然后构建和运行项目内SBT。这会运行 `Main.scala` 定义的 `主` 进程。
~~~
$ ./sbt
...
> compile
> run -f config/development.scala
...
[info] Running com.twitter.searchbird.Main -f config/development.scala
~~~
配置文件 (`development.scala`) 实例化一个新的服务,并监听9999端口。客户端可以连接到9999端口使用此服务。
现在,我们将使用 `控制台` shell脚本初始化和运行一个客户端实例,即 `SearchbirdConsoleClient` 实例 (`SearchbirdConsoleClient.scala`) 。在另一个窗口中运行此脚本:
~~~
$ ./console 127.0.0.1 9999
[info] Running com.twitter.searchbird.SearchbirdConsoleClient 127.0.0.1 9999
'client' is bound to your thrift client.
finagle-client>
~~~
客户端对象 `client` 现在连接到本地计算机上的9999端口,并可以跟服务交互了。接下来我们发送一些请求:
~~~
scala> client.put("marius", "Marius Eriksen")
res0: ...
scala> client.put("stevej", "Steve Jenson")
res1: ...
scala> client.get("marius")
res2: com.twitter.util.Future[String] = ...
scala> client.get("marius").get()
res3: String = Marius Eriksen
~~~
(第二个 `get()` 调用解析 `client.get()` 返回的 `Future` 类型值,阻塞直到该值准备好。)
该服务器还输出运行统计(配置文件中指定这些信息在9900端口)。这不仅方便对各个服务器进行检查,也利于聚集全局的服务统计(以机器可读的JSON接口)。打开第三个窗口来查看这些统计:
~~~
$ curl localhost:9900/stats.txt
counters:
Searchbird/connects: 1
Searchbird/received_bytes: 264
Searchbird/requests: 3
Searchbird/sent_bytes: 128
Searchbird/success: 3
jvm_gc_ConcurrentMarkSweep_cycles: 1
jvm_gc_ConcurrentMarkSweep_msec: 15
jvm_gc_ParNew_cycles: 24
jvm_gc_ParNew_msec: 191
jvm_gc_cycles: 25
jvm_gc_msec: 206
gauges:
Searchbird/connections: 1
Searchbird/pending: 0
jvm_fd_count: 135
jvm_fd_limit: 10240
jvm_heap_committed: 85000192
jvm_heap_max: 530186240
jvm_heap_used: 54778640
jvm_nonheap_committed: 89657344
jvm_nonheap_max: 136314880
jvm_nonheap_used: 66238144
jvm_num_cpus: 4
jvm_post_gc_CMS_Old_Gen_used: 36490088
jvm_post_gc_CMS_Perm_Gen_used: 54718880
jvm_post_gc_Par_Eden_Space_used: 0
jvm_post_gc_Par_Survivor_Space_used: 1315280
jvm_post_gc_used: 92524248
jvm_start_time: 1345072684280
jvm_thread_count: 16
jvm_thread_daemon_count: 7
jvm_thread_peak_count: 16
jvm_uptime: 1671792
labels:
metrics:
Searchbird/handletime_us: (average=9598, count=4, maximum=19138, minimum=637, p25=637, p50=4265, p75=14175, p90=19138, p95=19138, p99=19138, p999=19138, p9999=19138, sum=38393)
Searchbird/request_latency_ms: (average=4, count=3, maximum=9, minimum=0, p25=0, p50=5, p75=9, p90=9, p95=9, p99=9, p999=9, p9999=9, sum=14)
~~~
除了我们自己的服务统计信息以外,还有一些通用的JVM统计。
现在,让我们来看看配置、服务器和客户端的实现代码。
##### …/config/SearchbirdServiceConfig.scala
配置是一个Scala的特质,有一个方法 `apply: RuntimeEnvironment => T` 来创建一些 `T` 。在这个意义上,配置是“工厂” 。在运行时,配置文件(通过使用Scala编译器库)被取值为一个脚本,并产生一个配置对象。 `RuntimeEnvironment` 是一个提供各种运行参数(命令行标志, JVM版本,编译时间戳等)查询的一个对象。
`SearchbirdServiceConfig` 类就是这样一个配置类。它使用其默认值一起指定配置参数。 (Finagle 支持一个通用的跟踪系统,我们在本教程将不会介绍:[Zipkin](https://github.com/twitter/zipkin) 一个集合/聚合轨迹的 分布式跟踪系统。)
~~~
class SearchbirdServiceConfig extends ServerConfig[SearchbirdService.ThriftServer] {
var thriftPort: Int = 9999
var tracerFactory: Tracer.Factory = NullTracer.factory
def apply(runtime: RuntimeEnvironment) = new SearchbirdServiceImpl(this)
}
~~~
在我们的例子中,我们要创建一个 `SearchbirdService.ThriftServer`。这是由thrift代码生成器生成的服务器类型[2](http://twitter.github.io/scala_school/zh_cn/searchbird.html#fn2)。
##### …/Main.scala
在SBT控制台中键入“run”调用 `main` ,这将配置和初始化服务器。它读取配置(在 `development.scala` 中指定,并会作为参数传给“run”),创建`SearchbirdService.ThriftServer` ,并启动它。 `RuntimeEnvironment.loadRuntimeConfig` 执行配置赋值,并把自身作为一个参数来调用 `apply`[3](http://twitter.github.io/scala_school/zh_cn/searchbird.html#fn3)。
~~~
object Main {
private val log = Logger.get(getClass)
def main(args: Array[String]) {
val runtime = RuntimeEnvironment(this, args)
val server = runtime.loadRuntimeConfig[SearchbirdService.ThriftServer]
try {
log.info("Starting SearchbirdService")
server.start()
} catch {
case e: Exception =>
log.error(e, "Failed starting SearchbirdService, exiting")
ServiceTracker.shutdown()
System.exit(1)
}
}
}
~~~
##### …/SearchbirdServiceImpl.scala
这是实质的服务:我们用自己的实现扩展 `SearchbirdService.ThriftServer` 。回忆一下thrift为我们生成的 `SearchbirdService.ThriftServer` 。它为每一个thrift方法生成一个Scala方法。到目前为止,在我们的例子中生成的接口是:
~~~
trait SearchbirdService {
def put(key: String, value: String): Future[Void]
def get(key: String): Future[String]
}
~~~
返回值是 `Future[Value]` 而不是直接返回值,可以推迟它们的计算(finagle的 [文档](http://twitter.github.io/scala_school/zh_cn/finagle.html) 有 `Future` 更多的细节)。对本教程的目的来说,你唯一需要知道的有关 `Future` 的知识点是,可以通过 `get()` 获取其值。
`scala-bootstrapper` 默认实现的key-value存储很简单:它提供了一个通过 `get` 和 `put` 访问的 `数据库` 数据结构。
~~~
class SearchbirdServiceImpl(config: SearchbirdServiceConfig) extends SearchbirdService.ThriftServer {
val serverName = "Searchbird"
val thriftPort = config.thriftPort
override val tracerFactory = config.tracerFactory
val database = new mutable.HashMap[String, String]()
def get(key: String) = {
database.get(key) match {
case None =>
log.debug("get %s: miss", key)
Future.exception(SearchbirdException("No such key"))
case Some(value) =>
log.debug("get %s: hit", key)
Future(value)
}
}
def put(key: String, value: String) = {
log.debug("put %s", key)
database(key) = value
Future.Unit
}
def shutdown() = {
super.shutdown(0.seconds)
}
}
~~~
其结果是构建在 Scala `HashMap` 上的一个简单thrift接口。
## 一个简单的搜索引擎
现在,我们将扩展现有的例子,来创建一个简单的搜索引擎。然后,我们将进一步扩展它成为由多个分片组成的 *分布式* 搜索引擎,使我们能够适应比单台机器内存更大的语料库。
为了简单起见,我们将最小化扩展目前的thrift服务,以支持搜索操作。使用模型是用 `put` 把文件加入搜索引擎,其中每个文件包含了一系列的记号(词),那么我们就可以输入一串记号,然后搜索会返回包含这个串中所有记号的所有文件。该体系结构是与前面的例子相同,但增加了一个新的@search@调用。
![Searchbird implementation, revision 2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-07_55ed22e819a0f.svg "Searchbird implementation, revision 2")
要实现这样一个搜索引擎需要修改以下两个文件:
##### src/main/thrift/searchbird.thrift
~~~
service SearchbirdService {
string get(1: string key) throws(1: SearchbirdException ex)
void put(1: string key, 2: string value)
list<string> search(1: string query)
}
~~~
我们增加了一个 `search` 方法来搜索当前哈希表,返回其值与查询匹配的键列表。实现也很简单直观:
##### …/SearchbirdServiceImpl.scala
大部分修改都在这个文件中。
现在的 `数据库` HashMap保存一个正向索引来持有到文档的键映射。我们重命名它为 `forward` 并增加一个 `倒排(reverse)` 索引(映射记号到所有包含该记号的文件)。所以在 `SearchbirdServiceImpl.scala` 中,更换 `database` 定义:
~~~
val forward = new mutable.HashMap[String, String]
with mutable.SynchronizedMap[String, String]
val reverse = new mutable.HashMap[String, Set[String]]
with mutable.SynchronizedMap[String, Set[String]]
~~~
在 `get` 调用中,使用 `forward` 替换 `数据库` 即可,在其他方面 `get` 保持不变(仅执行正向查找)。不过 `put` 还需要改变:我们还需要为文件中的每个令牌填充反向索引,把文件的键附加到令牌关联的列表中。用下面的代码替换 `put` 调用。给定一个特定的搜索令牌,我们现在可以使用 `反向` 映射来查找文件。
~~~
def put(key: String, value: String) = {
log.debug("put %s", key)
forward(key) = value
// serialize updaters
synchronized {
value.split(" ").toSet foreach { token =>
val current = reverse.getOrElse(token, Set())
reverse(token) = current + key
}
}
Future.Unit
}
~~~
需要注意的是(即使 `HashMap` 是线程安全的)同时只能有一个线程可以更新 `倒排` 索引,以确保对映射条目的 读-修改-写 是一个原子操作。 (这段代码过于保守;在进行 检索-修改-写 操作时,它锁定了整个映射,而不是锁定单个条目。)。另外还要注意使用 `Set` 作为数据结构;这可以确保即使一个文件中两次出现同样的符号,它也只会被 `foreach` 循环处理一次。
这个实现仍然有一个问题,作为留给读者的一个练习:当我们用一个新文档覆盖的一个键的时候,我们诶有删除任何倒排索引中引用的旧文件。
现在进入搜索引擎的核心:新的 `search` 方法。他应该解析查询,寻找匹配的文档,然后对这些列表做相交操作。这将产生包含所有查询中的标记的文件列表。在Scala中可以很直接地表达;添加这段代码到 `SearchbirdServiceImpl` 类中:
~~~
def search(query: String) = Future.value {
val tokens = query.split(" ")
val hits = tokens map { token => reverse.getOrElse(token, Set()) }
val intersected = hits reduceLeftOption { _ & _ } getOrElse Set()
intersected.toList
}
~~~
在这段短短的代码中有几件事情是值得关注的。在构建命中列表时,如果键( `token` )没有被发现, `getOrElse` 会返回其第二个参数(在这种情况下,一个空 `Set` )。我们使用left-reduce执行实际的相交操作。特别是当 `reduceLeftOption` 发现 `hits` 为空时将不会继续尝试执行reduce操作。这使我们能够提供一个默认值,而不是抛出一个异常。其实这相当于:
~~~
def search(query: String) = Future.value {
val tokens = query.split(" ")
val hits = tokens map { token => reverse.getOrElse(token, Set()) }
if (hits.isEmpty)
Nil
else
hits reduceLeft { _ & _ } toList
}
~~~
使用哪种方式大多是个人喜好的问题,虽然函数式风格往往会避开带有合理默认值的条件语句。
现在,我们可以尝试在控制台中实验我们新的实现。重启服务器:
~~~
$ ./sbt
...
> compile
> run -f config/development.scala
...
[info] Running com.twitter.searchbird.Main -f config/development.scala
~~~
然后再从searchbird目录,启动客户端:
~~~
$ ./console 127.0.0.1 9999
...
[info] Running com.twitter.searchbird.SearchbirdConsoleClient 127.0.0.1 9999
'client' is bound to your thrift client.
finagle-client>
~~~
粘贴以下说明到控制台:
~~~
client.put("basics", " values functions classes methods inheritance try catch finally expression oriented")
client.put("basics", " case classes objects packages apply update functions are objects (uniform access principle) pattern")
client.put("collections", " lists maps functional combinators (map foreach filter zip")
client.put("pattern", " more functions! partialfunctions more pattern")
client.put("type", " basic types and type polymorphism type inference variance bounds")
client.put("advanced", " advanced types view bounds higher kinded types recursive types structural")
client.put("simple", " all about sbt the standard scala build")
client.put("more", " tour of the scala collections")
client.put("testing", " write tests with specs a bdd testing framework for")
client.put("concurrency", " runnable callable threads futures twitter")
client.put("java", " java interop using scala from")
client.put("searchbird", " building a distributed search engine using")
~~~
现在,我们可以执行一些搜索,返回包含搜索词的文件的键。
~~~
> client.search("functions").get()
res12: Seq[String] = ArrayBuffer(basics)
> client.search("java").get()
res13: Seq[String] = ArrayBuffer(java)
> client.search("java scala").get()
res14: Seq[String] = ArrayBuffer(java)
> client.search("functional").get()
res15: Seq[String] = ArrayBuffer(collections)
> client.search("sbt").get()
res16: Seq[String] = ArrayBuffer(simple)
> client.search("types").get()
res17: Seq[String] = ArrayBuffer(type, advanced)
~~~
回想一下,如果调用返回一个 `Future` ,我们必须使用一个阻塞的 `get()` 来获取其中包含的值。我们可以使用 `Future.collect` 命令来创建多个并发请求,并等待所有请求成功返回:
~~~
> import com.twitter.util.Future
...
> Future.collect(Seq(
client.search("types"),
client.search("sbt"),
client.search("functional")
)).get()
res18: Seq[Seq[String]] = ArrayBuffer(ArrayBuffer(type, advanced), ArrayBuffer(simple), ArrayBuffer(collections))
~~~
## 分发我们的服务
单台机器上一个简单的内存搜索引擎将无法搜索超过内存大小的语料库。现在,我们要大胆改进,用一个简单的分片计划来构建分布式节点。下面是框图:
![Distributed Searchbird service](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-07_55ed22e8dd75e.svg "Distributed Searchbird service")
### 抽象
为了帮助我们的工作,我们会先介绍另一个抽象 `索引` 来解耦 `SearchbirdService` 对索引实现的依赖。这是一个直观的重构。我们首先添加一个索引文件到构建 (创建文件 `searchbird/src/main/scala/com/twitter/searchbird/Index.scala` ):
##### …/Index.scala
~~~
package com.twitter.searchbird
import scala.collection.mutable
import com.twitter.util._
import com.twitter.conversions.time._
import com.twitter.logging.Logger
import com.twitter.finagle.builder.ClientBuilder
import com.twitter.finagle.thrift.ThriftClientFramedCodec
trait Index {
def get(key: String): Future[String]
def put(key: String, value: String): Future[Unit]
def search(key: String): Future[List[String]]
}
class ResidentIndex extends Index {
val log = Logger.get(getClass)
val forward = new mutable.HashMap[String, String]
with mutable.SynchronizedMap[String, String]
val reverse = new mutable.HashMap[String, Set[String]]
with mutable.SynchronizedMap[String, Set[String]]
def get(key: String) = {
forward.get(key) match {
case None =>
log.debug("get %s: miss", key)
Future.exception(SearchbirdException("No such key"))
case Some(value) =>
log.debug("get %s: hit", key)
Future(value)
}
}
def put(key: String, value: String) = {
log.debug("put %s", key)
forward(key) = value
// admit only one updater.
synchronized {
(Set() ++ value.split(" ")) foreach { token =>
val current = reverse.get(token) getOrElse Set()
reverse(token) = current + key
}
}
Future.Unit
}
def search(query: String) = Future.value {
val tokens = query.split(" ")
val hits = tokens map { token => reverse.getOrElse(token, Set()) }
val intersected = hits reduceLeftOption { _ & _ } getOrElse Set()
intersected.toList
}
}
~~~
现在,我们把thrift服务转换成一个简单的调度机制:为每一个 `索引` 实例提供一个thrift接口。这是一个强大的抽象,因为它分离了索引实现和服务实现。服务不再知道索引的任何细节;索引可以是本地的或远程的,甚至可能是许多索引的组合,但服务并不关心,索引实现可能会更改但是不用修改服务。
将 `SearchbirdServiceImpl` 类定义更换为以下(简单得多)的代码(其中不再包含索引实现细节)。注意初始化服务器现在需要第二个参数 `Index` 。
##### …/SearchbirdServiceImpl.scala
~~~
class SearchbirdServiceImpl(config: SearchbirdServiceConfig, index: Index) extends SearchbirdService.ThriftServer {
val serverName = "Searchbird"
val thriftPort = config.thriftPort
def get(key: String) = index.get(key)
def put(key: String, value: String) =
index.put(key, value) flatMap { _ => Future.Unit }
def search(query: String) = index.search(query)
def shutdown() = {
super.shutdown(0.seconds)
}
}
~~~
##### …/config/SearchbirdServiceConfig.scala
相应地更新 `SearchbirdServiceConfig` 的 `apply` 调用:
~~~
class SearchbirdServiceConfig extends ServerConfig[SearchbirdService.ThriftServer] {
var thriftPort: Int = 9999
var tracerFactory: Tracer.Factory = NullTracer.factory
def apply(runtime: RuntimeEnvironment) = new SearchbirdServiceImpl(this, new ResidentIndex)
}
~~~
我们将建立一个简单的分布式系统,一个主节点组织查询其子节点。为了实现这一目标,我们将需要两个新的 `Index` 类型。一个代表远程索引,另一种是其他多个 `Index` 实例的组合索引。这样我们的服务就可以实例化多个远程索引的复合索引来构建分布式索引。请注意这两个 `Index` 类型具有相同的接口,所以服务器不需要知道它们所连接的索引是远程的还是复合的。
##### …/Index.scala
在 `Index.scala` 中定义了 `CompositeIndex` :
~~~
class CompositeIndex(indices: Seq[Index]) extends Index {
require(!indices.isEmpty)
def get(key: String) = {
val queries = indices.map { idx =>
idx.get(key) map { r => Some(r) } handle { case e => None }
}
Future.collect(queries) flatMap { results =>
results.find { _.isDefined } map { _.get } match {
case Some(v) => Future.value(v)
case None => Future.exception(SearchbirdException("No such key"))
}
}
}
def put(key: String, value: String) =
Future.exception(SearchbirdException("put() not supported by CompositeIndex"))
def search(query: String) = {
val queries = indices.map { _.search(query) rescue { case _=> Future.value(Nil) } }
Future.collect(queries) map { results => (Set() ++ results.flatten) toList }
}
}
~~~
组合索引构建在一组相关 `Index` 实例的基础上。注意它并不关心这些实例实际上是如何实现的。这种组合类型在构建不同查询机制的时候具有极大的灵活性。我们没有定义拆分机制,所以复合索引不支持 `put` 操作。这些请求被直接交由子节点处理。 `get` 的实现是查询所有子节点,并提取第一个成功的结果。如果没有成功结果的话,则抛出一个异常。注意因为没有结果是通过抛出一个异常表示的,所以我们 `处理Future` ,是将任何异常转换成 `None` 。在实际系统中,我们很可能会为遗漏值填入适当的错误码,而不是使用异常。异常在构建原型时是方便和适宜的,但不能很好地组合。为了把真正的例外和遗漏值区分开,必须要检查异常本身。相反,把这种区别直接嵌入在返回值的类型中是更好的风格。
`search` 像以前一样工作。和提取第一个结果不同,我们把它们组合起来,通过使用 `Set` 确保其唯一性。
`RemoteIndex` 提供了到远程服务器的一个 `Index` 接口。
~~~
class RemoteIndex(hosts: String) extends Index {
val transport = ClientBuilder()
.name("remoteIndex")
.hosts(hosts)
.codec(ThriftClientFramedCodec())
.hostConnectionLimit(1)
.timeout(500.milliseconds)
.build()
val client = new SearchbirdService.FinagledClient(transport)
def get(key: String) = client.get(key)
def put(key: String, value: String) = client.put(key, value) map { _ => () }
def search(query: String) = client.search(query) map { _.toList }
}
~~~
这样就使用一些合理的默认值,调用代理,稍微调整类型,就构造出一个finagle thrift客户端。
### 全部放在一起
现在我们拥有了需要的所有功能。我们需要调整配置,以便能够调用一个给定的节点,不管是主节点亦或是数据分片节点。为了做到这一点,我们将通过创建一个新的配置项来在系统中枚举分片。我们还需要添加 `Index` 参数到我们的 `SearchbirdServiceImpl` 实例。然后,我们将使用命令行参数(还记得 `Config`是如何做到的吗)在这两种模式中启动服务器。
##### …/config/SearchbirdServiceConfig.scala
~~~
class SearchbirdServiceConfig extends ServerConfig[SearchbirdService.ThriftServer] {
var thriftPort: Int = 9999
var shards: Seq[String] = Seq()
def apply(runtime: RuntimeEnvironment) = {
val index = runtime.arguments.get("shard") match {
case Some(arg) =>
val which = arg.toInt
if (which >= shards.size || which < 0)
throw new Exception("invalid shard number %d".format(which))
// override with the shard port
val Array(_, port) = shards(which).split(":")
thriftPort = port.toInt
new ResidentIndex
case None =>
require(!shards.isEmpty)
val remotes = shards map { new RemoteIndex(_) }
new CompositeIndex(remotes)
}
new SearchbirdServiceImpl(this, index)
}
}
~~~
现在,我们将调整配置:添加“分片”初始化到 `SearchbirdServiceConfig` 的初始化中(我们可以通过端口9000访问分片0,9001访问分片1,依次类推)。
##### config/development.scala
~~~
new SearchbirdServiceConfig {
// Add your own config here
shards = Seq(
"localhost:9000",
"localhost:9001",
"localhost:9002"
)
...
~~~
注释掉 `admin.httpPort` 的设置(我们不希望在同一台机器上运行多个服务,而不注释的话这些服务都会试图打开相同的端口):
~~~
// admin.httpPort = 9900
~~~
现在,如果我们不带任何参数调用我们的服务器程序,它会启动一个主节点来和所有分片通信。如果我们指定一个分片参数,它会在指定端口启动一个分片服务器。
让我们试试吧!我们将启动3个服务:2个分片和1个主节点。首先编译改动:
~~~
$ ./sbt
> compile
...
> exit
~~~
然后启动三个服务:
~~~
$ ./sbt 'run -f config/development.scala -D shard=0'
$ ./sbt 'run -f config/development.scala -D shard=1'
$ ./sbt 'run -f config/development.scala'
~~~
您可以在3个不同的窗口中分别运行,或在同一窗口开始依次逐个运行,等待其启动后,只用ctrl-z悬挂这个命令,并使用 `bg` 将它放在后台执行。
然后,我们将通过控制台与它们进行互动。首先,让我们填充一些数据在两个分片节点。从searchbird目录运行:
~~~
$ ./console localhost 9000
...
> client.put("fromShardA", "a value from SHARD_A")
> client.put("hello", "world")
~~~
~~~
$ ./console localhost 9001
...
> client.put("fromShardB", "a value from SHARD_B")
> client.put("hello", "world again")
~~~
一旦完成就可以退出这些控制台会话。现在通过主节点查询我们的数据库(9999端口):
~~~
$ ./console localhost 9999
[info] Running com.twitter.searchbird.SearchbirdConsoleClient localhost 9999
'client' is bound to your thrift client.
finagle-client> client.get("hello").get()
res0: String = world
finagle-client> client.get("fromShardC").get()
SearchbirdException(No such key)
...
finagle-client> client.get("fromShardA").get()
res2: String = a value from SHARD_A
finagle-client> client.search("hello").get()
res3: Seq[String] = ArrayBuffer()
finagle-client> client.search("world").get()
res4: Seq[String] = ArrayBuffer(hello)
finagle-client> client.search("value").get()
res5: Seq[String] = ArrayBuffer(fromShardA, fromShardB)
~~~
这个设计有多个数据抽象,允许更加模块化和可扩展的实现:
* `ResidentIndex` 数据结构对网络、服务器或客户端一无所知。
* `CompositeIndex` 对其索引构成的底层数据结构和组合方式一无所知;它只是简单地把请求分配给他们。
* 服务器相同的 `search` 接口(特质)允许服务器查询其本地数据结构(`ResidentIndex`) ,或分发到其他服务器(`CompositeIndex`) 查询,而不需要知道这个区别,这是从调用隐藏的。
* `SearchbirdServiceImpl` 和 `Index` 现在是相互独立的模块,这使服务实现变得简单,同时实现了服务和其数据结构之间的分离。
* 这个设计灵活到允许一个或多个远程索引运行在本地机器或远程机器上。
这个实现的可能改进将包括:
* 当前的实现将 `put()` 调用发送到所有节点。取而代之,我们可以使用一个哈希表,将 `put()`调用只发送到一个节点,而在所有节点之间分配存储。
* 但是值得注意的是,在这个策略下我们失去了冗余。我们怎样在不需要完全复制的前提下保持一定的冗余度呢?
* 当系统出错时我们没有做任何有趣的处理(例如我们没有处理任何异常)。
[1](http://twitter.github.io/scala_school/zh_cn/searchbird.html#fnr1) 本地 `./sbt` 脚本只是保证该SBT版本和我们知道的所有库是一致的。
[2](http://twitter.github.io/scala_school/zh_cn/searchbird.html#fnr2) 在 `target/gen-scala/com/twitter/searchbird/SearchbirdService.scala` 。
[3](http://twitter.github.io/scala_school/zh_cn/searchbird.html#fnr3) 更多信息见Ostrich’s [README](https://github.com/twitter/ostrich/blob/master/README.md) 。
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
Finagle介绍
最后更新于:2022-04-01 03:05:24
[Finagle](https://github.com/twitter/finagle) 是 Twitter 研发的RPC系统。
[这篇博客](https://blog.twitter.com/2011/finagle-a-protocol-agnostic-rpc-system) 解释了其动机和核心设计原则, [finagle README](https://github.com/twitter/finagle/blob/master/README.md) 包含更详细的文档。Finagle的目标是方便地建立强大的客户端和服务器。
[TOC=2,3]
## Finagle-Friendly REPL
我们将要讨论的不是标准Scala的代码。如果你喜欢使用REPL学习,你可能想知道如何获得一个加入Finagle 及其依赖的 Scala REPL。
你可以在[这里](https://github.com/twitter/finagle/)获取Finagle源代码。
如果你在 `finagle`目录下有Finagle的源代码,你可以通过下面的命令得到一个控制台
~~~
$ cd finagle
$ ./sbt "project finagle-http" console
...build output...
scala>
~~~
## Futures
Finagle使用 `com.twitter.util.Future`[1](http://twitter.github.io/scala_school/zh_cn/finagle.html#fn1) 编码延迟操作。Future是尚未生成的值的一个句柄。Finagle使用Future作为其异步API的返回值。同步API会在返回前等待结果;但是异步API则不会等待。例如,个对互联网上一些服务的HTTP请求可能半秒都不会返回。你不希望你的程序阻塞等待半秒。 “慢”的API可以立即返回一个`Future`,然后在需要解析其值时“填充”。
~~~
val myFuture = MySlowService(request) // returns right away
...do other things...
val serviceResult = myFuture.get() // blocks until service "fills in" myFuture
~~~
在实践中,你不会发送一个请求,然后在几行代码后调用`myFuture.get`。Future提供了注册回调的方法,在值变得可用时会调用注册的回调函数。
如果你用过其他异步API,当看到“回调”你也许会畏缩。你可能会联想到他们难以辨认的代码流,被调用的函数藏在离调用处远远的地方。但是,Future可以利用Scala中“函数是一等公民”的特性编写出更可读的代码流。你可以在调用它的地方简单地定义一个处理函数。
例如,写代码调度请求,然后“处理”回应,你可以保持代码在一起:
~~~
val future = dispatch(req) // returns immediately, but future is "empty"
future onSuccess { reply => // when the future gets "filled", use its value
println(reply)
}
~~~
你可以在REPL中用体验一下Future。虽然不是学习如何在实际代码中使用他们的好方法,但可以帮助理解API。当你使用REPL,Promise是一个方便的类。它是Future抽象类的一个具体子类。你可以用它来创建一个还没有值的Future。
~~~
scala> import com.twitter.util.{Future,Promise}
import com.twitter.util.{Future, Promise}
scala> val f6 = Future.value(6) // create already-resolved future
f6: com.twitter.util.Future[Int] = com.twitter.util.ConstFuture@c63a8af
scala> f6.get()
res0: Int = 6
scala> val fex = Future.exception(new Exception) // create resolved sad future
fex: com.twitter.util.Future[Nothing] = com.twitter.util.ConstFuture@38ddab20
scala> fex.get()
java.lang.Exception
... stack trace ...
scala> val pr7 = new Promise[Int] // create unresolved future
pr7: com.twitter.util.Promise[Int] = Promise@1994943491(...)
scala> pr7.get()
...console hangs, waiting for future to resolve...
Ctrl-C
Execution interrupted by signal.
scala> pr7.setValue(7)
scala> pr7.get()
res1: Int = 7
scala>
~~~
在实际代码中使用Future时,你通常不会调用`get` ,而是使用回调函数。 `get`仅仅是方便在REPL修修补补。
### 顺序组合(Sequential composition)
Future有类似[集合API中的组合子](http://twitter.github.io/scala_school/zh_cn/collections.html#combinators)(如 map, flatMap) 。回顾一下集合组合子,它让你可以表达如 “我有一个整数List和一个square函数:map那个列表获得整数平方的列表”这样的操作。这种表达方式很灵巧;你可以把组合子函数和另一个函数放在一起有效地组成一个新函数。面向Future的组合子可以让你这样表达:“我有一个期望整数的Future和一个square函数:map那个Future获得一个期望整数平方的Future”。
如果你在定义一个异步API,传入一个请求值,你的API应该返回一个包装在Future中的响应。因此,这些把输入和函数加入Future的组合子是相当有用的:它们帮助你根据其它异步API定义你自己的异步API。
最重要的`Future`的组合子是`flatMap`[2](http://twitter.github.io/scala_school/zh_cn/finagle.html#fn2):
> `def Future[A].flatMap[B](f: A => Future[B]): Future[B]`
`flatMap` 序列化两个Future。即,它接受一个Future和一个异步函数,并返回另一个Future。方法签名中是这样写的:给定一个Future成功的值,函数`f`提供下一个`Future`。如果/当输入的`Future` 成功完成,`flatMap`自动调用`f`。只有当这两个Future都已完成,此操作所代表的`Future`才算完成。如果任何一个`Future`失败,则操作确定的 `Future`也将失败。这种隐交织的错误让我们只需要在必要时来处理错误,所以语法意义很大。`flatMap`是这些语义组合子的标准名称。
如果你有一个Future并且想在异步API使用其值,使用flatMap。例如,假设你有一个Future[User],需要一个Future[Boolean]表示用户是否已被禁止。有一个`isBanned` 的异步API来判断一个用户是否已被禁止。此时可以使用flatMap :
~~~
scala> import com.twitter.util.{Future,Promise}
import com.twitter.util.{Future, Promise}
scala> class User(n: String) { val name = n }
defined class User
scala> def isBanned(u: User) = { Future.value(false) }
isBanned: (u: User)com.twitter.util.Future[Boolean]
scala> val pru = new Promise[User]
pru: com.twitter.util.Promise[User] = Promise@897588993(...)
scala> val futBan = pru flatMap isBanned // apply isBanned to future
futBan: com.twitter.util.Future[Boolean] = Promise@1733189548(...)
scala> futBan.get()
...REPL hangs, futBan not resolved yet...
Ctrl-C
Execution interrupted by signal.
scala> pru.setValue(new User("prudence"))
scala> futBan.get()
res45: Boolean = false
scala>
~~~
同样,如果要在Future中应用一个*同步*函数,可以使用map。例如,假设你有一个Future[RawCredentials]需要一个Future[Credentials]。你有一个的同步的`normalize`函数将RawCredentials转换成Credentials。可以使用`map`:
~~~
scala> class RawCredentials(u: String, pw: String) {
| val username = u
| val password = pw
| }
defined class RawCredentials
scala> class Credentials(u: String, pw: String) {
| val username = u
| val password = pw
| }
defined class Credentials
scala> def normalize(raw: RawCredentials) = {
| new Credentials(raw.username.toLowerCase(), raw.password)
| }
normalize: (raw: RawCredentials)Credentials
scala> val praw = new Promise[RawCredentials]
praw: com.twitter.util.Promise[RawCredentials] = Promise@1341283926(...)
scala> val fcred = praw map normalize // apply normalize to future
fcred: com.twitter.util.Future[Credentials] = Promise@1309582018(...)
scala> fcred.get()
...REPL hangs, fcred doesn't have a value yet...
Ctrl-C
Execution interrupted by signal.
scala> praw.setValue(new RawCredentials("Florence", "nightingale"))
scala> fcred.get().username
res48: String = florence
scala>
~~~
Scala有快捷语法来调用flatMap:`for`表达式。假设你想通过异步API验证登录请求,然后通过另一个异步API检查用户是否被禁止。在for表达式的帮助下,我们可以这样写:
~~~
scala> def authenticate(req: LoginRequest) = {
| // TODO: we should check the password
| Future.value(new User(req.username))
| }
authenticate: (req: LoginRequest)com.twitter.util.Future[User]
scala> val f = for {
| u <- authenticate(request)
| b <- isBanned(u)
| } yield (u, b)
f: com.twitter.util.Future[(User, Boolean)] = Promise@35785606(...)
scala>
~~~
它产生一个`f: Future[(User, Boolean)]`,包含用户对象和一个表示该用户是否已被禁止的布尔值。注意这里是怎样实现顺序组合的:`isBanned`使用了`authenticate`的输出作为其输入。
### 并发组合
你可能想一次获取来自多个服务的数据。例如,如果你正在编写一个Web服务来显示内容和广告,它可能会从两个服务中分别获取内容和广告。但是,你怎么告诉代码来等待两份答复呢?如果必须自己实现可能会非常棘手,幸运的是你可以使用并发组合子。
`Future` 提供了一些并发组合子。一般来说,他们都是将`Future`的一个序列转换成包含一个序列的`Future`,只是方式略微不同。这是很好的,因为它(本质上)可以让你把几个Future封装成一个单一的Future。
~~~
object Future {
…
def collect[A](fs: Seq[Future[A]]): Future[Seq[A]]
def join(fs: Seq[Future[_]]): Future[Unit]
def select(fs: Seq[Future[A]]) : Future[(Try[A], Seq[Future[A]])]
}
~~~
`collect`参数是具有相同类型`Future`的一个集合,返回一个`Future`,其类型是包含那个类型值的一个序列。当所有的Future都成功完成或者当中任何一个失败,都会使这个Future完成。返回序列的顺序和传入序列的顺序相对应。
~~~
scala> val f2 = Future.value(2)
f2: com.twitter.util.Future[Int] = com.twitter.util.ConstFuture@13ecdec0
scala> val f3 = Future.value(3)
f3: com.twitter.util.Future[Int] = com.twitter.util.ConstFuture@263bb672
scala> val f23 = Future.collect(Seq(f2, f3))
f23: com.twitter.util.Future[Seq[Int]] = Promise@635209178(...)
scala> val f5 = f23 map (_.sum)
f5: com.twitter.util.Future[Int] = Promise@1954478838(...)
scala> f5.get()
res9: Int = 5
~~~
`join`参数是混合类型的`Future`序列,返回一个Future[Unit],当所有的相关Future完成时(无论他们是否失败)该Future完成。其作用是标识一组异构操作完成。对那个内容和广告的例子来说,这可能是一个很好的解决方案。
~~~
scala> val ready = Future.join(Seq(f2, f3))
ready: com.twitter.util.Future[Unit] = Promise@699347471(...)
scala> ready.get() // doesn't ret value, but I know my futures are done
scala>
~~~
当传入的`Future`序列的第一个`Future`完成的时候,`select`会返回一个`Future`。它会将那个完成的`Future`和其它未完成的Future一起放在Seq中返回。 (它不会做任何事情来取消剩余的Future。你可以等待更多的回应,或者忽略他们)
~~~
scala> val pr7 = new Promise[Int] // unresolved future
pr7: com.twitter.util.Promise[Int] = Promise@1608532943(...)
scala> val sel = Future.select(Seq(f2, pr7)) // select from 2 futs, one resolved
sel: com.twitter.util.Future[...] = Promise@1003382737(...)
scala> val(complete, stragglers) = sel.get()
complete: com.twitter.util.Try[Int] = Return(2)
stragglers: Seq[...] = List(...)
scala> complete.get()
res110: Int = 2
scala> stragglers(0).get() // our list of not-yet-finished futures has one item
...get() hangs the REPL because this straggling future is not finished...
Ctrl-C
Execution interrupted by signal.
scala> pr7.setValue(7)
scala> stragglers(0).get()
res113: Int = 7
scala>
~~~
### 组合例子:缓存速率限制
这些组合子表达了典型的网络服务操作。这段假设的代码在对速率进行限制(为了保持本地速率限制缓存)的同时,将用户的请求调度到后台服务:
~~~
// Find out if user is rate-limited. This can be slow; we have to ask
// the remote server that keeps track of who is rate-limited.
def isRateLimited(u: User): Future[Boolean] = {
...
}
// Notice how you can swap this implementation out now with something that might
// implement a different, more restrictive policy.
// Check the cache to find out if user is rate-limited. This cache
// implementation is just a Map, and can return a value right way. But we
// return a Future anyhow in case we need to use a slower implementation later.
def isLimitedByCache(u: User): Future[Boolean] = Future.value(limitCache(u))
// Update the cache
def setIsLimitedInCache(user: User, v: Boolean) { limitCache(user) = v }
// Get a timeline of tweets... unless the user is rate-limited (then throw
// an exception instead)
def getTimeline(cred: Credentials): Future[Timeline] =
isLimitedByCache(cred.user) flatMap {
case true => Future.exception(new Exception("rate limited"))
case false =>
// First we get auth'd user then we get timeline.
// Sequential composition of asynchronous APIs: use flatMap
val timeline = auth(cred) flatMap(getTimeline)
val limited = isRateLimited(cred.user) onSuccess(
setIsLimitedInCache(cred.user, _))
// 'join' concurrently combines differently-typed futures
// 'flatMap' sequentially combines, specifies what to do next
timeline join limited flatMap {
case (_, true) => Future.exception(new Exception("rate limited"))
case (timeline, _) => Future.value(timeline)
}
}
}
~~~
这个例子结合了顺序和并发组合。请注意,除了给转化速率限制回应一个异常以外,没有明确的错误处理。如果任何Future在这里失败,它会自动传播到返回的`Future`中。
### 组合例子:网络爬虫
你已经看到了怎样使用Future组合子的例子,不过也许意犹未尽。假设你有一个简单的互联网模型。该互联网中只有HTML网页和图片,其中页面可以链接到图像和其他网页。你可以获取一个页面或图像,但API是异步的。这个假设的API成这些“可获取”的数据为资源:
~~~
import com.twitter.util.{Try,Future,Promise}
// a fetchable thing
trait Resource {
def imageLinks(): Seq[String]
def links(): Seq[String]
}
// HTML pages can link to Imgs and to other HTML pages.
class HTMLPage(val i: Seq[String], val l: Seq[String]) extends Resource {
def imageLinks() = i
def links = l
}
// IMGs don't actually link to anything else
class Img() extends Resource {
def imageLinks() = Seq()
def links() = Seq()
}
// profile.html links to gallery.html and has an image link to portrait.jpg
val profile = new HTMLPage(Seq("portrait.jpg"), Seq("gallery.html"))
val portrait = new Img
// gallery.html links to profile.html and two images
val gallery = new HTMLPage(Seq("kitten.jpg", "puppy.jpg"), Seq("profile.html"))
val kitten = new Img
val puppy = new Img
val internet = Map(
"profile.html" -> profile,
"gallery.html" -> gallery,
"portrait.jpg" -> portrait,
"kitten.jpg" -> kitten,
"puppy.jpg" -> puppy
)
// fetch(url) attempts to fetch a resource from our fake internet.
// Its returned Future might contain a Resource or an exception
def fetch(url: String) = { new Promise(Try(internet(url))) }
~~~
**顺序组合**
假设给定一个页面URL,而你希望获取该页面的第一个图。也许你正在做一个网站,在上面用户可以发布有趣的网页链接。为了帮助其他用户决定某个链接是否值得追踪,你打算显示那个链接中第一张图像的缩略图。
即使你不知道组合子,你仍然可以写一个缩略图获取函数:
~~~
def getThumbnail(url: String): Future[Resource]={
val returnVal = new Promise[Resource]
fetch(url) onSuccess { page => // callback for successful page fetch
fetch(page.imageLinks()(0)) onSuccess { p => // callback for successful img fetch
returnVal.setValue(p)
} onFailure { exc => // callback for failed img fetch
returnVal.setException(exc)
}
} onFailure { exc => // callback for failed page fetch
returnVal.setException(exc)
}
returnVal
}
~~~
这个版本的函数能工作。它的大部分内容用来解析Future,然后把他们的内容传给另一个Future。
我们希望得到一个页面,然后从该页面获得一个图像。如果你想获得A,然后再获得B的,这通常意味着顺序组合。由于B是异步的,所以需要使用flatMap:
~~~
def getThumbnail(url: String): Future[Resource] =
fetch(url) flatMap { page => fetch(page.imageLinks()(0)) }
~~~
**…通过并发组合**
抓取页面的第一个图片是好的,但也许我们应该获取所有图片,并让用户自己进行选择。我们可以使用`for`循环一个个地抓取,但这需要很长时间;所以我们想并行获取它们。如果你想的事情“并行”发生,这通常意味着并发组合。所以我们使用Future.collect的提取所有的图像:
~~~
def getThumbnails(url:String): Future[Seq[Resource]] =
fetch(url) flatMap { page =>
Future.collect(
page.imageLinks map { u => fetch(u) }
)
}
~~~
如果这对你有意义,那太好了。你可能会看不懂这行代码 `page.imageLinks map { u => fetch(u) }`:它使用`map`和`map`后的函数返回一个Future。当接下来的事情是返回一个Future时,我们不是应该使用flatMap吗?但是请注意,在`map`*前*的不是一个Future;它是一个集合。collection map function返回一个集合;我们使用Future.collect收集Future的集合到一个Future中。
**并发 + 递归**
除了页面中的图片以外,我们可能会想获取它链接的其他页面。通过递归我们可以构建一个简单的网络爬虫。
~~~
// Return
def crawl(url: String): Future[Seq[Resource]] =
fetch(url) flatMap { page =>
Future.collect(
page.links map { u => crawl(u) }
) map { pps => pps.flatten }
}
crawl("profile.html")
...hangs REPL, infinite loop...
Ctrl-C
Execution interrupted by signal.
scala>
// She's gone rogue, captain! Have to take her out!
// Calling Thread.stop on runaway Thread[Thread-93,5,main] with offending code:
// scala> crawl("profile.html")
~~~
在实践中,这个网络爬虫不是很有用:首先我们没有告诉它何时停止爬行;其次即使资源刚刚被获取过,它仍然会不厌其烦地重新获取。
## 服务
一个Finagle `服务`用来处理RPC,读取请求并给予回复的。服务是针对请求和回应的一个函数`Req => Future[Rep]`。
> `abstract class Service[-Req, +Rep] extends (Req => Future[Rep])`
![Client and Server](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-07_55ed22af19c27.png "Client and Server")
在服务中,我们要同时定义客户端和服务器。
一个Finagle客户端“引入”一个网络服务。从概念上讲,Finagle客户端由两部分组成
* 一个*使用*服务的函数:分发一个 `Req`并处理 `Future[Rep]`
* 配置怎样分发这些请求;例如,作为HTTP请求发送到`api.twitter.com`的80端口
同样,Finagle服务端“输出”网络服务。一个服务端由两个部分组成:
* 一个*实现*服务的函数:传入一个`Req` 并返回一个`Future[Rep]`
* 配置如何“监听”输入的 Reqs;例如,在80端口的HTTP请求。
这种设计分离了服务的“业务逻辑”和数据如何在网络中流动的配置。
![Filter and Server](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-07_55ed22afee2ac.png "Filter and Server")
我们也谈论Finagle“过滤器”。过滤器在服务之间,修改流经它的数据。过滤器可以很好地和服务组合在一起。例如,如果你有一个速率限制过滤器和一个tweet服务,你可以把它们组合在一起形成有速率限制的tweet服务。
## 客户端
一个Finagle客户端“引入”一个网络服务。它有一些配置来设定如何在网络上发送数据。一个简单的HTTP客户端可能看起来像这样:
~~~
import org.jboss.netty.handler.codec.http.{DefaultHttpRequest, HttpRequest, HttpResponse, HttpVersion, HttpMethod}
import com.twitter.finagle.Service
import com.twitter.finagle.builder.ClientBuilder
import com.twitter.finagle.http.Http
// Don't worry, we discuss this magic "ClientBuilder" later
val client: Service[HttpRequest, HttpResponse] = ClientBuilder()
.codec(Http())
.hosts("twitter.com:80") // If >1 host, client does simple load-balancing
.hostConnectionLimit(1)
.build()
val req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")
val f = client(req) // Client, send the request
// Handle the response:
f onSuccess { res =>
println("got response", res)
} onFailure { exc =>
println("failed :-(", exc)
}
~~~
## 服务端
一个服务端按服务进行定义,并配置如何“监听”网络上的请求。一个简单的HTTP服务端可能看起来像这样:
~~~
import com.twitter.finagle.Service
import com.twitter.finagle.http.Http
import com.twitter.util.Future
import org.jboss.netty.handler.codec.http.{DefaultHttpResponse, HttpVersion, HttpResponseStatus, HttpRequest, HttpResponse}
import java.net.{SocketAddress, InetSocketAddress}
import com.twitter.finagle.builder.{Server, ServerBuilder}
import com.twitter.finagle.builder.ServerBuilder
// Define our service: OK response for root, 404 for other paths
val rootService = new Service[HttpRequest, HttpResponse] {
def apply(request: HttpRequest) = {
val r = request.getUri match {
case "/" => new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK)
case _ => new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND)
}
Future.value(r)
}
}
// Serve our service on a port
val address: SocketAddress = new InetSocketAddress(10000)
val server: Server = ServerBuilder()
.codec(Http())
.bindTo(address)
.name("HttpServer")
.build(rootService)
~~~
这个`name`是我们强加的,虽然没有在例子中使用它,但这个字段对分析和调试是很有用的。
## 过滤器
过滤器改造服务,它们可以提供*通用的服务*功能。例如你有几个服务需要支持速率限制,这时可以写一个限速过滤器并将其应用于所有的服务就解决问题了。过滤器也可以将服务分解成不同的阶段。
一个简单的代理可能看起来像这样:
~~~
class MyService(client: Service[..]) extends Service[HttpRequest, HttpResponse]
{
def apply(request: HttpRequest) = {
client(rewriteReq(request)) map { res =>
rewriteRes(res)
}
}
}
~~~
其中`rewriteReq` 和 `rewriteRes`可以提供协议翻译,例如。
~~~
abstract class Filter[-ReqIn, +RepOut, +ReqOut, -RepIn]
extends ((ReqIn, Service[ReqOut, RepIn]) => Future[RepOut])
~~~
通过图示可以更清晰地看出其类型:
~~~
((ReqIn, Service[ReqOut, RepIn])
=> Future[RepOut])
(* Service *)
[ReqIn -> (ReqOut -> RepIn) -> RepOut]
~~~
下面的例子展示了怎样通过过滤器来提供服务超时机制。
~~~
class TimeoutFilter[Req, Rep](
timeout: Duration,
exception: RequestTimeoutException,
timer: Timer)
extends Filter[Req, Rep, Req, Rep]
{
def this(timeout: Duration, timer: Timer) =
this(timeout, new IndividualRequestTimeoutException(timeout), timer)
def apply(request: Req, service: Service[Req, Rep]): Future[Rep] = {
val res = service(request)
res.within(timer, timeout) rescue {
case _: java.util.concurrent.TimeoutException =>
res.cancel()
Trace.record(TimeoutFilter.TimeoutAnnotation)
Future.exception(exception)
}
}
}
~~~
这个例子展示了怎样(通过认证服务)提供身份验证来将 `Service[AuthHttpReq, HttpRep]` 转换为 `Service[HttpReq, HttpRep]`。
~~~
class RequireAuthentication(authService: AuthService)
extends Filter[HttpReq, HttpRep, AuthHttpReq, HttpRep] {
def apply(
req: HttpReq,
service: Service[AuthHttpReq, HttpRep]
) = {
authService.auth(req) flatMap {
case AuthResult(AuthResultCode.OK, Some(passport), _) =>
service(AuthHttpReq(req, passport))
case ar: AuthResult =>
Future.exception(
new RequestUnauthenticated(ar.resultCode))
}
}
}
~~~
这样使用过滤器是有好处的。它可以帮助你将“身份验证逻辑”固定在一个地方。拥有一个独立的类型执行请求授权,会使追查程序安全问题变得更容易。
过滤器可以使用 `andThen` 组合在一起。传入一个`Service`参数给`andThen` 将创建一个(添加了过滤功能)的`Service`(类型用来做说明)。
~~~
val authFilter: Filter[HttpReq, HttpRep, AuthHttpReq, HttpRep]
val timeoutfilter[Req, Rep]: Filter[Req, Rep, Req, Rep]
val serviceRequiringAuth: Service[AuthHttpReq, HttpRep]
val authenticateAndTimedOut: Filter[HttpReq, HttpRep, AuthHttpReq, HttpRep] =
authFilter andThen timeoutFilter
val authenticatedTimedOutService: Service[HttpReq, HttpRep] =
authenticateAndTimedOut andThen serviceRequiringAuth
~~~
## 生成器(Builder)
生成器把所有组件组合在一起。一个`ClientBuilder`对给定的一组参数生成一个`Service`,而一个 `ServerBuilder` 获取一个 `Service` 的实例,并调度传入请求给它。为了确定`Service`的类型,我们必须提供一个`编解码器(Codec)`。编解码器提供底层协议的实现(如HTTP,thrift,memcached)。这两个Builder都有很多参数,其中一些是必填的。
下面是一个调用`ClientBuilder`的例子(类型用来做说明)
~~~
val client: Service[HttpRequest, HttpResponse] = ClientBuilder()
.codec(Http)
.hosts("host1.twitter.com:10000,host2.twitter.com:10001,host3.twitter.com:10003")
.hostConnectionLimit(1)
.tcpConnectTimeout(1.second)
.retries(2)
.reportTo(new OstrichStatsReceiver)
.build()
~~~
这将构建一个客户端在三个主机上进行负载平衡,最多在每台主机建立一个连接,并在两次失败尝试后放弃。统计数据会报给 [ostrich](https://github.com/twitter/ostrich) 。以下生成器选项是必须的(而且它们也被静态强制填写了):`hosts` 或 `cluster`, `codec` 和 `hostConnectionLimit`。
同样的,你也可以使用一个`ServerBuilder`来创建“监听”传入请求的服务:
~~~
val service = new MyService(...) // construct instance of your Finagle service
var filter = new MyFilter(...) // and maybe some filters
var filteredServce = filter andThen service
val server = ServerBuilder()
.bindTo(new InetSocketAddress(port))
.codec(ThriftServerFramedCodec())
.name("my filtered service")
// .hostConnectionMaxLifeTime(5.minutes)
// .readTimeout(2.minutes)
.build(filteredService)
~~~
通过这些参数会生成一个Thrift服务器监听端口port,并将请求分发给service。如果我们去掉`hostConnectionMaxLifeTime`的注释,每个连接将被允许留存长达5分钟。如果我们去掉`readTimeout`的注释,那么我们就需要在2分钟之内发送请求。`ServerBuilder`必选项有:`name`, `bindTo` 和 `codec`。
## 不要阻塞(除非你用正确的方式)
Finagle 自动操纵线程来保证服务顺利运行。但是,如果你的服务阻塞了,它会阻塞所有Finagle线程。
* 如果你的代码调用了一个阻塞操作(`apply` 或 `get`),使用[Future 池](https://github.com/twitter/finagle#Using%20Future%20Pools)来包装阻塞代码。阻塞操作将运行在自己的线程池中,返回一个Future来完成(或失败)这个操作,并可以和其它Future组合。
* 如果你的代码中使用Future的顺序组合,不用担心它会“阻塞”组合中的Future。
[1](http://twitter.github.io/scala_school/zh_cn/finagle.html#fnr1) 小心,还有其它“Future”类。不要将`com.twitter.util.Future` 和`scala.actor.Future` 或 `java.util.concurrent.Future`混淆起来!
[2](http://twitter.github.io/scala_school/zh_cn/finagle.html#fnr2) 如果你学习类型系统和/或分类理论,你会高兴地发现`flatMap`相当于一元绑定。
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
Java + Scala
最后更新于:2022-04-01 03:05:21
课程内容涵盖了Java互操作性。
[TOC=2,2]
## Javap
javap的是JDK附带的一个工具。不是JRE,这里是有区别的。 javap反编译类定义,给你展示里面有什么。用法很简单
~~~
[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTrait
Compiled from "Scalaisms.scala"
public interface com.twitter.interop.MyTrait extends scala.ScalaObject{
public abstract java.lang.String traitName();
public abstract java.lang.String upperTraitName();
}
~~~
如果你是底层控可以看看字节码
~~~
[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap -c MyTrait\$class
Compiled from "Scalaisms.scala"
public abstract class com.twitter.interop.MyTrait$class extends java.lang.Object{
public static java.lang.String upperTraitName(com.twitter.interop.MyTrait);
Code:
0: aload_0
1: invokeinterface #12, 1; //InterfaceMethod com/twitter/interop/MyTrait.traitName:()Ljava/lang/String;
6: invokevirtual #17; //Method java/lang/String.toUpperCase:()Ljava/lang/String;
9: areturn
public static void $init$(com.twitter.interop.MyTrait);
Code:
0: return
}
~~~
如果你搞不清为什么程序在Java上不起作用,就用javap看看吧!
## 类
在Java中使用Scala *类* 时要考虑的四个要点
* 类参数
* 类常量
* 类变量
* 异常
我们将构建一个简单的Scala类来展示这一系列实体
~~~
package com.twitter.interop
import java.io.IOException
import scala.throws
import scala.reflect.{BeanProperty, BooleanBeanProperty}
class SimpleClass(name: String, val acc: String, @BeanProperty var mutable: String) {
val foo = "foo"
var bar = "bar"
@BeanProperty
val fooBean = "foobean"
@BeanProperty
var barBean = "barbean"
@BooleanBeanProperty
var awesome = true
def dangerFoo() = {
throw new IOException("SURPRISE!")
}
@throws(classOf[IOException])
def dangerBar() = {
throw new IOException("NO SURPRISE!")
}
}
~~~
### 类参数
* 默认情况下,类参数都是有效的Java构造函数的参数。这意味着你不能从类的外部访问。
* 声明一个类参数为val/var 和这段代码是相同的
~~~
class SimpleClass(acc_: String) {
val acc = acc_
}
~~~
这使得它在Java代码中就像其他常量一样可以被访问
### 类常量
* 常量(val)在Java中定义了一个获取方法。你可以通过方法“foo()”访问“val foo”的值
### 类变量
* 变量(var)会生成一个 _$eq 方法。你可以这样调用它
~~~
foo$_eq("newfoo");
~~~
### BeanProperty
你可以通过@BeanProperty注解val和var定义。这会按照POJO定义生成getter/setter方法。如果你想生成isFoo方法,使用BooleanBeanProperty注解。丑陋的foo$_eq将变为
~~~
setFoo("newfoo");
getFoo();
~~~
### 异常
Scala没有像java那样有受检异常(checked exception)。需不需要受检异常是一个我们不会进入的哲学辩论,不过当你需要在Java中捕获它时就 **很重要** 了。dangerFoo和dangerBar将演示这一点。在Java中不能这样做
~~~
// exception erasure!
try {
s.dangerFoo();
} catch (IOException e) {
// UGLY
}
~~~
Java会抱怨说 s.dangerFoo从未抛出过 IOException异常。我们可以通过捕获Throwable来跳过,但是这样不好。
相反,作为一个良好的Scala公民,可以很体面地像在dangerBar中那样使用throws注解。这使我们能够继续在Java中使用受检异常。
### 进一步阅读
支持Java互操作的Scala注解的完整列表在这里 [http://www.scala-lang.org/node/106](http://www.scala-lang.org/node/106)。
## 特质
你如何获得一个接口+实现?让我们看一个简单的特质定义
~~~
trait MyTrait {
def traitName:String
def upperTraitName = traitName.toUpperCase
}
~~~
这个特质有一个抽象方法(traitName)和一个实现的方法(upperTraitName)。Scala为我们生成了什么呢?一个名为MyTrait的的接口,和一个名为MyTrait$class的实现类。
MyTrait和你期望的一样
~~~
[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTrait
Compiled from "Scalaisms.scala"
public interface com.twitter.interop.MyTrait extends scala.ScalaObject{
public abstract java.lang.String traitName();
public abstract java.lang.String upperTraitName();
}
~~~
MyTrait$class更有趣
~~~
[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTrait\$class
Compiled from "Scalaisms.scala"
public abstract class com.twitter.interop.MyTrait$class extends java.lang.Object{
public static java.lang.String upperTraitName(com.twitter.interop.MyTrait);
public static void $init$(com.twitter.interop.MyTrait);
}
~~~
MyTrait$class只有以MyTrait实例为参数的静态方法。这给了我们一个如何在Java中来扩展一个特质的提示。
首先尝试下面的操作
~~~
package com.twitter.interop;
public class JTraitImpl implements MyTrait {
private String name = null;
public JTraitImpl(String name) {
this.name = name;
}
public String traitName() {
return name;
}
}
~~~
我们会得到以下错误
~~~
[info] Compiling main sources...
[error] /Users/mmcbride/projects/interop/src/main/java/com/twitter/interop/JTraitImpl.java:3: com.twitter.interop.JTraitImpl is not abstract and does not override abstract method upperTraitName() in com.twitter.interop.MyTrait
[error] public class JTraitImpl implements MyTrait {
[error] ^
~~~
我们 *可以* 自己实现。但有一个鬼鬼祟祟的方式。
~~~
package com.twitter.interop;
public String upperTraitName() {
return MyTrait$class.upperTraitName(this);
}
~~~
我们只要把调用代理到生成的Scala实现上就可以了。如果愿意我们也可以覆盖它。
## 单例对象
单例对象是Scala实现静态方法/单例模式的方式。在Java中使用它会有点奇怪。没有一个使用它们的完美风格,但在Scala2.8中用起来并不很糟糕
一个Scala单例对象会被编译成由“$”结尾的类。让我们创建一个类和一个伴生对象
~~~
class TraitImpl(name: String) extends MyTrait {
def traitName = name
}
object TraitImpl {
def apply = new TraitImpl("foo")
def apply(name: String) = new TraitImpl(name)
}
~~~
我们可以像这样天真地在Java中访问
~~~
MyTrait foo = TraitImpl$.MODULE$.apply("foo");
~~~
现在你可能会问自己,这是神马玩意?这是一个正常的反应。让我们来看看TraitImpl$里面实际上是什么
~~~
local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap TraitImpl\$
Compiled from "Scalaisms.scala"
public final class com.twitter.interop.TraitImpl$ extends java.lang.Object implements scala.ScalaObject{
public static final com.twitter.interop.TraitImpl$ MODULE$;
public static {};
public com.twitter.interop.TraitImpl apply();
public com.twitter.interop.TraitImpl apply(java.lang.String);
}
~~~
其实它里面没有任何静态方法。取而代之的是一个名为MODULE$的静态成员。方法实现被委托给该成员。这使得访问代码很难看,但却是可行的。
### 转发方法(Forwarding Methods)
在Scala2.8中处理单例对象变得相对容易一点。如果你有一个类与一个伴生对象,2.8编译器会生成转发方法在伴生类中。所以,如果你用2.8,你可以像这样调用TraitImpl单例对象的方法
~~~
MyTrait foo = TraitImpl.apply("foo");
~~~
## 闭包函数
Scala的最重要的特点之一,就是把函数作为头等公民。让我们来定义一个类,它定义了一些以函数作为参数的方法。
~~~
class ClosureClass {
def printResult[T](f: => T) = {
println(f)
}
def printResult[T](f: String => T) = {
println(f("HI THERE"))
}
}
~~~
在Scala中可以像这样调用
~~~
val cc = new ClosureClass
cc.printResult { "HI MOM" }
~~~
在Java中却不那么容易,不过也并不可怕。让我们来看看ClosureClass实际上被编译成什么:
~~~
[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap ClosureClass
Compiled from "Scalaisms.scala"
public class com.twitter.interop.ClosureClass extends java.lang.Object implements scala.ScalaObject{
public void printResult(scala.Function0);
public void printResult(scala.Function1);
public com.twitter.interop.ClosureClass();
}
~~~
这也不是那么恐怖。 “f: => T”被转义成“Function0”,“f: String => T”被转义成“Function1”。Scala实际上从Function0定义到Function22,最多支持22个参数。这真的应该足够了。
现在我们只需要弄清楚如何在Java中使用这些东东。我们可以传入Scala提供的AbstractFunction0和AbstractFunction1,像这样
~~~
@Test public void closureTest() {
ClosureClass c = new ClosureClass();
c.printResult(new AbstractFunction0() {
public String apply() {
return "foo";
}
});
c.printResult(new AbstractFunction1<String, String>() {
public String apply(String arg) {
return arg + "foo";
}
});
}
~~~
注意我们可以使用泛型参数。
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
Scala 并发编程
最后更新于:2022-04-01 03:05:19
[TOC=2,2]
## Runnable/Callable
Runnable接口只有一个没有返回值的方法。
~~~
trait Runnable {
def run(): Unit
}
~~~
Callable与之类似,除了它有一个返回值
~~~
trait Callable[V] {
def call(): V
}
~~~
## 线程
Scala并发是建立在Java并发模型基础上的。
在Sun JVM上,对IO密集的任务,我们可以在一台机器运行成千上万个线程。
一个线程需要一个Runnable。你必须调用线程的 `start` 方法来运行Runnable。
~~~
scala> val hello = new Thread(new Runnable {
def run() {
println("hello world")
}
})
hello: java.lang.Thread = Thread[Thread-3,5,main]
scala> hello.start
hello world
~~~
当你看到一个类实现了Runnable接口,你就知道它的目的是运行在一个线程中。
### 单线程代码
这里有一个可以工作但有问题的代码片断。
~~~
import java.net.{Socket, ServerSocket}
import java.util.concurrent.{Executors, ExecutorService}
import java.util.Date
class NetworkService(port: Int, poolSize: Int) extends Runnable {
val serverSocket = new ServerSocket(port)
def run() {
while (true) {
// This will block until a connection comes in.
val socket = serverSocket.accept()
(new Handler(socket)).run()
}
}
}
class Handler(socket: Socket) extends Runnable {
def message = (Thread.currentThread.getName() + "\n").getBytes
def run() {
socket.getOutputStream.write(message)
socket.getOutputStream.close()
}
}
(new NetworkService(2020, 2)).run
~~~
每个请求都会回应当前线程的名称,所以结果始终是 `main` 。
这段代码的主要缺点是在同一时间,只有一个请求可以被相应!
你可以把每个请求放入一个线程中处理。只要简单改变
~~~
(new Handler(socket)).run()
~~~
为
~~~
(new Thread(new Handler(socket))).start()
~~~
但如果你想重用线程或者对线程的行为有其他策略呢?
## Executors
随着Java 5的发布,它决定提供一个针对线程的更抽象的接口。
你可以通过 `Executors` 对象的静态方法得到一个 `ExecutorService` 对象。这些方法为你提供了可以通过各种政策配置的 `ExecutorService` ,如线程池。
下面改写我们之前的阻塞式网络服务器来允许并发请求。
~~~
import java.net.{Socket, ServerSocket}
import java.util.concurrent.{Executors, ExecutorService}
import java.util.Date
class NetworkService(port: Int, poolSize: Int) extends Runnable {
val serverSocket = new ServerSocket(port)
val pool: ExecutorService = Executors.newFixedThreadPool(poolSize)
def run() {
try {
while (true) {
// This will block until a connection comes in.
val socket = serverSocket.accept()
pool.execute(new Handler(socket))
}
} finally {
pool.shutdown()
}
}
}
class Handler(socket: Socket) extends Runnable {
def message = (Thread.currentThread.getName() + "\n").getBytes
def run() {
socket.getOutputStream.write(message)
socket.getOutputStream.close()
}
}
(new NetworkService(2020, 2)).run
~~~
这里有一个连接脚本展示了内部线程是如何重用的。
~~~
$ nc localhost 2020
pool-1-thread-1
$ nc localhost 2020
pool-1-thread-2
$ nc localhost 2020
pool-1-thread-1
$ nc localhost 2020
pool-1-thread-2
~~~
## Futures
`Future` 代表异步计算。你可以把你的计算包装在Future中,当你需要计算结果的时候,你只需调用一个阻塞的 `get()` 方法就可以了。一个 `Executor` 返回一个 `Future` 。如果使用Finagle RPC系统,你可以使用 `Future` 实例持有可能尚未到达的结果。
一个 `FutureTask` 是一个Runnable实现,就是被设计为由 `Executor` 运行的
~~~
val future = new FutureTask[String](new Callable[String]() {
def call(): String = {
searcher.search(target);
}})
executor.execute(future)
~~~
现在我需要结果,所以阻塞直到其完成。
~~~
val blockingResult = future.get()
~~~
**参考** [Scala School的Finagle介绍](http://twitter.github.io/scala_school/zh_cn/finagle.html)中大量使用了`Future`,包括一些把它们结合起来的不错的方法。以及 Effective Scala 对[Futures](http://twitter.github.com/effectivescala/#Twitter's standard libraries-Futures)的意见。
## 线程安全问题
~~~
class Person(var name: String) {
def set(changedName: String) {
name = changedName
}
}
~~~
这个程序在多线程环境中是不安全的。如果有两个线程有引用到同一个Person实例,并调用 `set` ,你不能预测两个调用结束后 `name` 的结果。
在Java内存模型中,允许每个处理器把值缓存在L1或L2缓存中,所以在不同处理器上运行的两个线程都可以有自己的数据视图。
让我们来讨论一些工具,来使线程保持一致的数据视图。
### 三种工具
#### 同步
互斥锁(Mutex)提供所有权语义。当你进入一个互斥体,你拥有它。同步是JVM中使用互斥锁最常见的方式。在这个例子中,我们会同步Person。
在JVM中,你可以同步任何不为null的实例。
~~~
class Person(var name: String) {
def set(changedName: String) {
this.synchronized {
name = changedName
}
}
}
~~~
#### volatile
随着Java 5内存模型的变化,volatile和synchronized基本上是相同的,除了volatile允许空值。
`synchronized` 允许更细粒度的锁。 而 `volatile` 则对每次访问同步。
~~~
class Person(@volatile var name: String) {
def set(changedName: String) {
name = changedName
}
}
~~~
#### AtomicReference
此外,在Java 5中还添加了一系列低级别的并发原语。 `AtomicReference` 类是其中之一
~~~
import java.util.concurrent.atomic.AtomicReference
class Person(val name: AtomicReference[String]) {
def set(changedName: String) {
name.set(changedName)
}
}
~~~
#### 这个成本是什么?
`AtomicReference` 是这两种选择中最昂贵的,因为你必须去通过方法调度(method dispatch)来访问值。
`volatile` 和 `synchronized` 是建立在Java的内置监视器基础上的。如果没有资源争用,监视器的成本很小。由于 `synchronized` 允许你进行更细粒度的控制权,从而会有更少的争夺,所以 `synchronized` 往往是最好的选择。
当你进入同步点,访问volatile引用,或去掉AtomicReferences引用时, Java会强制处理器刷新其缓存线从而提供了一致的数据视图。
如果我错了,请大家指正。这是一个复杂的课题,我敢肯定要弄清楚这一点需要一个漫长的课堂讨论。
### Java5的其他灵巧的工具
正如前面提到的 `AtomicReference` ,Java5带来了许多很棒的工具。
#### CountDownLatch
`CountDownLatch` 是一个简单的多线程互相通信的机制。
~~~
val doneSignal = new CountDownLatch(2)
doAsyncWork(1)
doAsyncWork(2)
doneSignal.await()
println("both workers finished!")
~~~
先不说别的,这是一个优秀的单元测试。比方说,你正在做一些异步工作,并要确保功能完成。你的函数只需要 `倒数计数(countDown)` 并在测试中 `等待(await)` 就可以了。
#### AtomicInteger/Long
由于对Int和Long递增是一个经常用到的任务,所以增加了 `AtomicInteger` 和 `AtomicLong` 。
#### AtomicBoolean
我可能不需要解释这是什么。
#### ReadWriteLocks
`读写锁(ReadWriteLock)` 使你拥有了读线程和写线程的锁控制。当写线程获取锁的时候读线程只能等待。
## 让我们构建一个不安全的搜索引擎
下面是一个简单的倒排索引,它不是线程安全的。我们的倒排索引按名字映射到一个给定的用户。
这里的代码天真地假设只有单个线程来访问。
注意使用了 `mutable.HashMap` 替代了默认的构造函数 `this()`
~~~
import scala.collection.mutable
case class User(name: String, id: Int)
class InvertedIndex(val userMap: mutable.Map[String, User]) {
def this() = this(new mutable.HashMap[String, User])
def tokenizeName(name: String): Seq[String] = {
name.split(" ").map(_.toLowerCase)
}
def add(term: String, user: User) {
userMap += term -> user
}
def add(user: User) {
tokenizeName(user.name).foreach { term =>
add(term, user)
}
}
}
~~~
这里没有写如何从索引中获取用户。稍后我们会补充。
## 让我们把它变为线程安全
在上面的倒排索引例子中,userMap不能保证是线程安全的。多个客户端可以同时尝试添加项目,并有可能出现前面 `Person` 例子中的视图错误。
由于userMap不是线程安全的,那我们怎样保持在同一个时间只有一个线程能改变它呢?
你可能会考虑在做添加操作时锁定userMap。
~~~
def add(user: User) {
userMap.synchronized {
tokenizeName(user.name).foreach { term =>
add(term, user)
}
}
}
~~~
不幸的是,这个粒度太粗了。一定要试图在互斥锁以外做尽可能多的耗时的工作。还记得我说过如果不存在资源争夺,锁开销就会很小吗。如果在锁代码块里面做的工作越少,争夺就会越少。
~~~
def add(user: User) {
// tokenizeName was measured to be the most expensive operation.
val tokens = tokenizeName(user.name)
tokens.foreach { term =>
userMap.synchronized {
add(term, user)
}
}
}
~~~
## SynchronizedMap
我们可以通过SynchronizedMap特质将同步混入一个可变的HashMap。
我们可以扩展现有的InvertedIndex,提供给用户一个简单的方式来构建同步索引。
~~~
import scala.collection.mutable.SynchronizedMap
class SynchronizedInvertedIndex(userMap: mutable.Map[String, User]) extends InvertedIndex(userMap) {
def this() = this(new mutable.HashMap[String, User] with SynchronizedMap[String, User])
}
~~~
如果你看一下其实现,你就会意识到,它只是在每个方法上加同步锁来保证其安全性,所以它很可能没有你希望的性能。
## Java ConcurrentHashMap
Java有一个很好的线程安全的ConcurrentHashMap。值得庆幸的是,我们可以通过JavaConverters获得不错的Scala语义。
事实上,我们可以通过扩展老的不安全的代码,来无缝地接入新的线程安全InvertedIndex。
~~~
import java.util.concurrent.ConcurrentHashMap
import scala.collection.JavaConverters._
class ConcurrentInvertedIndex(userMap: collection.mutable.ConcurrentMap[String, User])
extends InvertedIndex(userMap) {
def this() = this(new ConcurrentHashMap[String, User] asScala)
}
~~~
## 让我们加载InvertedIndex
### 原始方式
~~~
trait UserMaker {
def makeUser(line: String) = line.split(",") match {
case Array(name, userid) => User(name, userid.trim().toInt)
}
}
class FileRecordProducer(path: String) extends UserMaker {
def run() {
Source.fromFile(path, "utf-8").getLines.foreach { line =>
index.add(makeUser(line))
}
}
}
~~~
对于文件中的每一行,我们可以调用 `makeUser` 然后 `add` 到 InvertedIndex中。如果我们使用并发InvertedIndex,我们可以并行调用add因为makeUser没有副作用,所以我们的代码已经是线程安全的了。
我们不能并行读取文件,但我们 *可以* 并行构造用户并且把它添加到索引中。
### 一个解决方案:生产者/消费者
异步计算的一个常见模式是把消费者和生产者分开,让他们只能通过 `队列(Queue)` 沟通。让我们看看如何将这个模式应用在我们的搜索引擎索引中。
~~~
import java.util.concurrent.{BlockingQueue, LinkedBlockingQueue}
// Concrete producer
class Producer[T](path: String, queue: BlockingQueue[T]) extends Runnable {
def run() {
Source.fromFile(path, "utf-8").getLines.foreach { line =>
queue.put(line)
}
}
}
// Abstract consumer
abstract class Consumer[T](queue: BlockingQueue[T]) extends Runnable {
def run() {
while (true) {
val item = queue.take()
consume(item)
}
}
def consume(x: T)
}
val queue = new LinkedBlockingQueue[String]()
// One thread for the producer
val producer = new Producer[String]("users.txt", q)
new Thread(producer).start()
trait UserMaker {
def makeUser(line: String) = line.split(",") match {
case Array(name, userid) => User(name, userid.trim().toInt)
}
}
class IndexerConsumer(index: InvertedIndex, queue: BlockingQueue[String]) extends Consumer[String](queue) with UserMaker {
def consume(t: String) = index.add(makeUser(t))
}
// Let's pretend we have 8 cores on this machine.
val cores = 8
val pool = Executors.newFixedThreadPool(cores)
// Submit one consumer per core.
for (i <- i to cores) {
pool.submit(new IndexerConsumer[String](index, q))
}
~~~
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
使用specs进行测试
最后更新于:2022-04-01 03:05:17
本节课将介绍如何使用specs —— 一个Scala行为驱动设计(BDD)框架,来进行测试。
[TOC=2,2]
## 扩展规格
让我们直接开始。
~~~
import org.specs._
object ArithmeticSpec extends Specification {
"Arithmetic" should {
"add two numbers" in {
1 + 1 mustEqual 2
}
"add three numbers" in {
1 + 1 + 1 mustEqual 3
}
}
}
~~~
**Arithmetic(算术)** 是一个 **规范约束下的系统**
**add(加)** 是一个上下文。
**add two numbers(两个数相加)**,和 **add three numbers(三个数字相加)** 是例子。
`mustEqual` 表示 **预期**
`1 mustEqual 1` 是编写实际测试前使用的一种常见的 **预期** 占位符。所有的测试用例都应该至少有一个预期。
### 复制
注意到两个测试都是怎样将 `add` 加在其名称中的吗?我们可以通过 **嵌套** 预期摆脱这种重复。
~~~
import org.specs._
object ArithmeticSpec extends Specification {
"Arithmetic" should {
"add" in {
"two numbers" in {
1 + 1 mustEqual 2
}
"three numbers" in {
1 + 1 + 1 mustEqual 3
}
}
}
}
~~~
## 执行模型
~~~
object ExecSpec extends Specification {
"Mutations are isolated" should {
var x = 0
"x equals 1 if we set it." in {
x = 1
x mustEqual 1
}
"x is the default value if we don't change it" in {
x mustEqual 0
}
}
}
~~~
## Setup, Teardown
### doBefore & doAfter
~~~
"my system" should {
doBefore { resetTheSystem() /** user-defined reset function */ }
"mess up the system" in {...}
"and again" in {...}
doAfter { cleanThingsUp() }
}
~~~
**注意** `doBefore`/`doAfter` 只能运行在叶子用例上。
### doFirst & doLast
`doFirst`/`doLast` 用来做一次性的设置。(需要例子,我不使用这个)
~~~
"Foo" should {
doFirst { openTheCurtains() }
"test stateless methods" in {...}
"test other stateless methods" in {...}
doLast { closeTheCurtains() }
}
~~~
## Matchers
你有数据,并且想要确保它是正确的。让我们看看最常用的匹配器是如何帮助你的。 (参考 [匹配器指南](http://code.google.com/p/specs/wiki/MatchersGuide) )
### mustEqual
我们已经看到几个mustEqual的例子了。
~~~
1 mustEqual 1
"a" mustEqual "a"
~~~
引用相等,值相等。
### 序列中的元素
~~~
val numbers = List(1, 2, 3)
numbers must contain(1)
numbers must not contain(4)
numbers must containAll(List(1, 2, 3))
numbers must containInOrder(List(1, 2, 3))
List(1, List(2, 3, List(4)), 5) must haveTheSameElementsAs(List(5, List(List(4), 2, 3), 1))
~~~
### 映射中的元素
~~~
map must haveKey(k)
map must notHaveKey(k)
map must haveValue(v)
map must notHaveValue(v)
~~~
### 数字
~~~
a must beGreaterThan(b)
a must beGreaterThanOrEqualTo(b)
a must beLessThan(b)
a must beLessThanOrEqualTo(b)
a must beCloseTo(b, delta)
~~~
### Options
~~~
a must beNone
a must beSome[Type]
a must beSomething
a must beSome(value)
~~~
### throwA
~~~
a must throwA[WhateverException]
~~~
这是一个针对try\catch块中有异常抛出的用例的简写。
您也可以期望一个特定的消息
~~~
a must throwA(WhateverException("message"))
~~~
您也可以匹配异常:
~~~
a must throwA(new Exception) like {
case Exception(m) => m.startsWith("bad")
}
~~~
### 编写你自己的匹配器
~~~
import org.specs.matcher.Matcher
~~~
#### 作为一个不变量
~~~
"A matcher" should {
"be created as a val" in {
val beEven = new Matcher[Int] {
def apply(n: => Int) = {
(n % 2 == 0, "%d is even".format(n), "%d is odd".format(n))
}
}
2 must beEven
}
}
~~~
契约是返回一个包含三个值的元组,分别是期望是否为真、为真时的消息和为假时的消息。
#### 作为一个样本类
~~~
case class beEven(b: Int) extends Matcher[Int]() {
def apply(n: => Int) = (n % 2 == 0, "%d is even".format(n), "%d is odd".format(n))
}
~~~
使用样本类可以增加代码的重用性。
## Mocks
~~~
import org.specs.Specification
import org.specs.mock.Mockito
class Foo[T] {
def get(i: Int): T
}
object MockExampleSpec extends Specification with Mockito {
val m = mock[Foo[String]]
m.get(0) returns "one"
m.get(0)
there was one(m).get(0)
there was no(m).get(1)
}
~~~
**参考** [Using Mockito](http://code.google.com/p/specs/wiki/UsingMockito)
## Spies
Spies(间谍)可以对真正的对象做一些“局部mocking”:
~~~
val list = new LinkedList[String]
val spiedList = spy(list)
// methods can be stubbed on a spy
spiedList.size returns 100
// other methods can also be used
spiedList.add("one")
spiedList.add("two")
// and verification can happen on a spy
there was one(spiedList).add("one")
~~~
然而,使用间谍可能会出现非常诡异的情况:
~~~
// if the list is empty, this will throws an IndexOutOfBoundsException
spiedList.get(0) returns "one"
~~~
这里必须使用 `doReturn` :
~~~
doReturn("one").when(spiedList).get(0)
~~~
## 在sbt中运行单个specs
~~~
> test-only com.twitter.yourservice.UserSpec
~~~
将只运行那个规范。
~~~
> ~ test-only com.twitter.yourservice.UserSpec
~~~
将在一个循环中运行该测试,文件的每一次修改都将触发测试运行。
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
更多集合
最后更新于:2022-04-01 03:05:14
Scala提供了一套很好的集合实现,提供了一些集合类型的抽象。这让你的代码可以与`Foo`的集合交互,而无需担心该集合是是一个`List`,还是`Set`,或是任何你有的类型。
[这里](http://www.decodified.com/scala/collections-api.xml) 提供了一个很好的页面来查看各种集合的默认实现,并链接到他们的scala在线文档。
[TOC=2,2]
## 基础知识
### 表 List
标准的链表。
~~~
scala> List(1, 2, 3)
res0: List[Int] = List(1, 2, 3)
~~~
你可以用函数式语言的方式连接它们。
~~~
scala> 1 :: 2 :: 3 :: Nil
res1: List[Int] = List(1, 2, 3)
~~~
**参考** [API文档](http://www.scala-lang.org/api/current/scala/collection/immutable/List.html)
### 集 Set
集没有重复
~~~
scala> Set(1, 1, 2)
res2: scala.collection.immutable.Set[Int] = Set(1, 2)
~~~
**参考** [API文档](http://www.scala-lang.org/api/current/scala/collection/immutable/Set.html)
### 序列 Seq
序列有一个给定的顺序。
~~~
scala> Seq(1, 1, 2)
res3: Seq[Int] = List(1, 1, 2)
~~~
(请注意返回的是一个列表。因为`Seq`是一个特质;而列表是序列的很好实现。如你所见,`Seq`也是一个工厂单例对象,可以用来创建列表。)
**参考** [API文档](http://www.scala-lang.org/api/current/scala/collection/Seq.html)
### 映射 Map
映射是键值容器。
~~~
scala> Map('a' -> 1, 'b' -> 2)
res4: scala.collection.immutable.Map[Char,Int] = Map((a,1), (b,2))
~~~
**参考** [API文档](http://www.scala-lang.org/api/current/scala/collection/immutable/Map.html)
## 层次结构
下面介绍的都是特质,它们在可变的(mutable)和不可变的(immutable)的包中都有特定实现。
### Traversable
所有集合都可以被遍历。这个特质定义了标准函数组合子。 这些组合子根据 `foreach` 来写,所有集合必须实现。
**参考** [API文档](http://www.scala-lang.org/api/current/scala/collection/Traversable.html)
### Iterable
`iterator()`方法返回一个Iterator来迭代元素。
**参考** [API文档](http://www.scala-lang.org/api/current/scala/collection/Iterable.html)
### Seq 序列
有顺序的对象序列。
**参考** [API文档](http://www.scala-lang.org/api/current/scala/collection/Seq.html)
### Set 集
没有重复的对象集合。
**参考** [API文档](http://www.scala-lang.org/api/current/scala/collection/immutable/Set.html)
### Map
键值对。
**参考** [API文档](http://www.scala-lang.org/api/current/scala/collection/immutable/Map.html)
## 方法
### Traversable
下面所有方法在子类中都是可用的。参数和返回值的类型可能会因为子类的覆盖而看起来不同。
~~~
def head : A
def tail : Traversable[A]
~~~
这里是函数组合子定义的地方。
`def map [B] (f: (A) => B) : CC[B]`
返回每个元素都被 `f` 转化的集合
`def foreach[U](f: Elem => U): Unit`
在集合中的每个元素上执行 `f` 。
`def find (p: (A) => Boolean) : Option[A]`
返回匹配谓词函数的第一个元素
`def filter (p: (A) => Boolean) : Traversable[A]`
返回所有匹配谓词函数的元素集合
划分:
`def partition (p: (A) ⇒ Boolean) : (Traversable[A], Traversable[A])`
按照谓词函数把一个集合分割成两部分
`def groupBy [K] (f: (A) => K) : Map[K, Traversable[A]]`
转换:
有趣的是,你可以转换集合类型。
~~~
def toArray : Array[A]
def toArray [B >: A] (implicit arg0: ClassManifest[B]) : Array[B]
def toBuffer [B >: A] : Buffer[B]
def toIndexedSeq [B >: A] : IndexedSeq[B]
def toIterable : Iterable[A]
def toIterator : Iterator[A]
def toList : List[A]
def toMap [T, U] (implicit ev: <:<[A, (T, U)]) : Map[T, U]
def toSeq : Seq[A]
def toSet [B >: A] : Set[B]
def toStream : Stream[A]
def toString () : String
def toTraversable : Traversable[A]
~~~
把映射转换为一个数组,您会得到一个键值对的数组。
~~~
scala> Map(1 -> 2).toArray
res41: Array[(Int, Int)] = Array((1,2))
~~~
### Iterable
添加一个迭代器的访问。
~~~
def iterator: Iterator[A]
~~~
一个迭代器能给你提供什么?
~~~
def hasNext(): Boolean
def next(): A
~~~
这是非常Java式的。你通常不会看到在Scala中使用迭代器,通常更容易出现的是函数组合器或for循环的使用。
### Set
~~~
def contains(key: A): Boolean
def +(elem: A): Set[A]
def -(elem: A): Set[A]
~~~
### Map
通过键查找的键值对的序列。
可以像这样将一个键值对列表传入apply()
~~~
scala> Map("a" -> 1, "b" -> 2)
res0: scala.collection.immutable.Map[java.lang.String,Int] = Map((a,1), (b,2))
~~~
或者像这样:
~~~
scala> Map(("a", 2), ("b", 2))
res0: scala.collection.immutable.Map[java.lang.String,Int] = Map((a,2), (b,2))
~~~
###### 题外话
什么是`->`?这不是特殊的语法,这是一个返回元组的方法。
~~~
scala> "a" -> 2
res0: (java.lang.String, Int) = (a,2)
~~~
请记住,这仅仅是下面代码的语法糖
~~~
scala> "a".->(2)
res1: (java.lang.String, Int) = (a,2)
~~~
您也可以使用`++`操作符构建
~~~
scala> Map.empty ++ List(("a", 1), ("b", 2), ("c", 3))
res0: scala.collection.immutable.Map[java.lang.String,Int] = Map((a,1), (b,2), (c,3))
~~~
### 常用的子类
**HashSet和HashMap** 的快速查找,这些集合的最常用的形式。 [HashSet API](http://www.scala-lang.org/api/current/scala/collection/immutable/HashSet.html), [HashMap API](http://www.scala-lang.org/api/current/scala/collection/immutable/HashMap.html)
**TreeMap** 是SortedMap的一个子类,它可以让你进行有序访问。 [TreeMap API](http://www.scala-lang.org/api/current/scala/collection/immutable/TreeMap.html)
**Vector** 快速随机选择和快速更新。 [Vector API](http://www.scala-lang.org/api/current/scala/collection/immutable/Vector.html)
~~~
scala> IndexedSeq(1, 2, 3)
res0: IndexedSeq[Int] = Vector(1, 2, 3)
~~~
**Range** 等间隔的Int有序序列。你经常会在for循环看到。 [Range API](http://www.scala-lang.org/api/current/scala/collection/immutable/Range.html)
~~~
scala> for (i <- 1 to 3) { println(i) }
1
2
3
~~~
Ranges支持标准的函数组合子。
~~~
scala> (1 to 3).map { i => i }
res0: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3)
~~~
### 默认实现
使用特质的apply方法会给你默认实现的实例,例如,Iterable(1, 2)会返回一个列表作为其默认实现。
~~~
scala> Iterable(1, 2)
res0: Iterable[Int] = List(1, 2)
~~~
序列Seq也是一样的,正如我们前面所看到的
~~~
scala> Seq(1, 2)
res3: Seq[Int] = List(1, 2)
scala> Iterable(1, 2)
res1: Iterable[Int] = List(1, 2)
scala> Sequence(1, 2)
warning: there were deprecation warnings; re-run with -deprecation for details
res2: Seq[Int] = List(1, 2)
~~~
Set
~~~
scala> Set(1, 2)
res31: scala.collection.immutable.Set[Int] = Set(1, 2)
~~~
### 一些描述性的特质
**IndexedSeq** 快速随机访问元素和一个快速的长度操作。"API 文档":http://www.scala-lang.org/api/current/scala/collection/IndexedSeq.html
**LinearSeq** 通过head快速访问第一个元素,也有一个快速的tail操作。 [API文档](http://www.scala-lang.org/api/current/scala/collection/LinearSeq.html)
#### 可变 vs 不可变
不可变
优点
* 在多线程中不会改变
缺点
* 一点也不能改变
Scala允许我们是务实的,它鼓励不变性,但不惩罚我们需要的可变性。这和var vs. val非常相似。我们总是先从val开始并在必要时回退为var。
我们赞成使用不可改变的版本的集合,但如果性能使然,也可以切换到可变的。使用不可变集合意味着你在多线程不会意外地改变事物。
## 可变集合
前面讨论的所有类都是不可变的。让我们来讨论常用的可变集合。
**HashMap** 定义了 `getOrElseUpdate`, `+=` [HashMap API](http://www.scala-lang.org/api/current/scala/collection/mutable/HashMap.html)
~~~
scala> val numbers = collection.mutable.Map(1 -> 2)
numbers: scala.collection.mutable.Map[Int,Int] = Map((1,2))
scala> numbers.get(1)
res0: Option[Int] = Some(2)
scala> numbers.getOrElseUpdate(2, 3)
res54: Int = 3
scala> numbers
res55: scala.collection.mutable.Map[Int,Int] = Map((2,3), (1,2))
scala> numbers += (4 -> 1)
res56: numbers.type = Map((2,3), (4,1), (1,2))
~~~
**ListBuffer和ArrayBuffer** 定义 `+=` [ListBuffer API](http://www.scala-lang.org/api/current/scala/collection/mutable/ListBuffer.html), [ArrayBuffer API](http://www.scala-lang.org/api/current/scala/collection/mutable/ArrayBuffer.html)
**LinkedList and DoubleLinkedList** [LinkedList API](http://www.scala-lang.org/api/current/scala/collection/mutable/LinkedList.html), [DoubleLinkedList API](http://www.scala-lang.org/api/current/scala/collection/mutable/DoubleLinkedList.html)
**LinkedList和DoubleLinkedList** [LinkedList API](http://www.scala-lang.org/api/current/scala/collection/mutable/LinkedList.html), [DoubleLinkedList API](http://www.scala-lang.org/api/current/scala/collection/mutable/DoubleLinkedList.html)
**PriorityQueue** [API 文档](http://www.scala-lang.org/api/current/scala/collection/mutable/PriorityQueue.html)
**Stack 和 ArrayStack** [Stack API](http://www.scala-lang.org/api/current/scala/collection/mutable/Stack.html), [ArrayStack API](http://www.scala-lang.org/api/current/scala/collection/mutable/ArrayStack.html)
**StringBuilder** 有趣的是,StringBuilder的是一个集合。 [API文档](http://www.scala-lang.org/api/current/scala/collection/mutable/StringBuilder.html)
## 与Java生活
您可以通过[JavaConverters package](http://www.scala-lang.org/api/current/index.html#scala.collection.JavaConverters$)轻松地在Java和Scala的集合类型之间转换。它用`asScala` 装饰常用的Java集合以和用`asJava` 方法装饰Scala集合。
~~~
import scala.collection.JavaConverters._
val sl = new scala.collection.mutable.ListBuffer[Int]
val jl : java.util.List[Int] = sl.asJava
val sl2 : scala.collection.mutable.Buffer[Int] = jl.asScala
assert(sl eq sl2)
~~~
双向转换:
~~~
scala.collection.Iterable <=> java.lang.Iterable
scala.collection.Iterable <=> java.util.Collection
scala.collection.Iterator <=> java.util.{ Iterator, Enumeration }
scala.collection.mutable.Buffer <=> java.util.List
scala.collection.mutable.Set <=> java.util.Set
scala.collection.mutable.Map <=> java.util.{ Map, Dictionary }
scala.collection.mutable.ConcurrentMap <=> java.util.concurrent.ConcurrentMap
~~~
此外,也提供了以下单向转换
~~~
scala.collection.Seq => java.util.List
scala.collection.mutable.Seq => java.util.List
scala.collection.Set => java.util.Set
scala.collection.Map => java.util.Map
~~~
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
简单构建工具
最后更新于:2022-04-01 03:05:12
这堂课将概述SBT!具体议题包括:
[TOC=2,2]
## 关于SBT
SBT是一个现代化的构建工具。虽然它由Scala编写并提供了很多Scala便利,但它是一个通用的构建工具。
## 为什么选择SBT?
* 明智的依赖管理
* 使用Ivy做依赖管理
* “只在请求时更新”的模型
* 对创建任务全面的Scala语言支持
* 连续执行命令
* 在项目上下文内启动解释器
## 入门
**译注** 最新的SBT安装方式请参考 [scala-sbt的文档](http://www.scala-sbt.org/release/docs/Getting-Started/Setup.html)
* 下载jar包 [地址](http://code.google.com/p/simple-build-tool/downloads/list)
* 创建一个调用这个jar的SBT shell脚本,例如
~~~
java -Xmx512M -jar sbt-launch.jar "$@"
~~~
* 确保它是可执行的,并在你的path下
* 运行sbt来创建项目
~~~
[local ~/projects]$ sbt
Project does not exist, create new project? (y/N/s) y
Name: sample
Organization: com.twitter
Version [1.0]: 1.0-SNAPSHOT
Scala version [2.7.7]: 2.8.1
sbt version [0.7.4]:
Getting Scala 2.7.7 ...
:: retrieving :: org.scala-tools.sbt#boot-scala
confs: [default]
2 artifacts copied, 0 already retrieved (9911kB/221ms)
Getting org.scala-tools.sbt sbt_2.7.7 0.7.4 ...
:: retrieving :: org.scala-tools.sbt#boot-app
confs: [default]
15 artifacts copied, 0 already retrieved (4096kB/167ms)
[success] Successfully initialized directory structure.
Getting Scala 2.8.1 ...
:: retrieving :: org.scala-tools.sbt#boot-scala
confs: [default]
2 artifacts copied, 0 already retrieved (15118kB/386ms)
[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1
[info] using sbt.DefaultProject with sbt 0.7.4 and Scala 2.7.7
>
~~~
可以看到它已经以较好的形式创建了项目的快照版本。
## 项目布局
* 项目 – 项目定义文件
* project/build/.scala – 主项目定义文件
* project/build.properties – 项目、sbt和Scala版本定义
* src/main – 你的应用程序代码出现在这里,在子目录表明代码的语言(如src/main/scala, src/main/java)
* src/main/resources – 你想要添加到jar包中的静态文件(如日志配置)
* src/test – 就像src/main,不过是对测试
* lib_managed – 你的项目依赖的jar文件。由sbt update时填充
* target – 生成物的目标路径(如自动生成的thrift代码,类文件,jar包)
## 添加一些代码
我们将为简单的tweet消息创建一个简单的JSON解析器。将以下代码加在这个文件中
src/main/scala/com/twitter/sample/SimpleParser.scala
~~~
package com.twitter.sample
case class SimpleParsed(id: Long, text: String)
class SimpleParser {
val tweetRegex = "\"id\":(.*),\"text\":\"(.*)\"".r
def parse(str: String) = {
tweetRegex.findFirstMatchIn(str) match {
case Some(m) => {
val id = str.substring(m.start(1), m.end(1)).toInt
val text = str.substring(m.start(2), m.end(2))
Some(SimpleParsed(id, text))
}
case _ => None
}
}
}
~~~
这段代码丑陋并有bug,但应该能够编译通过。
## 在控制台中的测试
SBT既可以用作命令行脚本,也可以作为构建控制台。我们将主要利用它作为构建控制台,不过大多数命令可以作为参数传递给SBT独立运行,如
~~~
sbt test
~~~
需要注意如果一个命令需要参数,你需要使用引号包括住整个参数路径,例如
~~~
sbt 'test-only com.twitter.sample.SampleSpec'
~~~
这种方式很奇怪。
不管怎样,要开始我们的代码工作了,启动SBT吧
~~~
[local ~/projects/sbt-sample]$ sbt
[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1
[info] using sbt.DefaultProject with sbt 0.7.4 and Scala 2.7.7
>
~~~
SBT允许你启动一个Scala REPL并加载所有项目依赖。它会在启动控制台前编译项目的源代码,从而为我们提供一个快速测试解析器的工作台。
~~~
> console
[info]
[info] == compile ==
[info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.
[info] Compiling main sources...
[info] Nothing to compile.
[info] Post-analysis: 3 classes.
[info] == compile ==
[info]
[info] == copy-test-resources ==
[info] == copy-test-resources ==
[info]
[info] == test-compile ==
[info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.
[info] Compiling test sources...
[info] Nothing to compile.
[info] Post-analysis: 0 classes.
[info] == test-compile ==
[info]
[info] == copy-resources ==
[info] == copy-resources ==
[info]
[info] == console ==
[info] Starting scala interpreter...
[info]
Welcome to Scala version 2.8.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_22).
Type in expressions to have them evaluated.
Type :help for more information.
scala>
~~~
我们代码编译通过了,并提供了典型的Scala提示符。我们将创建一个新的解析器,一个tweet以确保其“能工作”
~~~
scala> import com.twitter.sample._
import com.twitter.sample._
scala> val tweet = """{"id":1,"text":"foo"}"""
tweet: java.lang.String = {"id":1,"text":"foo"}
scala> val parser = new SimpleParser
parser: com.twitter.sample.SimpleParser = com.twitter.sample.SimpleParser@71060c3e
scala> parser.parse(tweet)
res0: Option[com.twitter.sample.SimpleParsed] = Some(SimpleParsed(1,"foo"}))
scala>
~~~
## 添加依赖
我们简单的解析器对这个非常小的输入集工作正常,但我们需要添加更多的测试并让它出错。第一步是在我们的项目中添加specs测试库和一个真正的JSON解析器。要做到这一点,我们必须超越默认的SBT项目布局来创建一个项目。
SBT认为project/build目录中的Scala文件是项目定义。添加以下内容到这个文件中project/build/SampleProject.scala
~~~
import sbt._
class SampleProject(info: ProjectInfo) extends DefaultProject(info) {
val jackson = "org.codehaus.jackson" % "jackson-core-asl" % "1.6.1"
val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5" % "test"
}
~~~
一个项目定义是一个SBT类。在上面例子中,我们扩展了SBT的DefaultProject。
这里是通过val声明依赖。SBT使用反射来扫描项目中的所有val依赖,并在构建时建立依赖关系树。这里使用的语法可能是新的,但本质和Maven依赖是相同的
~~~
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.scala-tools.testing</groupId>
<artifactId>specs_2.8.0</artifactId>
<version>1.6.5</version>
<scope>test</scope>
</dependency>
~~~
现在可以下载我们的项目依赖了。在命令行中(而不是sbt console中)运行sbt update
~~~
[local ~/projects/sbt-sample]$ sbt update
[info] Building project sample 1.0-SNAPSHOT against Scala 2.8.1
[info] using SampleProject with sbt 0.7.4 and Scala 2.7.7
[info]
[info] == update ==
[info] :: retrieving :: com.twitter#sample_2.8.1 [sync]
[info] confs: [compile, runtime, test, provided, system, optional, sources, javadoc]
[info] 1 artifacts copied, 0 already retrieved (2785kB/71ms)
[info] == update ==
[success] Successful.
[info]
[info] Total time: 1 s, completed Nov 24, 2010 8:47:26 AM
[info]
[info] Total session time: 2 s, completed Nov 24, 2010 8:47:26 AM
[success] Build completed successfully.
~~~
你会看到sbt检索到specs库。现在还增加了一个lib_managed目录,并且在lib_managed/scala_2.8.1/test目录中包含 specs_2.8.0-1.6.5.jar
## 添加测试
现在有了测试库,可以把下面的测试代码写入src/test/scala/com/twitter/sample/SimpleParserSpec.scala文件
~~~
package com.twitter.sample
import org.specs._
object SimpleParserSpec extends Specification {
"SimpleParser" should {
val parser = new SimpleParser()
"work with basic tweet" in {
val tweet = """{"id":1,"text":"foo"}"""
parser.parse(tweet) match {
case Some(parsed) => {
parsed.text must be_==("foo")
parsed.id must be_==(1)
}
case _ => fail("didn't parse tweet")
}
}
}
}
~~~
在SBT控制台中运行test
~~~
> test
[info]
[info] == compile ==
[info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.
[info] Compiling main sources...
[info] Nothing to compile.
[info] Post-analysis: 3 classes.
[info] == compile ==
[info]
[info] == test-compile ==
[info] Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.
[info] Compiling test sources...
[info] Nothing to compile.
[info] Post-analysis: 10 classes.
[info] == test-compile ==
[info]
[info] == copy-test-resources ==
[info] == copy-test-resources ==
[info]
[info] == copy-resources ==
[info] == copy-resources ==
[info]
[info] == test-start ==
[info] == test-start ==
[info]
[info] == com.twitter.sample.SimpleParserSpec ==
[info] SimpleParserSpec
[info] SimpleParser should
[info] + work with basic tweet
[info] == com.twitter.sample.SimpleParserSpec ==
[info]
[info] == test-complete ==
[info] == test-complete ==
[info]
[info] == test-finish ==
[info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0
[info]
[info] All tests PASSED.
[info] == test-finish ==
[info]
[info] == test-cleanup ==
[info] == test-cleanup ==
[info]
[info] == test ==
[info] == test ==
[success] Successful.
[info]
[info] Total time: 0 s, completed Nov 24, 2010 8:54:45 AM
>
~~~
我们的测试通过了!现在,我们可以增加更多。运行触发动作是SBT提供的优秀特性之一。在动作开始添加一个波浪线会启动一个循环,在源文件发生变化时重新运行动作。让我们运行 ~test 并看看会发生什么吧。
~~~
[info] == test ==
[success] Successful.
[info]
[info] Total time: 0 s, completed Nov 24, 2010 8:55:50 AM
1\. Waiting for source changes... (press enter to interrupt)
~~~
现在,让我们添加下面的测试案例
~~~
"reject a non-JSON tweet" in {
val tweet = """"id":1,"text":"foo""""
parser.parse(tweet) match {
case Some(parsed) => fail("didn't reject a non-JSON tweet")
case e => e must be_==(None)
}
}
"ignore nested content" in {
val tweet = """{"id":1,"text":"foo","nested":{"id":2}}"""
parser.parse(tweet) match {
case Some(parsed) => {
parsed.text must be_==("foo")
parsed.id must be_==(1)
}
case _ => fail("didn't parse tweet")
}
}
"fail on partial content" in {
val tweet = """{"id":1}"""
parser.parse(tweet) match {
case Some(parsed) => fail("didn't reject a partial tweet")
case e => e must be_==(None)
}
}
~~~
在我们保存文件后,SBT会检测到变化,运行测试,并通知我们的解析器有问题
~~~
[info] == com.twitter.sample.SimpleParserSpec ==
[info] SimpleParserSpec
[info] SimpleParser should
[info] + work with basic tweet
[info] x reject a non-JSON tweet
[info] didn't reject a non-JSON tweet (Specification.scala:43)
[info] x ignore nested content
[info] 'foo","nested":{"id' is not equal to 'foo' (SimpleParserSpec.scala:31)
[info] + fail on partial content
~~~
因此,让我们返工实现真正的JSON解析器
~~~
package com.twitter.sample
import org.codehaus.jackson._
import org.codehaus.jackson.JsonToken._
case class SimpleParsed(id: Long, text: String)
class SimpleParser {
val parserFactory = new JsonFactory()
def parse(str: String) = {
val parser = parserFactory.createJsonParser(str)
if (parser.nextToken() == START_OBJECT) {
var token = parser.nextToken()
var textOpt:Option[String] = None
var idOpt:Option[Long] = None
while(token != null) {
if (token == FIELD_NAME) {
parser.getCurrentName() match {
case "text" => {
parser.nextToken()
textOpt = Some(parser.getText())
}
case "id" => {
parser.nextToken()
idOpt = Some(parser.getLongValue())
}
case _ => // noop
}
}
token = parser.nextToken()
}
if (textOpt.isDefined && idOpt.isDefined) {
Some(SimpleParsed(idOpt.get, textOpt.get))
} else {
None
}
} else {
None
}
}
}
~~~
这是一个简单的Jackson解析器。当我们保存,SBT会重新编译代码和运行测试。代码变得越来越好了!
~~~
info] SimpleParser should
[info] + work with basic tweet
[info] + reject a non-JSON tweet
[info] x ignore nested content
[info] '2' is not equal to '1' (SimpleParserSpec.scala:32)
[info] + fail on partial content
[info] == com.twitter.sample.SimpleParserSpec ==
~~~
哦。我们需要检查嵌套对象。让我们在token读取循环处添加一些丑陋的守卫。
~~~
def parse(str: String) = {
val parser = parserFactory.createJsonParser(str)
var nested = 0
if (parser.nextToken() == START_OBJECT) {
var token = parser.nextToken()
var textOpt:Option[String] = None
var idOpt:Option[Long] = None
while(token != null) {
if (token == FIELD_NAME && nested == 0) {
parser.getCurrentName() match {
case "text" => {
parser.nextToken()
textOpt = Some(parser.getText())
}
case "id" => {
parser.nextToken()
idOpt = Some(parser.getLongValue())
}
case _ => // noop
}
} else if (token == START_OBJECT) {
nested += 1
} else if (token == END_OBJECT) {
nested -= 1
}
token = parser.nextToken()
}
if (textOpt.isDefined && idOpt.isDefined) {
Some(SimpleParsed(idOpt.get, textOpt.get))
} else {
None
}
} else {
None
}
}
~~~
…测试通过了!
## 打包和发布
现在我们已经可以运行package命令来生成一个jar文件。不过我们可能要与其他组分享我们的jar包。要做到这一点,我们将在StandardProject基础上构建,这给了我们一个良好的开端。
第一步是引入StandardProject为SBT插件。插件是一种为你的构建引进依赖的方式,注意不是为你的项目引入。这些依赖关系定义在project/plugins/Plugins.scala文件中。添加以下代码到Plugins.scala文件中。
~~~
import sbt._
class Plugins(info: ProjectInfo) extends PluginDefinition(info) {
val twitterMaven = "twitter.com" at "http://maven.twttr.com/"
val defaultProject = "com.twitter" % "standard-project" % "0.7.14"
}
~~~
注意我们指定了一个Maven仓库和一个依赖。这是因为这个标准项目库是由twitter托管的,不在SBT默认检查的仓库中。
我们也将更新项目定义来扩展StandardProject,包括SVN发布特质,和我们希望发布的仓库定义。修改SampleProject.scala
~~~
import sbt._
import com.twitter.sbt._
class SampleProject(info: ProjectInfo) extends StandardProject(info) with SubversionPublisher {
val jackson = "org.codehaus.jackson" % "jackson-core-asl" % "1.6.1"
val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5" % "test"
override def subversionRepository = Some("http://svn.local.twitter.com/maven/")
}
~~~
现在如果我们运行发布操作,将看到以下输出
~~~
[info] == deliver ==
IvySvn Build-Version: null
IvySvn Build-DateTime: null
[info] :: delivering :: com.twitter#sample;1.0-SNAPSHOT :: 1.0-SNAPSHOT :: release :: Wed Nov 24 10:26:45 PST 2010
[info] delivering ivy file to /Users/mmcbride/projects/sbt-sample/target/ivy-1.0-SNAPSHOT.xml
[info] == deliver ==
[info]
[info] == make-pom ==
[info] Wrote /Users/mmcbride/projects/sbt-sample/target/sample-1.0-SNAPSHOT.pom
[info] == make-pom ==
[info]
[info] == publish ==
[info] :: publishing :: com.twitter#sample
[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.jar
[info] published sample to com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.jar
[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.pom
[info] published sample to com/twitter/sample/1.0-SNAPSHOT/sample-1.0-SNAPSHOT.pom
[info] Scheduling publish to http://svn.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/ivy-1.0-SNAPSHOT.xml
[info] published ivy to com/twitter/sample/1.0-SNAPSHOT/ivy-1.0-SNAPSHOT.xml
[info] Binary diff deleting com/twitter/sample/1.0-SNAPSHOT
[info] Commit finished r977 by 'mmcbride' at Wed Nov 24 10:26:47 PST 2010
[info] Copying from com/twitter/sample/.upload to com/twitter/sample/1.0-SNAPSHOT
[info] Binary diff finished : r978 by 'mmcbride' at Wed Nov 24 10:26:47 PST 2010
[info] == publish ==
[success] Successful.
[info]
[info] Total time: 4 s, completed Nov 24, 2010 10:26:47 AM
~~~
这样(一段时间后),就可以在 [binaries.local.twitter.com](http://binaries.local.twitter.com/maven/com/twitter/sample/1.0-SNAPSHOT/) 上看到我们发布的jar包。
## 添加任务
任务就是Scala函数。添加一个任务最简单的方法是,在你的项目定义中引入一个val定义的任务方法,如
~~~
lazy val print = task {log.info("a test action"); None}
~~~
你也可以这样加上依赖和描述
~~~
lazy val print = task {log.info("a test action"); None}.dependsOn(compile) describedAs("prints a line after compile")
~~~
刷新项目,并执行print操作,我们将看到以下输出
~~~
> print
[info]
[info] == print ==
[info] a test action
[info] == print ==
[success] Successful.
[info]
[info] Total time: 0 s, completed Nov 24, 2010 11:05:12 AM
>
~~~
所以它起作用了。如果你只是在一个项目定义一个任务的话,这工作得很好。然而如果你定义的是一个插件的话,它就很不灵活了。我可能要
~~~
lazy val print = printAction
def printAction = printTask.dependsOn(compile) describedAs("prints a line after compile")
def printTask = task {log.info("a test action"); None}
~~~
这可以让消费者覆盖任务本身,依赖和/或任务的描述,或动作本身。大多数SBT内建的动作都遵循这种模式。作为一个例子,我们可以通过修改内置打包任务来打印当前时间戳
~~~
lazy val printTimestamp = task { log.info("current time is " + System.currentTimeMillis); None}
override def packageAction = super.packageAction.dependsOn(printTimestamp)
~~~
有很多例子介绍了怎样调整SBT默认的StandardProject,和如何添加自定义任务。
## 快速参考
### 常用命令
* actions – 显示这个项目中可用的动作
* update – 下载依赖
* compile – 编译源文件
* test – 运行测试
* package – 创建一个可发布的jar文件
* publish-local – 在本地ivy缓存中安装构建好的jar包
* publish – 将你的jar推到一个远程库中(如果配置了的话)
### 更多命令
* test-failed – 运行所有失败的规格测试
* test-quick – 运行任何失败的和/或依赖更新的规格
* clean-cache – 删除SBT缓存各种的东西。就像sbt的clean命令
* clean-lib – 删除lib_managed下的一切
### 项目布局
待续
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
高级类型
最后更新于:2022-04-01 03:05:10
课程内容:
[TOC=2,2]
## 视界(“类型类”)
有时候,你并不需要指定一个类型是等/子/超于另一个类,你可以通过转换这个类来伪装这种关联关系。一个视界指定一个类型可以被“看作是”另一个类型。这对对象的只读操作是很有用的。
**隐** 函数允许类型自动转换。更确切地说,在隐式函数可以帮助满足类型推断时,它们允许按需的函数应用。例如:
~~~
scala> implicit def strToInt(x: String) = x.toInt
strToInt: (x: String)Int
scala> "123"
res0: java.lang.String = 123
scala> val y: Int = "123"
y: Int = 123
scala> math.max("123", 111)
res1: Int = 123
~~~
视界,就像类型边界,要求对给定的类型存在这样一个函数。您可以使用`<%`指定类型限制,例如:
~~~
scala> class Container[A <% Int] { def addIt(x: A) = 123 + x }
defined class Container
~~~
这是说 **A** 必须“可被视”为 **Int** 。让我们试试。
~~~
scala> (new Container[String]).addIt("123")
res11: Int = 246
scala> (new Container[Int]).addIt(123)
res12: Int = 246
scala> (new Container[Float]).addIt(123.2F)
<console>:8: error: could not find implicit value for evidence parameter of type (Float) => Int
(new Container[Float]).addIt(123.2)
^
~~~
## 其他类型限制
方法可以通过隐含参数执行更复杂的类型限制。例如,`List`支持对数字内容执行`sum`,但对其他内容却不行。可是Scala的数字类型并不都共享一个超类,所以我们不能使用`T <: Number`。相反,要使之能工作,Scala的math库[对适当的类型T 定义了一个隐含的`Numeric[T]`](http://www.azavea.com/blogs/labs/2011/06/scalas-numeric-type-class-pt-1/)。 然后在`List`定义中使用它:
~~~
sum[B >: A](implicit num: Numeric[B]): B
~~~
如果你调用`List(1,2).sum()`,你并不需要传入一个 *num* 参数;它是隐式设置的。但如果你调用`List("whoop").sum()`,它会抱怨无法设置`num`。
在没有设定陌生的对象为`Numeric`的时候,方法可能会要求某种特定类型的“证据”。这时可以使用以下类型-关系运算符:
| A =:= B | A 必须和 B相等 |
| A <:< B | A 必须是 B的子类 |
| A <%< B | A 必须可以被看做是 B |
~~~
scala> class Container[A](value: A) { def addIt(implicit evidence: A =:= Int) = 123 + value }
defined class Container
scala> (new Container(123)).addIt
res11: Int = 246
scala> (new Container("123")).addIt
<console>:10: error: could not find implicit value for parameter evidence: =:=[java.lang.String,Int]
~~~
类似地,根据之前的隐式转换,我们可以放松约束为可视性:
~~~
scala> class Container[A](value: A) { def addIt(implicit evidence: A <%< Int) = 123 + value }
defined class Container
scala> (new Container("123")).addIt
res15: Int = 246
~~~
### 使用视图进行泛型编程
在Scala标准库中,视图主要用于实现集合的通用函数。例如“min”函数(在 **Seq[]** 上)就使用了这种技术:
~~~
def min[B >: A](implicit cmp: Ordering[B]): A = {
if (isEmpty)
throw new UnsupportedOperationException("empty.min")
reduceLeft((x, y) => if (cmp.lteq(x, y)) x else y)
}
~~~
其主要优点是:
* 集合中的元素并不是必须实现 **Ordered** 特质,但 **Ordered** 的使用仍然可以执行静态类型检查。
* 无需任何额外的库支持,你也可以定义自己的排序:
~~~
scala> List(1,2,3,4).min
res0: Int = 1
scala> List(1,2,3,4).min(new Ordering[Int] { def compare(a: Int, b: Int) = b compare a })
res3: Int = 4
~~~
作为旁注,标准库中有视图来将 **Ordered** 转换为 **Ordering** (反之亦然)。
~~~
trait LowPriorityOrderingImplicits {
implicit def ordered[A <: Ordered[A]]: Ordering[A] = new Ordering[A] {
def compare(x: A, y: A) = x.compare(y)
}
}
~~~
#### 上下文边界和implicitly[]
Scala2.8引入了一种串联和访问隐式参数的快捷方式。
~~~
scala> def foo[A](implicit x: Ordered[A]) {}
foo: [A](implicit x: Ordered[A])Unit
scala> def foo[A : Ordered] {}
foo: [A](implicit evidence$1: Ordered[A])Unit
~~~
隐式值可能会通过 **implicitly** 被访问
~~~
scala> implicitly[Ordering[Int]]
res37: Ordering[Int] = scala.math.Ordering$Int$@3a9291cf
~~~
相结合后往往会使用更少的代码,尤其是串联视图的时候。
## 更高级多态性类型 和 特设多态性
Scala可以对“更高阶”的类型进行抽象。例如,假设您需要用几种类型的容器处理几种类型的数据。你可能定义了一个`Container`的接口,它可以被实现为几种类型的容器:`Option`、`List`等。你要定义可以使用这些容器里的值的接口,但不想确定值的类型。
这类似与函数柯里化。例如,尽管“一元类型”有类似`List[A]`的构造函数,这意味着我们必须满足一个“级别”的类型变量来产生一个具体的类型(就像一个没有柯里化的函数需要只提供一个参数列表来被调用),更高阶的类型需要更多。
~~~
scala> trait Container[M[_]] { def put[A](x: A): M[A]; def get[A](m: M[A]): A }
scala> val container = new Container[List] { def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head }
container: java.lang.Object with Container[List] = $anon$1@7c8e3f75
scala> container.put("hey")
res24: List[java.lang.String] = List(hey)
scala> container.put(123)
res25: List[Int] = List(123)
~~~
注意:*Container*是参数化类型的多态(“容器类型”)。
如果我们结合隐式转换implicits使用容器,我们会得到“特设的”多态性:即对容器写泛型函数的能力。
~~~
scala> trait Container[M[_]] { def put[A](x: A): M[A]; def get[A](m: M[A]): A }
scala> implicit val listContainer = new Container[List] { def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head }
scala> implicit val optionContainer = new Container[Some] { def put[A](x: A) = Some(x); def get[A](m: Some[A]) = m.get }
scala> def tupleize[M[_]: Container, A, B](fst: M[A], snd: M[B]) = {
| val c = implicitly[Container[M]]
| c.put(c.get(fst), c.get(snd))
| }
tupleize: [M[_],A,B](fst: M[A],snd: M[B])(implicit evidence$1: Container[M])M[(A, B)]
scala> tupleize(Some(1), Some(2))
res33: Some[(Int, Int)] = Some((1,2))
scala> tupleize(List(1), List(2))
res34: List[(Int, Int)] = List((1,2))
~~~
## F-界多态性
通常有必要来访问一个(泛型)特质的具体子类。例如,想象你有一些泛型特质,但需要可以与它的某一子类进行比较。
~~~
trait Container extends Ordered[Container]
~~~
然而,现在比较方法是必须的了
~~~
def compare(that: Container): Int
~~~
因此,我们不能访问具体子类型,例如:
~~~
class MyContainer extends Container {
def compare(that: MyContainer): Int
}
~~~
编译失败,因为我们对 **Container** 指定了Ordered特质,而不是对特定子类型指定的。
为了调和这一点,我们改用F-界的多态性。
~~~
trait Container[A <: Container[A]] extends Ordered[A]
~~~
奇怪的类型!但可以看到怎样对 **A** 实现了Ordered参数化,它本身就是 **Container[A]**
所以,现在
~~~
class MyContainer extends Container[MyContainer] {
def compare(that: MyContainer) = 0
}
~~~
他们是有序的了:
~~~
scala> List(new MyContainer, new MyContainer, new MyContainer)
res3: List[MyContainer] = List(MyContainer@30f02a6d, MyContainer@67717334, MyContainer@49428ffa)
scala> List(new MyContainer, new MyContainer, new MyContainer).min
res4: MyContainer = MyContainer@33dfeb30
~~~
鉴于他们都是 **Container[_]** 的子类型,我们可以定义另一个子类并创建 **Container[_]** 的一个混合列表:
~~~
scala> class YourContainer extends Container[YourContainer] { def compare(that: YourContainer) = 0 }
defined class YourContainer
scala> List(new MyContainer, new MyContainer, new MyContainer, new YourContainer)
res2: List[Container[_ >: YourContainer with MyContainer <: Container[_ >: YourContainer with MyContainer <: ScalaObject]]]
= List(MyContainer@3be5d207, MyContainer@6d3fe849, MyContainer@7eab48a7, YourContainer@1f2f0ce9)
~~~
注意结果类型是怎样成为 **YourContainer 和 MyContainer** 类型确定的下界。这是类型推断的工作。有趣的是,这种类型甚至不需要是有意义的,它只是提供了一个合乎逻辑的最大下界为列表的统一类型。如果现在我们尝试使用 **Ordered** 会发生什么?
~~~
(new MyContainer, new MyContainer, new MyContainer, new YourContainer).min
<console>:9: error: could not find implicit value for parameter cmp:
Ordering[Container[_ >: YourContainer with MyContainer <: Container[_ >: YourContainer with MyContainer <: ScalaObject]]]
~~~
对统一的类型 **Ordered[]**不存在了。太糟糕了。
## 结构类型
Scala 支持 **结构类型 structural types** — 类型需求由接口 *构造* 表示,而不是由具体的类型表示。
~~~
scala> def foo(x: { def get: Int }) = 123 + x.get
foo: (x: AnyRef{def get: Int})Int
scala> foo(new { def get = 10 })
res0: Int = 133
~~~
这可能在很多场景都是相当不错的,但这个实现中使用了反射,所以要注意性能!
## 抽象类型成员
在特质中,你可以让类型成员保持抽象。
~~~
scala> trait Foo { type A; val x: A; def getX: A = x }
defined trait Foo
scala> (new Foo { type A = Int; val x = 123 }).getX
res3: Int = 123
scala> (new Foo { type A = String; val x = "hey" }).getX
res4: java.lang.String = hey
~~~
在做依赖注入等情况下,这往往是一个有用的技巧。
您可以使用hash操作符来引用一个抽象类型的变量:
~~~
scala> trait Foo[M[_]] { type t[A] = M[A] }
defined trait Foo
scala> val x: Foo[List]#t[Int] = List(1)
x: List[Int] = List(1)
~~~
## 类型擦除和清单
正如我们所知道的,类型信息在编译的时候会因为 *擦除* 而丢失。 Scala的 **清单(Manifests)** 功能,使我们能够选择性地恢复类型信息。清单提供了一个隐含值,根据需要由编译器生成。
~~~
scala> class MakeFoo[A](implicit manifest: Manifest[A]) { def make: A = manifest.erasure.newInstance.asInstanceOf[A] }
scala> (new MakeFoo[String]).make
res10: String = ""
~~~
## 案例分析: Finagle
参见: https://github.com/twitter/finagle
~~~
trait Service[-Req, +Rep] extends (Req => Future[Rep])
trait Filter[-ReqIn, +RepOut, +ReqOut, -RepIn]
extends ((ReqIn, Service[ReqOut, RepIn]) => Future[RepOut])
{
def andThen[Req2, Rep2](next: Filter[ReqOut, RepIn, Req2, Rep2]) =
new Filter[ReqIn, RepOut, Req2, Rep2] {
def apply(request: ReqIn, service: Service[Req2, Rep2]) = {
Filter.this.apply(request, new Service[ReqOut, RepIn] {
def apply(request: ReqOut): Future[RepIn] = next(request, service)
override def release() = service.release()
override def isAvailable = service.isAvailable
})
}
}
def andThen(service: Service[ReqOut, RepIn]) = new Service[ReqIn, RepOut] {
private[this] val refcounted = new RefcountedService(service)
def apply(request: ReqIn) = Filter.this.apply(request, refcounted)
override def release() = refcounted.release()
override def isAvailable = refcounted.isAvailable
}
}
~~~
一个服务可以通过过滤器对请求进行身份验证。
~~~
trait RequestWithCredentials extends Request {
def credentials: Credentials
}
class CredentialsFilter(credentialsParser: CredentialsParser)
extends Filter[Request, Response, RequestWithCredentials, Response]
{
def apply(request: Request, service: Service[RequestWithCredentials, Response]): Future[Response] = {
val requestWithCredentials = new RequestWrapper with RequestWithCredentials {
val underlying = request
val credentials = credentialsParser(request) getOrElse NullCredentials
}
service(requestWithCredentials)
}
}
~~~
注意底层服务是如何需要对请求进行身份验证的,而且还是静态验证。因此,过滤器可以被看作是服务转换器。
许多过滤器可以被组合在一起:
~~~
val upFilter =
logTransaction andThen
handleExceptions andThen
extractCredentials andThen
homeUser andThen
authenticate andThen
route
~~~
享用安全的类型吧!
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
类型和多态基础
最后更新于:2022-04-01 03:05:07
课程内容:
[TOC=2,2]
## 什么是静态类型?它们为什么有用?
按Pierce的话讲:“类型系统是一个语法方法,它们根据程序计算的值的种类对程序短语进行分类,通过分类结果错误行为进行自动检查。”
类型允许你表示函数的定义域和值域。例如,从数学角度看这个定义:
~~~
f: R -> N
~~~
它告诉我们函数“f”是从实数集到自然数集的映射。
抽象地说,这就是 *具体* 类型的准确定义。类型系统给我们提供了一些更强大的方式来表达这些集合。
鉴于这些注释,编译器可以 *静态地* (在编译时)验证程序是 *合理* 的。也就是说,如果值(在运行时)不符合程序规定的约束,编译将失败。
一般说来,类型检查只能保证 *不合理* 的程序不能编译通过。它不能保证每一个合理的程序都 *可以* 编译通过。
随着类型系统表达能力的提高,我们可以生产更可靠的代码,因为它能够在我们运行程序之前验证程序的不变性(当然是发现类型本身的模型bug!)。学术界一直很努力地提高类型系统的表现力,包括值依赖(value-dependent)类型!
需要注意的是,所有的类型信息会在编译时被删去,因为它已不再需要。这就是所谓的擦除。
## Scala中的类型
Scala强大的类型系统拥有非常丰富的表现力。其主要特性有:
* **参数化多态性** 粗略地说,就是泛型编程
* **(局部)类型推断** 粗略地说,就是为什么你不需要这样写代码`val i: Int = 12: Int`
* **存在量化** 粗略地说,为一些没有名称的类型进行定义
* **视窗** 我们将下周学习这些;粗略地说,就是将一种类型的值“强制转换”为另一种类型
## 参数化多态性
多态性是在不影响静态类型丰富性的前提下,用来(给不同类型的值)编写通用代码的。
例如,如果没有参数化多态性,一个通用的列表数据结构总是看起来像这样(事实上,它看起来很像使用泛型前的Java):
~~~
scala> 2 :: 1 :: "bar" :: "foo" :: Nil
res5: List[Any] = List(2, 1, bar, foo)
~~~
现在我们无法恢复其中成员的任何类型信息。
~~~
scala> res5.head
res6: Any = 2
~~~
所以我们的应用程序将会退化为一系列类型转换(“asInstanceOf[]”),并且会缺乏类型安全的保障(因为这些都是动态的)。
多态性是通过指定 *类型变量* 实现的。
~~~
scala> def drop1[A](l: List[A]) = l.tail
drop1: [A](l: List[A])List[A]
scala> drop1(List(1,2,3))
res1: List[Int] = List(2, 3)
~~~
### Scala有秩1多态性
粗略地说,这意味着在Scala中,有一些你想表达的类型概念“过于泛化”以至于编译器无法理解。假设你有一个函数
~~~
def toList[A](a: A) = List(a)
~~~
你希望继续泛型地使用它:
~~~
def foo[A, B](f: A => List[A], b: B) = f(b)
~~~
这段代码不能编译,因为所有的类型变量只有在调用上下文中才被固定。即使你“钉住”了类型`B`:
~~~
def foo[A](f: A => List[A], i: Int) = f(i)
~~~
…你也会得到一个类型不匹配的错误。
## 类型推断
静态类型的一个传统反对意见是,它有大量的语法开销。Scala通过 *类型推断* 来缓解这个问题。
在函数式编程语言中,类型推断的经典方法是 *Hindley Milner算法*,它最早是实现在ML中的。
Scala类型推断系统的实现稍有不同,但本质类似:推断约束,并试图统一类型。
例如,在Scala中你无法这样做:
~~~
scala> { x => x }
<console>:7: error: missing parameter type
{ x => x }
~~~
而在OCaml中你可以:
~~~
# fun x -> x;;
- : 'a -> 'a = <fun>
~~~
在Scala中所有类型推断是 *局部的* 。Scala一次分析一个表达式。例如:
~~~
scala> def id[T](x: T) = x
id: [T](x: T)T
scala> val x = id(322)
x: Int = 322
scala> val x = id("hey")
x: java.lang.String = hey
scala> val x = id(Array(1,2,3,4))
x: Array[Int] = Array(1, 2, 3, 4)
~~~
类型信息都保存完好,Scala编译器为我们进行了类型推断。请注意我们并不需要明确指定返回类型。
## 变性 Variance
Scala的类型系统必须同时解释类层次和多态性。类层次结构可以表达子类关系。在混合OO和多态性时,一个核心问题是:如果T’是T一个子类,Container[T’]应该被看做是Container[T]的子类吗?变性(Variance)注解允许你表达类层次结构和多态类型之间的关系:
| | **含义** | **Scala 标记** |
| **协变covariant** | C[T’]是 C[T] 的子类 | [+T] |
| **逆变contravariant** | C[T] 是 C[T’]的子类 | [-T] |
| **不变invariant** | C[T] 和 C[T’]无关 | [T] |
子类型关系的真正含义:对一个给定的类型T,如果T’是其子类型,你能替换它吗?
~~~
scala> class Covariant[+A]
defined class Covariant
scala> val cv: Covariant[AnyRef] = new Covariant[String]
cv: Covariant[AnyRef] = Covariant@4035acf6
scala> val cv: Covariant[String] = new Covariant[AnyRef]
<console>:6: error: type mismatch;
found : Covariant[AnyRef]
required: Covariant[String]
val cv: Covariant[String] = new Covariant[AnyRef]
^
~~~
~~~
scala> class Contravariant[-A]
defined class Contravariant
scala> val cv: Contravariant[String] = new Contravariant[AnyRef]
cv: Contravariant[AnyRef] = Contravariant@49fa7ba
scala> val fail: Contravariant[AnyRef] = new Contravariant[String]
<console>:6: error: type mismatch;
found : Contravariant[String]
required: Contravariant[AnyRef]
val fail: Contravariant[AnyRef] = new Contravariant[String]
^
~~~
逆变似乎很奇怪。什么时候才会用到它呢?令人惊讶的是,函数特质的定义就使用了它!
~~~
trait Function1 [-T1, +R] extends AnyRef
~~~
如果你仔细从替换的角度思考一下,会发现它是非常合理的。让我们先定义一个简单的类层次结构:
~~~
scala> class Animal { val sound = "rustle" }
defined class Animal
scala> class Bird extends Animal { override val sound = "call" }
defined class Bird
scala> class Chicken extends Bird { override val sound = "cluck" }
defined class Chicken
~~~
假设你需要一个以`Bird`为参数的函数:
~~~
scala> val getTweet: (Bird => String) = // TODO
~~~
标准动物库有一个函数满足了你的需求,但它的参数是`Animal`。在大多数情况下,如果你说“我需要一个___,我有一个___的子类”是可以的。但是,在函数参数这里是逆变的。如果你需要一个接受参数类型`Bird`的函数变量,但却将这个变量指向了接受参数类型为`Chicken`的函数,那么给它传入一个`Duck`时就会出错。然而,如果将该变量指向一个接受参数类型为`Animal`的函数就不会有这种问题:
~~~
scala> val getTweet: (Bird => String) = ((a: Animal) => a.sound )
getTweet: Bird => String = <function1>
~~~
函数的返回值类型是协变的。如果你需要一个返回`Bird`的函数,但指向的函数返回类型是`Chicken`,这当然是可以的。
~~~
scala> val hatch: (() => Bird) = (() => new Chicken )
hatch: () => Bird = <function0>
~~~
## 边界
Scala允许你通过 *边界* 来限制多态变量。这些边界表达了子类型关系。
~~~
scala> def cacophony[T](things: Seq[T]) = things map (_.sound)
<console>:7: error: value sound is not a member of type parameter T
def cacophony[T](things: Seq[T]) = things map (_.sound)
^
scala> def biophony[T <: Animal](things: Seq[T]) = things map (_.sound)
biophony: [T <: Animal](things: Seq[T])Seq[java.lang.String]
scala> biophony(Seq(new Chicken, new Bird))
res5: Seq[java.lang.String] = List(cluck, call)
~~~
类型下界也是支持的,这让逆变和巧妙协变的引入得心应手。`List[+T]`是协变的;一个Bird的列表也是Animal的列表。`List`定义一个操作`::(elem T)`返回一个加入了`elem`的新的`List`。新的`List`和原来的列表具有相同的类型:
~~~
scala> val flock = List(new Bird, new Bird)
flock: List[Bird] = List(Bird@7e1ec70e, Bird@169ea8d2)
scala> new Chicken :: flock
res53: List[Bird] = List(Chicken@56fbda05, Bird@7e1ec70e, Bird@169ea8d2)
~~~
`List` *同样* 定义了`::[B >: T](x: B)` 来返回一个`List[B]`。请注意`B >: T`,这指明了类型`B`为类型`T`的超类。这个方法让我们能够做正确地处理在一个`List[Bird]`前面加一个`Animal`的操作:
~~~
scala> new Animal :: flock
res59: List[Animal] = List(Animal@11f8d3a8, Bird@7e1ec70e, Bird@169ea8d2)
~~~
注意返回类型是`Animal`。
## 量化
有时候,你并不关心是否能够命名一个类型变量,例如:
~~~
scala> def count[A](l: List[A]) = l.size
count: [A](List[A])Int
~~~
这时你可以使用“通配符”取而代之:
~~~
scala> def count(l: List[_]) = l.size
count: (List[_])Int
~~~
这相当于是下面代码的简写:
~~~
scala> def count(l: List[T forSome { type T }]) = l.size
count: (List[T forSome { type T }])Int
~~~
注意量化会的结果会变得非常难以理解:
~~~
scala> def drop1(l: List[_]) = l.tail
drop1: (List[_])List[Any]
~~~
突然,我们失去了类型信息!让我们细化代码看看发生了什么:
~~~
scala> def drop1(l: List[T forSome { type T }]) = l.tail
drop1: (List[T forSome { type T }])List[T forSome { type T }]
~~~
我们不能使用T因为类型不允许这样做。
你也可以为通配符类型变量应用边界:
~~~
scala> def hashcodes(l: Seq[_ <: AnyRef]) = l map (_.hashCode)
hashcodes: (Seq[_ <: AnyRef])Seq[Int]
scala> hashcodes(Seq(1,2,3))
<console>:7: error: type mismatch;
found : Int(1)
required: AnyRef
Note: primitive types are not implicitly converted to AnyRef.
You can safely force boxing by casting x.asInstanceOf[AnyRef].
hashcodes(Seq(1,2,3))
^
scala> hashcodes(Seq("one", "two", "three"))
res1: Seq[Int] = List(110182, 115276, 110339486)
~~~
**参考** D. R. MacIver写的[Scala中的存在类型](http://www.drmaciver.com/2008/03/existential-types-in-scala/)
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
模式匹配与函数组合
最后更新于:2022-04-01 03:05:05
课程内容:
[TOC=2,2]
## 函数组合
让我们创建两个函数:
~~~
scala> def f(s: String) = "f(" + s + ")"
f: (String)java.lang.String
scala> def g(s: String) = "g(" + s + ")"
g: (String)java.lang.String
~~~
### compose
`compose` 组合其他函数形成一个新的函数 `f(g(x))`
~~~
scala> val fComposeG = f _ compose g _
fComposeG: (String) => java.lang.String = <function>
scala> fComposeG("yay")
res0: java.lang.String = f(g(yay))
~~~
### andThen
`andThen` 和 `compose`很像,但是调用顺序是先调用第一个函数,然后调用第二个,即`g(f(x))`
~~~
scala> val fAndThenG = f _ andThen g _
fAndThenG: (String) => java.lang.String = <function>
scala> fAndThenG("yay")
res1: java.lang.String = g(f(yay))
~~~
## 柯里化 vs 偏应用
### case 语句
#### 那么究竟什么是case语句?
这是一个名为PartialFunction的函数的子类。
#### 多个case语句的集合是什么?
他们是共同组合在一起的多个PartialFunction。
## 理解PartialFunction(偏函数)
对给定的输入参数类型,函数可接受该类型的任何值。换句话说,一个`(Int) => String` 的函数可以接收任意Int值,并返回一个字符串。
对给定的输入参数类型,偏函数只能接受该类型的某些特定的值。一个定义为`(Int) => String` 的偏函数可能不能接受所有Int值为输入。
`isDefinedAt` 是PartialFunction的一个方法,用来确定PartialFunction是否能接受一个给定的参数。
*注意* 偏函数`PartialFunction` 和我们前面提到的部分应用函数是无关的。
**参考** Effective Scala 对[PartialFunction](http://twitter.github.com/effectivescala/#Functional programming-Partial functions)的意见。
~~~
scala> val one: PartialFunction[Int, String] = { case 1 => "one" }
one: PartialFunction[Int,String] = <function1>
scala> one.isDefinedAt(1)
res0: Boolean = true
scala> one.isDefinedAt(2)
res1: Boolean = false
~~~
您可以调用一个偏函数。
~~~
scala> one(1)
res2: String = one
~~~
PartialFunctions可以使用`orElse`组成新的函数,得到的PartialFunction反映了是否对给定参数进行了定义。
~~~
scala> val two: PartialFunction[Int, String] = { case 2 => "two" }
two: PartialFunction[Int,String] = <function1>
scala> val three: PartialFunction[Int, String] = { case 3 => "three" }
three: PartialFunction[Int,String] = <function1>
scala> val wildcard: PartialFunction[Int, String] = { case _ => "something else" }
wildcard: PartialFunction[Int,String] = <function1>
scala> val partial = one orElse two orElse three orElse wildcard
partial: PartialFunction[Int,String] = <function1>
scala> partial(5)
res24: String = something else
scala> partial(3)
res25: String = three
scala> partial(2)
res26: String = two
scala> partial(1)
res27: String = one
scala> partial(0)
res28: String = something else
~~~
### case 之谜
上周我们看到一些新奇的东西。我们在通常应该使用函数的地方看到了一个case语句。
~~~
scala> case class PhoneExt(name: String, ext: Int)
defined class PhoneExt
scala> val extensions = List(PhoneExt("steve", 100), PhoneExt("robey", 200))
extensions: List[PhoneExt] = List(PhoneExt(steve,100), PhoneExt(robey,200))
scala> extensions.filter { case PhoneExt(name, extension) => extension < 200 }
res0: List[PhoneExt] = List(PhoneExt(steve,100))
~~~
为什么这段代码可以工作?
filter使用一个函数。在这个例子中是一个谓词函数(PhoneExt) => Boolean。
PartialFunction是Function的子类型,所以filter也可以使用PartialFunction!
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
集合
最后更新于:2022-04-01 03:05:03
课程内容:
[TOC=1,2]
# 基本数据结构
Scala提供了一些不错的集合。
**参考** Effective Scala 对怎样使用 [集合](http://twitter.github.com/effectivescala/#Collections)的观点。
## 列表 List
~~~
scala> val numbers = List(1, 2, 3, 4)
numbers: List[Int] = List(1, 2, 3, 4)
~~~
## 集 Set
集没有重复
~~~
scala> Set(1, 1, 2)
res0: scala.collection.immutable.Set[Int] = Set(1, 2)
~~~
## 元组 Tuple
元组是在不使用类的前提下,将元素组合起来形成简单的逻辑集合。
~~~
scala> val hostPort = ("localhost", 80)
hostPort: (String, Int) = (localhost, 80)
~~~
与样本类不同,元组不能通过名称获取字段,而是使用位置下标来读取对象;而且这个下标基于1,而不是基于0。
~~~
scala> hostPort._1
res0: String = localhost
scala> hostPort._2
res1: Int = 80
~~~
元组可以很好得与模式匹配相结合。
~~~
hostPort match {
case ("localhost", port) => ...
case (host, port) => ...
}
~~~
在创建两个元素的元组时,可以使用特殊语法:`->`
~~~
scala> 1 -> 2
res0: (Int, Int) = (1,2)
~~~
**参考** Effective Scala 对 [解构绑定](http://twitter.github.com/effectivescala/#Functional programming-Destructuring bindings) (“拆解”一个元组)的观点。
## 映射 Map
它可以持有基本数据类型。
~~~
Map(1 -> 2)
Map("foo" -> "bar")
~~~
这看起来像是特殊的语法,不过不要忘了上文讨论的`->`可以用来创建二元组。
Map()方法也使用了从第一节课学到的变参列表:`Map(1 -> "one", 2 -> "two")`将变为 `Map((1, "one"), (2, "two"))`,其中第一个参数是映射的键,第二个参数是映射的值。
映射的值可以是映射甚或是函数。
~~~
Map(1 -> Map("foo" -> "bar"))
~~~
~~~
Map("timesTwo" -> { timesTwo(_) })
~~~
## 选项 Option
`Option` 是一个表示有可能包含值的容器。
Option基本的接口是这样的:
~~~
trait Option[T] {
def isDefined: Boolean
def get: T
def getOrElse(t: T): T
}
~~~
Option本身是泛型的,并且有两个子类: `Some[T]` 或 `None`
我们看一个使用Option的例子:
`Map.get` 使用 `Option` 作为其返回值,表示这个方法也许不会返回你请求的值。
~~~
scala> val numbers = Map("one" -> 1, "two" -> 2)
numbers: scala.collection.immutable.Map[java.lang.String,Int] = Map(one -> 1, two -> 2)
scala> numbers.get("two")
res0: Option[Int] = Some(2)
scala> numbers.get("three")
res1: Option[Int] = None
~~~
现在我们的数据似乎陷在`Option`中了,我们怎样获取这个数据呢?
直觉上想到的可能是在`isDefined`方法上使用条件判断来处理。
~~~
// We want to multiply the number by two, otherwise return 0.
val result = if (res1.isDefined) {
res1.get * 2
} else {
0
}
~~~
我们建议使用`getOrElse`或模式匹配处理这个结果。
`getOrElse` 让你轻松地定义一个默认值。
`val result = res1.getOrElse(0) * 2`
模式匹配能自然地配合`Option`使用。
~~~
val result = res1 match {
case Some(n) => n * 2
case None => 0
}
~~~
**参考** Effective Scala 对使用[Options](http://twitter.github.com/effectivescala/#Functional programming-Options)的意见。
# 函数组合子(Functional Combinators)
`List(1, 2, 3) map squared`对列表中的每一个元素都应用了`squared`平方函数,并返回一个新的列表`List(1, 4, 9)`。我们称这个操作`map` *组合子*。 (如果想要更好的定义,你可能会喜欢Stackoverflow上对[组合子的说明](http://stackoverflow.com/questions/7533837/explanation-of-combinators-for-the-working-man)。)他们常被用在标准的数据结构上。
## map
`map`对列表中的每个元素应用一个函数,返回应用后的元素所组成的列表。
~~~
scala> numbers.map((i: Int) => i * 2)
res0: List[Int] = List(2, 4, 6, 8)
~~~
或传入一个部分应用函数
~~~
scala> def timesTwo(i: Int): Int = i * 2
timesTwo: (i: Int)Int
scala> numbers.map(timesTwo _)
res0: List[Int] = List(2, 4, 6, 8)
~~~
## foreach
`foreach`很像map,但没有返回值。foreach仅用于有副作用[side-effects]的函数。
~~~
scala> numbers.foreach((i: Int) => i * 2)
~~~
什么也没有返回。
你可以尝试存储返回值,但它会是Unit类型(即void)
~~~
scala> val doubled = numbers.foreach((i: Int) => i * 2)
doubled: Unit = ()
~~~
## filter
`filter`移除任何对传入函数计算结果为false的元素。返回一个布尔值的函数通常被称为谓词函数[或判定函数]。
~~~
scala> numbers.filter((i: Int) => i % 2 == 0)
res0: List[Int] = List(2, 4)
~~~
~~~
scala> def isEven(i: Int): Boolean = i % 2 == 0
isEven: (i: Int)Boolean
scala> numbers.filter(isEven _)
res2: List[Int] = List(2, 4)
~~~
## zip
`zip`将两个列表的内容聚合到一个对偶列表中。
~~~
scala> List(1, 2, 3).zip(List("a", "b", "c"))
res0: List[(Int, String)] = List((1,a), (2,b), (3,c))
~~~
## partition
`partition`将使用给定的谓词函数分割列表。
~~~
scala> val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> numbers.partition(_ % 2 == 0)
res0: (List[Int], List[Int]) = (List(2, 4, 6, 8, 10),List(1, 3, 5, 7, 9))
~~~
## find
`find`返回集合中第一个匹配谓词函数的元素。
~~~
scala> numbers.find((i: Int) => i > 5)
res0: Option[Int] = Some(6)
~~~
## drop & dropWhile
`drop` 将删除前i个元素
~~~
scala> numbers.drop(5)
res0: List[Int] = List(6, 7, 8, 9, 10)
~~~
`dropWhile` 将删除元素直到找到第一个匹配谓词函数的元素。例如,如果我们在numbers列表上使用`dropWhile`奇数的函数, `1`将被丢弃(但`3`不会被丢弃,因为他被`2`“保护”了)。
~~~
scala> numbers.dropWhile(_ % 2 != 0)
res0: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)
~~~
## foldLeft
~~~
scala> numbers.foldLeft(0)((m: Int, n: Int) => m + n)
res0: Int = 55
~~~
0为初始值(记住numbers是List[Int]类型),m作为一个累加器。
直接观察运行过程:
~~~
scala> numbers.foldLeft(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n }
m: 0 n: 1
m: 1 n: 2
m: 3 n: 3
m: 6 n: 4
m: 10 n: 5
m: 15 n: 6
m: 21 n: 7
m: 28 n: 8
m: 36 n: 9
m: 45 n: 10
res0: Int = 55
~~~
### foldRight
和foldLeft一样,只是运行过程相反。
~~~
scala> numbers.foldRight(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n }
m: 10 n: 0
m: 9 n: 10
m: 8 n: 19
m: 7 n: 27
m: 6 n: 34
m: 5 n: 40
m: 4 n: 45
m: 3 n: 49
m: 2 n: 52
m: 1 n: 54
res0: Int = 55
~~~
## flatten
`flatten`将嵌套结构扁平化为一个层次的集合。
~~~
scala> List(List(1, 2), List(3, 4)).flatten
res0: List[Int] = List(1, 2, 3, 4)
~~~
## flatMap
`flatMap`是一种常用的组合子,结合映射[mapping]和扁平化[flattening]。 flatMap需要一个处理嵌套列表的函数,然后将结果串连起来。
~~~
scala> val nestedNumbers = List(List(1, 2), List(3, 4))
nestedNumbers: List[List[Int]] = List(List(1, 2), List(3, 4))
scala> nestedNumbers.flatMap(x => x.map(_ * 2))
res0: List[Int] = List(2, 4, 6, 8)
~~~
可以把它看做是“先映射后扁平化”的快捷操作:
~~~
scala> nestedNumbers.map((x: List[Int]) => x.map(_ * 2)).flatten
res1: List[Int] = List(2, 4, 6, 8)
~~~
这个例子先调用map,然后可以马上调用flatten,这就是“组合子”的特征,也是这些函数的本质。
**参考** Effective Scala 对[flatMap](http://twitter.github.com/effectivescala/#Functional programming-`flatMap`)的意见。
## 扩展函数组合子
现在我们已经学过集合上的一些函数。
我们将尝试写自己的函数组合子。
有趣的是,上面所展示的每一个函数组合子都可以用fold方法实现。让我们看一些例子。
~~~
def ourMap(numbers: List[Int], fn: Int => Int): List[Int] = {
numbers.foldRight(List[Int]()) { (x: Int, xs: List[Int]) =>
fn(x) :: xs
}
}
scala> ourMap(numbers, timesTwo(_))
res0: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
~~~
为什么是List[Int]()?Scala没有聪明到理解你的目的是将结果积聚在一个空的Int类型的列表中。
## Map?
所有展示的函数组合子都可以在Map上使用。Map可以被看作是一个二元组的列表,所以你写的函数要处理一个键和值的二元组。
~~~
scala> val extensions = Map("steve" -> 100, "bob" -> 101, "joe" -> 201)
extensions: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101), (joe,201))
~~~
现在筛选出电话分机号码低于200的条目。
~~~
scala> extensions.filter((namePhone: (String, Int)) => namePhone._2 < 200)
res0: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101))
~~~
因为参数是元组,所以你必须使用位置获取器来读取它们的键和值。呃!
幸运的是,我们其实可以使用模式匹配更优雅地提取键和值。
~~~
scala> extensions.filter({case (name, extension) => extension < 200})
res0: scala.collection.immutable.Map[String,Int] = Map((steve,100), (bob,101))
~~~
为什么这个代码可以工作?为什么你可以传递一个部分模式匹配?
敬请关注下周的内容!
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
基础知识(续)
最后更新于:2022-04-01 03:05:01
课程内容:
[TOC=2,3]
## apply 方法
当类或对象有一个主要用途的时候,apply方法为你提供了一个很好的语法糖。
~~~
scala> class Foo {}
defined class Foo
scala> object FooMaker {
| def apply() = new Foo
| }
defined module FooMaker
scala> val newFoo = FooMaker()
newFoo: Foo = Foo@5b83f762
~~~
或
~~~
scala> class Bar {
| def apply() = 0
| }
defined class Bar
scala> val bar = new Bar
bar: Bar = Bar@47711479
scala> bar()
res8: Int = 0
~~~
在这里,我们实例化对象看起来像是在调用一个方法。以后会有更多介绍!
## 单例对象
单例对象用于持有一个类的唯一实例。通常用于工厂模式。
~~~
object Timer {
var count = 0
def currentCount(): Long = {
count += 1
count
}
}
~~~
可以这样使用:
~~~
scala> Timer.currentCount()
res0: Long = 1
~~~
单例对象可以和类具有相同的名称,此时该对象也被称为“伴生对象”。我们通常将伴生对象作为工厂使用。
下面是一个简单的例子,可以不需要使用’new’来创建一个实例了。
~~~
class Bar(foo: String)
object Bar {
def apply(foo: String) = new Bar(foo)
}
~~~
## 函数即对象
在Scala中,我们经常谈论对象的函数式编程。这是什么意思?到底什么是函数呢?
函数是一些特质的集合。具体来说,具有一个参数的函数是Function1特质的一个实例。这个特征定义了`apply()`语法糖,让你调用一个对象时就像你在调用一个函数。
~~~
scala> object addOne extends Function1[Int, Int] {
| def apply(m: Int): Int = m + 1
| }
defined module addOne
scala> addOne(1)
res2: Int = 2
~~~
这个Function特质集合下标从0开始一直到22。为什么是22?这是一个主观的魔幻数字(magic number)。我从来没有使用过多于22个参数的函数,所以这个数字似乎是合理的。
apply语法糖有助于统一对象和函数式编程的二重性。你可以传递类,并把它们当做函数使用,而函数本质上是类的实例。
这是否意味着,当你在类中定义一个方法时,得到的实际上是一个Function*的实例?不是的,在类中定义的方法是方法而不是函数。在repl中独立定义的方法是Function*的实例。
类也可以扩展Function,这些类的实例可以使用()调用。
~~~
scala> class AddOne extends Function1[Int, Int] {
| def apply(m: Int): Int = m + 1
| }
defined class AddOne
scala> val plusOne = new AddOne()
plusOne: AddOne = <function1>
scala> plusOne(1)
res0: Int = 2
~~~
可以使用更直观快捷的`extends (Int => Int)`代替`extends Function1[Int, Int]`
~~~
class AddOne extends (Int => Int) {
def apply(m: Int): Int = m + 1
}
~~~
## 包
你可以将代码组织在包里。
~~~
package com.twitter.example
~~~
在文件头部定义包,会将文件中所有的代码声明在那个包中。
值和函数不能在类或单例对象之外定义。单例对象是组织静态函数(static function)的有效工具。
~~~
package com.twitter.example
object colorHolder {
val BLUE = "Blue"
val RED = "Red"
}
~~~
现在你可以直接访问这些成员
~~~
println("the color is: " + com.twitter.example.colorHolder.BLUE)
~~~
注意在你定义这个对象时Scala解释器的返回:
~~~
scala> object colorHolder {
| val Blue = "Blue"
| val Red = "Red"
| }
defined module colorHolder
~~~
这暗示了Scala的设计者是把对象作为Scala的模块系统的一部分进行设计的。
## 模式匹配
这是Scala中最有用的部分之一。
匹配值
~~~
val times = 1
times match {
case 1 => "one"
case 2 => "two"
case _ => "some other number"
}
~~~
使用守卫进行匹配
~~~
times match {
case i if i == 1 => "one"
case i if i == 2 => "two"
case _ => "some other number"
}
~~~
注意我们是怎样将值赋给变量’i’的。
在最后一行指令中的`_`是一个通配符;它保证了我们可以处理所有的情况。
否则当传进一个不能被匹配的数字的时候,你将获得一个运行时错误。我们以后会继续讨论这个话题的。
**参考** Effective Scala 对[什么时候使用模式匹配](http://twitter.github.com/effectivescala/#Functional programming-Pattern matching) 和 [模式匹配格式化](http://twitter.github.com/effectivescala/#Formatting-Pattern matching)的建议. A Tour of Scala 也描述了 [模式匹配](http://www.scala-lang.org/node/120)
### 匹配类型
你可以使用 `match`来分别处理不同类型的值。
~~~
def bigger(o: Any): Any = {
o match {
case i: Int if i < 0 => i - 1
case i: Int => i + 1
case d: Double if d < 0.0 => d - 0.1
case d: Double => d + 0.1
case text: String => text + "s"
}
}
~~~
### 匹配类成员
还记得我们之前的计算器吗。
让我们通过类型对它们进行分类。
一开始会很痛苦。
~~~
def calcType(calc: Calculator) = calc match {
case _ if calc.brand == "hp" && calc.model == "20B" => "financial"
case _ if calc.brand == "hp" && calc.model == "48G" => "scientific"
case _ if calc.brand == "hp" && calc.model == "30B" => "business"
case _ => "unknown"
}
~~~
(⊙o⊙)哦,太痛苦了。幸好Scala提供了一些应对这种情况的有效工具。
## 样本类 Case Classes
使用样本类可以方便得存储和匹配类的内容。你不用new关键字就可以创建它们。
~~~
scala> case class Calculator(brand: String, model: String)
defined class Calculator
scala> val hp20b = Calculator("hp", "20b")
hp20b: Calculator = Calculator(hp,20b)
~~~
样本类基于构造函数的参数,自动地实现了相等性和易读的toString方法。
~~~
scala> val hp20b = Calculator("hp", "20b")
hp20b: Calculator = Calculator(hp,20b)
scala> val hp20B = Calculator("hp", "20b")
hp20B: Calculator = Calculator(hp,20b)
scala> hp20b == hp20B
res6: Boolean = true
~~~
样本类也可以像普通类那样拥有方法。
###### 使用样本类进行模式匹配
case classes are designed to be used with pattern matching. Let’s simplify our calculator classifier example from earlier.
样本类就是被设计用在模式匹配中的。让我们简化之前的计算器分类器的例子。
~~~
val hp20b = Calculator("hp", "20B")
val hp30b = Calculator("hp", "30B")
def calcType(calc: Calculator) = calc match {
case Calculator("hp", "20B") => "financial"
case Calculator("hp", "48G") => "scientific"
case Calculator("hp", "30B") => "business"
case Calculator(ourBrand, ourModel) => "Calculator: %s %s is of unknown type".format(ourBrand, ourModel)
}
~~~
最后一句也可以这样写
~~~
case Calculator(_, _) => "Calculator of unknown type"
~~~
或者我们完全可以不将匹配对象指定为Calculator类型
~~~
case _ => "Calculator of unknown type"
~~~
或者我们也可以将匹配的值重新命名。
~~~
case c@Calculator(_, _) => "Calculator: %s of unknown type".format(c)
~~~
## 异常
Scala中的异常可以在try-catch-finally语法中通过模式匹配使用。
~~~
try {
remoteCalculatorService.add(1, 2)
} catch {
case e: ServerIsDownException => log.error(e, "the remote calculator service is unavailable. should have kept your trusty HP.")
} finally {
remoteCalculatorService.close()
}
~~~
`try`也是面向表达式的
~~~
val result: Int = try {
remoteCalculatorService.add(1, 2)
} catch {
case e: ServerIsDownException => {
log.error(e, "the remote calculator service is unavailable. should have kept your trusty HP.")
0
}
} finally {
remoteCalculatorService.close()
}
~~~
这并不是一个完美编程风格的展示,而只是一个例子,用来说明try-catch-finally和Scala中其他大部分事物一样是表达式。
当一个异常被捕获处理了,finally块将被调用;它不是表达式的一部分。
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
Licensed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0).
基础
最后更新于:2022-04-01 03:04:58
课程内容:
[TOC=3,3]
## 关于这节课
最初的几个星期将涵盖基本语法和概念,然后我们将通过更多的练习展开这些内容。
有一些例子是以解释器交互的形式给出的,另一些则是以源文件的形式给出的。
安装一个解释器,可以使探索问题空间变得更容易。
### 为什么选择 Scala?
* 表达能力
* 函数是一等公民
* 闭包
* 简洁
* 类型推断
* 函数创建的文法支持
* Java互操作性
* 可重用Java库
* 可重用Java工具
* 没有性能惩罚
### Scala 如何工作?
* 编译成Java字节码
* 可在任何标准JVM上运行
* 甚至是一些不规范的JVM上,如Dalvik
* Scala编译器是Java编译器的作者写的
### 用 Scala 思考
Scala不仅仅是更好的Java。你应该用全新的头脑来学习它,你会从这些课程中认识到这一点的。
### 启动解释器
使用自带的`sbt console`启动。
~~~
$ sbt console
[...]
Welcome to Scala version 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_20).
Type in expressions to have them evaluated.
Type :help for more information.
scala>
~~~
## 表达式
~~~
scala> 1 + 1
res0: Int = 2
~~~
res0是解释器自动创建的变量名称,用来指代表达式的计算结果。它是Int类型,值为2。
Scala中(几乎)一切都是表达式。
## 值
你可以给一个表达式的结果起个名字赋成一个不变量(val)。
~~~
scala> val two = 1 + 1
two: Int = 2
~~~
你不能改变这个不变量的值.
### 变量
如果你需要修改这个名称和结果的绑定,可以选择使用`var`。
~~~
scala> var name = "steve"
name: java.lang.String = steve
scala> name = "marius"
name: java.lang.String = marius
~~~
## 函数
你可以使用def创建函数.
~~~
scala> def addOne(m: Int): Int = m + 1
addOne: (m: Int)Int
~~~
在Scala中,你需要为函数参数指定类型签名。
~~~
scala> val three = addOne(2)
three: Int = 3
~~~
如果函数不带参数,你可以不写括号。
~~~
scala> def three() = 1 + 2
three: ()Int
scala> three()
res2: Int = 3
scala> three
res3: Int = 3
~~~
### 匿名函数
你可以创建匿名函数。
~~~
scala> (x: Int) => x + 1
res2: (Int) => Int = <function1>
~~~
这个函数为名为x的Int变量加1。
~~~
scala> res2(1)
res3: Int = 2
~~~
你可以传递匿名函数,或将其保存成不变量。
~~~
scala> val addOne = (x: Int) => x + 1
addOne: (Int) => Int = <function1>
scala> addOne(1)
res4: Int = 2
~~~
如果你的函数有很多表达式,可以使用{}来格式化代码,使之易读。
~~~
def timesTwo(i: Int): Int = {
println("hello world")
i * 2
}
~~~
对匿名函数也是这样的。
~~~
scala> { i: Int =>
println("hello world")
i * 2
}
res0: (Int) => Int = <function1>
~~~
在将一个匿名函数作为参数进行传递时,这个语法会经常被用到。
### 部分应用(Partial application)
你可以使用下划线“_”部分应用一个函数,结果将得到另一个函数。Scala使用下划线表示不同上下文中的不同事物,你通常可以把它看作是一个没有命名的神奇通配符。在`{ _ + 2 }`的上下文中,它代表一个匿名参数。你可以这样使用它:
~~~
scala> def adder(m: Int, n: Int) = m + n
adder: (m: Int,n: Int)Int
~~~
~~~
scala> val add2 = adder(2, _:Int)
add2: (Int) => Int = <function1>
scala> add2(3)
res50: Int = 5
~~~
你可以部分应用参数列表中的任意参数,而不仅仅是最后一个。
### 柯里化函数
有时会有这样的需求:允许别人一会在你的函数上应用一些参数,然后又应用另外的一些参数。
例如一个乘法函数,在一个场景需要选择乘数,而另一个场景需要选择被乘数。
~~~
scala> def multiply(m: Int)(n: Int): Int = m * n
multiply: (m: Int)(n: Int)Int
~~~
你可以直接传入两个参数。
~~~
scala> multiply(2)(3)
res0: Int = 6
~~~
你可以填上第一个参数并且部分应用第二个参数。
~~~
scala> val timesTwo = multiply(2) _
timesTwo: (Int) => Int = <function1>
scala> timesTwo(3)
res1: Int = 6
~~~
你可以对任何多参数函数执行柯里化。例如之前的`adder`函数
~~~
scala> (adder _).curried
res1: (Int) => (Int) => Int = <function1>
~~~
### 可变长度参数
这是一个特殊的语法,可以向方法传入任意多个同类型的参数。例如要在多个字符串上执行String的`capitalize`函数,可以这样写:
~~~
def capitalizeAll(args: String*) = {
args.map { arg =>
arg.capitalize
}
}
scala> capitalizeAll("rarity", "applejack")
res2: Seq[String] = ArrayBuffer(Rarity, Applejack)
~~~
## 类
~~~
scala> class Calculator {
| val brand: String = "HP"
| def add(m: Int, n: Int): Int = m + n
| }
defined class Calculator
scala> val calc = new Calculator
calc: Calculator = Calculator@e75a11
scala> calc.add(1, 2)
res1: Int = 3
scala> calc.brand
res2: String = "HP"
~~~
上面的例子展示了如何在类中用def定义方法和用val定义字段值。方法就是可以访问类的状态的函数。
### 构造函数
构造函数不是特殊的方法,他们是除了类的方法定义之外的代码。让我们扩展计算器的例子,增加一个构造函数参数,并用它来初始化内部状态。
~~~
class Calculator(brand: String) {
/**
* A constructor.
*/
val color: String = if (brand == "TI") {
"blue"
} else if (brand == "HP") {
"black"
} else {
"white"
}
// An instance method.
def add(m: Int, n: Int): Int = m + n
}
~~~
注意两种不同风格的评论。
你可以使用构造函数来构造一个实例:
~~~
scala> val calc = new Calculator("HP")
calc: Calculator = Calculator@1e64cc4d
scala> calc.color
res0: String = black
~~~
### 表达式
上文的Calculator例子说明了Scala是如何面向表达式的。颜色的值就是绑定在一个if/else表达式上的。Scala是高度面向表达式的:大多数东西都是表达式而非指令。
### 旁白: 函数 vs 方法
函数和方法在很大程度上是可以互换的。由于函数和方法是如此的相似,你可能都不知道你调用的*东西*是一个函数还是一个方法。而当真正碰到的方法和函数之间的差异的时候,你可能会感到困惑。
~~~
scala> class C {
| var acc = 0
| def minc = { acc += 1 }
| val finc = { () => acc += 1 }
| }
defined class C
scala> val c = new C
c: C = C@1af1bd6
scala> c.minc // calls c.minc()
scala> c.finc // returns the function as a value:
res2: () => Unit = <function0>
~~~
当你可以调用一个不带括号的“函数”,但是对另一个却必须加上括号的时候,你可能会想*哎呀,我还以为自己知道Scala是怎么工作的呢。也许他们有时需要括号?*你可能以为自己用的是函数,但实际使用的是方法。
在实践中,即使不理解方法和函数上的区别,你也可以用Scala做伟大的事情。如果你是Scala新手,而且在读[两者的差异解释](https://www.google.com/search?q=difference+scala+function+method),你可能会跟不上。不过这并不意味着你在使用Scala上有麻烦。它只是意味着函数和方法之间的差异是很微妙的,只有深入语言内部才能清楚理解它。
## 继承
~~~
class ScientificCalculator(brand: String) extends Calculator(brand) {
def log(m: Double, base: Double) = math.log(m) / math.log(base)
}
~~~
**参考** Effective Scala 指出如果子类与父类实际上没有区别,[类型别名](http://twitter.github.com/effectivescala/#Types%20and%20Generics-Type%20aliases)是优于`继承`的。A Tour of Scala 详细介绍了[子类化](http://www.scala-lang.org/node/125)。
### 重载方法
~~~
class EvenMoreScientificCalculator(brand: String) extends ScientificCalculator(brand) {
def log(m: Int): Double = log(m, math.exp(1))
}
~~~
### 抽象类
你可以定义一个*抽象类*,它定义了一些方法但没有实现它们。取而代之是由扩展抽象类的子类定义这些方法。你不能创建抽象类的实例。
~~~
scala> abstract class Shape {
| def getArea():Int // subclass should define this
| }
defined class Shape
scala> class Circle(r: Int) extends Shape {
| def getArea():Int = { r * r * 3 }
| }
defined class Circle
scala> val s = new Shape
<console>:8: error: class Shape is abstract; cannot be instantiated
val s = new Shape
^
scala> val c = new Circle(2)
c: Circle = Circle@65c0035b
~~~
## 特质(Traits)
`特质`是一些字段和行为的集合,可以扩展或混入(mixin)你的类中。
~~~
trait Car {
val brand: String
}
trait Shiny {
val shineRefraction: Int
}
~~~
~~~
class BMW extends Car {
val brand = "BMW"
}
~~~
通过`with`关键字,一个类可以扩展多个特质:
~~~
class BMW extends Car with Shiny {
val brand = "BMW"
val shineRefraction = 12
}
~~~
**参考** Effective Scala 对[特质的观点](http://twitter.github.com/effectivescala/#Object oriented programming-Traits)。
**什么时候应该使用特质而不是抽象类?** 如果你想定义一个类似接口的类型,你可能会在特质和抽象类之间难以取舍。这两种形式都可以让你定义一个类型的一些行为,并要求继承者定义一些其他行为。一些经验法则:
* 优先使用特质。一个类扩展多个特质是很方便的,但却只能扩展一个抽象类。
* 如果你需要构造函数参数,使用抽象类。因为抽象类可以定义带参数的构造函数,而特质不行。例如,你不能说`trait t(i: Int) {}`,参数`i`是非法的。
你不是问这个问题的第一人。可以查看更全面的答案: [stackoverflow: Scala特质 vs 抽象类](http://stackoverflow.com/questions/1991042/scala-traits-vs-abstract-classes) , [抽象类和特质的区别](http://stackoverflow.com/questions/2005681/difference-between-abstract-class-and-trait), and [Scala编程: 用特质,还是不用特质?](http://www.artima.com/pins1ed/traits.html#12.7)
## 类型
此前,我们定义了一个函数的参数为`Int`,表示输入是一个数字类型。其实函数也可以是泛型的,来适用于所有类型。当这种情况发生时,你会看到用方括号语法引入的类型参数。下面的例子展示了一个使用泛型键和值的缓存。
~~~
trait Cache[K, V] {
def get(key: K): V
def put(key: K, value: V)
def delete(key: K)
}
~~~
方法也可以引入类型参数。
~~~
def remove[K](key: K)
~~~
Built at [@twitter](http://twitter.com/twitter) by [@stevej](http://twitter.com/stevej), [@marius](http://twitter.com/marius), and [@lahosken](http://twitter.com/lahosken) with much help from [@evanm](http://twitter.com/evanm), [@sprsquish](http://twitter.com/sprsquish), [@kevino](http://twitter.com/kevino), [@zuercher](http://twitter.com/zuercher), [@timtrueman](http://twitter.com/timtrueman), [@wickman](http://twitter.com/wickman), and[@mccv](http://twitter.com/mccv); Russian translation by [appigram](https://github.com/appigram); Chinese simple translation by [jasonqu](https://github.com/jasonqu); Korean translation by [enshahar](https://github.com/enshahar);
课程
最后更新于:2022-04-01 03:04:56
方法
最后更新于:2022-04-01 03:04:54
我们认为最有意义的教学方式是,不要把Scala看做是改进的Java,而是把它作为一门新的语言。所以这里不会介绍Java的使用经验,而将聚焦在解释器和“对象-函数式”的风格,以及我们的编程风格。特别强调了可维护性,清晰的表达,和利用类型系统的优势。
大部分课程除了Scala的交互命令行之外不需要其他软件。我们鼓励读者按顺序学习,并且不仅限于此。让这些课程作为您探索Scala的起点吧!
关于
最后更新于:2022-04-01 03:04:51
> 原文出处:http://twitter.github.io/scala_school/zh_cn/index.html
Scala课堂是Twitter启动的一系列讲座,用来帮助有经验的工程师成为高效的[Scala](http://www.scala-lang.org/) 程序员。Scala是一种相对较新的语言,但借鉴了许多熟悉的概念。因此,课程中的讲座假设听众知道这些概念,并展示了如何在Scala中使用它们。我们发现这是一个让新工程师能够快速上手的有效方法。网站里的是伴随这些讲座的书面材料,这些文字材料本身也是很有用的。