scala中tuple太长_说说选择Scala的几点缘由

点击关注上方“知了小巷”,

设为“置顶或星标”,第一时间送达干货。

Scala的亮点

简洁代码

Scala提供的脚本特性以及将函数作为一等公民的方式,使得它可以去掉不少在Java中显得冗余的代码,例如不必要的类定义,不必要的main函数声明。Scala提供的类型推断机制,也使得代码精简成为可能。Scala还有一个巧妙的设计,就是允许在定义类的同时定义该类的主构造函数。在大多数情况下,可以避免我们声明不必要的构造函数。

Scala还提供了一些非常有用的语法糖,如伴生对象,样例类,既简化了接口,也简化了我们需要书写的代码。例如如下代码:

case class Person(name: String, age: Int)
val l = List(Person("Jack", 28), Person("Bruce", 30))

这里的List和Person都提供了伴生对象,避免再写冗余的new。这种方式对于DSL支持也是有帮助的。Person是一个样例类,虽然只有这么一行代码,蕴含的含义却非常丰富——它为Person提供了属性,属性对应的访问器,equals和hashcode方法,伴生对象,以及对模式匹配的支持。在Scala 2.11版本中,还突破了样例类属性个数的约束。由于样例类是不变的,也能实现trait,因而通常作为message而被广泛应用到系统中。例如在AKKA中,actor之间传递的消息都应该尽量定义为样例类。

支持OO与FP

将面向对象与函数式编程有机地结合,本身就是Martin Odersky以及Scala的目标。个人认为应针对不同场景,选择不同的设计思想。

关于如何设计没有副作用的纯函数纯函数针对给定的输入,总是返回相同的输出,且没有任何副作用,就使得纯函数更容易推论(这意味着它更容易测试),更容易组合。从某种角度来讲,这样的设计指导思想与OO阵营中的CQS原则(命令-查询-分离)非常一致,只是重用的粒度不一样罢了。

比如如下代码中的declareWinner函数并非纯函数

object Game {
  def printWinner(p: Player): Unit =
    println(p.name + " is the winner!")

  def declareWinner(p1: Player, p2: Player): Unit =
    if (p1.score > p2.score)
      printWinner(p1)
    else printWinner(p2)
}

这里的printWinner要向控制台输出字符串,从而产生了副作用。(简单的判断标准是看函数的返回值是否为Unit)我们需要分离出专门返回winner的函数:

def winner(p1: Player, p2: Player): Player =
    if (p1.score > p2.score) p1 else p2

消除了副作用,函数的职责变得单一,我们就很容易对函数进行组合或重用了。除了可以打印winner之外,例如我们可以像下面的代码那样获得List中最终的获胜者:

val players = List(Player("Sue", 7), Player("Bob", 8), Player("Joe", 4))
val finalWinner = players.reduceLeft(winner)

函数的抽象有时候需要脑洞大开,需要敏锐地去发现变化点与不变点,然后提炼出函数。例如,当我们定义了这样的List之后,比较sum与product的异同:

sealed trait MyList[+T]
case object Nil extends MyList[Nothing]
case class Cons[+T](h: T, t: MyList[T]) extends MyList[T]

object MyList {
  def sum(ints: MyList[Int]):Int = ints match {
    case Nil => 0
    case Cons(h, t) => h + sum(t)
  }

  def product(ds: MyList[Double]):Double = ds match {
    case Nil => 1.0
    case Cons(h, t) => h * product(t)
  }

  def apply[T](xs: T*):MyList[T] =
    if (xs.isEmpty) Nil
    else Cons(xs.head, apply(xs.tail: _*))
}

sum与product的相同之处都是针对List的元素进行运算,运算规律是计算两个元素,将结果与第三个元素进行计算,然后依次类推。这就是在函数式领域中非常常见的折叠(fold)计算

def foldRight[A, B](l: MyList[A], z: B)(f: (A, B) => B):B = l match {
    case Nil => z
    case Cons(x, xs) => f(x, foldRight(xs, z)(f))
}

在引入了foldRight函数后,sum和product就可以重用foldRight了:

def sum(ints: MyList[Int]):Int = foldRight(ints, 0)(_ + _)
def product(ds: MyList[Double]):Double = foldRight(ds, 0.0)(_ * _)

在函数式编程的世界里,事实上大多数数据操作都可以抽象为filter,map,fold以及flatten几个操作。查看Scala的集合库,可以验证这个观点。虽然Scala集合提供了非常丰富的接口,但其实现基本上没有超出这四个操作的范围。

高阶函数

虽然Java 8引入了简洁的Lambda表达式,使得我们终于脱离了冗长而又多重嵌套的匿名类之苦,但就其本质,它实则还是接口,未能实现高阶函数,即未将函数视为一等公民,无法将函数作为方法参数或返回值。例如,在Java中,当我们需要定义一个能够接收lambda表达式的方法时,还需要声明形参为接口类型,Scala则省去了这个步骤:

def find(predicate: Person => Boolean)

结合Curry化(柯里化),还可以对函数玩出如下的魔法:

def add(x: Int)(y: Int) = x + y
val addFor = add(2) _
val result = addFor(5)

表达式add(2) _返回的事实上是需要接受一个参数的函数,因此addFor变量的类型为函数。此时result的结果为7。

当然,从底层实现来看,Scala中的所有函数其实仍然是接口类型,可以说这种高阶函数仍然是语法糖Scala之所以能让高阶函数显得如此自然,还在于它自己提供了基于JVM的编译器

丰富的集合操作

虽然集合的多数操作都可以视为对foreach, filter, map, fold等操作的封装,但一个具有丰富API的集合库,却可以让开发人员更加高效。例如Twitter给出了如下的案例,要求从一组投票结果(语言,票数)中统计不同程序语言的票数并按照得票的顺序显示:

val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10))
val orderedVotes = votes
  .groupBy(_._1)
  .map { case (which, counts) =>
  (which, counts.foldLeft(0)(_ + _._2))
}.toSeq
  .sortBy(_._2)
  .reverse

这段代码首先将Seq按照语言类别进行分组。分组后得到一个Map[String, Seq[(Stirng, Int)]]类型:

scala.collection.immutable.Map[String,Seq[(String, Int)]] = Map(scala -> List((scala,1), (scala,10), (scala,1)), java -> List((java,4)), python -> List((python,10)))

然后将这个类型转换为一个Map。转换时,通过foldLeft操作对前面List中tuple的Int值累加,所以得到的结果为:

scala.collection.immutable.Map[String,Int] = Map(scala -> 12, java -> 4, python -> 10)

之后,将Map转换为Seq,然后按照统计的数值降序排列,接着反转顺序即可。

显然,这些操作非常适用于数据处理场景。事实上,Spark的RDD也可以视为一种集合,提供了比Scala更加丰富的操作。此外,当我们需要编写这样的代码时,还可以在Scala提供的交互窗口下对算法进行spike,这是目前的Java所不具备的。

Stream

Stream与大数据集合操作的性能有关。由于函数式编程对不变性的要求,当我们操作集合时,都会产生一个新的集合,当集合元素较多时,会导致大量内存的消耗。例如如下的代码,除原来的集合外,还另外产生了三个临时的集合:

List(1,2,3,4).map (_ + 10).filter (_ % 2 == 0).map (_ * 3)

比较对集合的while操作,这是函数式操作的缺陷。虽可换以while来遍历集合,却又丢失了函数的高阶组合(high-level compositon)优势。

解决之道就是采用non-strictness的集合。在Scala中,就是使用stream。

并发与并行

Scala本身属于JVM语言,因此仍然支持Java的并发处理方式。若我们能遵循函数式编程思想,则建议有效运用Scala支持的并发特性。

除了Actor,Scala中值得重视的并发特性就是Future与Promise默认情况下,future和promise都是非阻塞的,通过提供回调的方式获得执行的结果。future提供了onComplete、onSuccess、onFailure回调。如下代码:

println("starting calculation ...")

val f = Future {
  sleep(Random.nextInt(500))
  42
}

println("before onComplete")
f.onComplete {
  case Success(value) => println(s"Got the callback, meaning = $value")
  case Failure(e) => e.printStackTrace
}

// do the rest of your work
println("A ..."); sleep(100)
println("B ..."); sleep(100)
println("C ..."); sleep(100)
println("D ..."); sleep(100)
println("E ..."); sleep(100)
println("F ..."); sleep(100)

sleep(2000)

f的执行结果可能会在打印A到F的任何一个时间触发onComplete回调,以打印返回的结果。注意,这里的f是Future对象。

我们还可以利用for表达式组合多个future,AKKA中的ask模式也经常采用这种方式:

object Cloud {
    def runAlgorithm(times: Int): Future[Int] = Future {
      Thread.sleep(times)
      times
    }
}
object CloudApp extends App {
  val result1 = Cloud.runAlgorithm(10)  //假设runAlgorithm需要耗费较长时间
  val result2 = Cloud.runAlgorithm(20)
  val result3 = Cloud.runAlgorithm(30)

  val result = for {
    r1     r2     r3   } yield (r1 + r2 + r3)

  result onSuccess {
    case result => println(s"total = $result")
  }

  Thread.sleep(2000)
}

这个例子会并行的执行三个操作,最终需要的时间取决于耗时最长的操作。注意,yield返回的仍然是一个future对象,它持有三个future结果的和。

promise相当于是future的工厂,只是比单纯地创建future具有更强的功能。这里不再详细介绍。

Scala提供了非常丰富的并行集合,它的核心抽象是splitter与combiner,前者负责分解,后者就像builder那样将拆分的集合再进行合并。在Scala中,几乎每个集合都对应定义了并行集合。多数情况下,可以调用集合的par方法来创建。

例如,我们需要抓取两个网站的内容并显示:

val urls = List("http://scala-lang.org",
  "http://agiledon.github.com")

def fromURL(url: String) = scala.io.Source.fromURL(url).getLines().mkString("\n")

val t = System.currentTimeMillis()
// par
urls.par.map(fromURL(_))
println
println("time: " + (System.currentTimeMillis - t) + "ms")

如果没有添加par方法,程序就会顺序抓取两个网站内容,效率差不多会低一半。

那么,什么时候需要将集合转换为并行集合呢?这当然取决于集合大小。但这并没有所谓的标准值。因为影响执行效率的因素有很多,包括CPU的类型、核数、JVM的版本、集合元素的workload、特定操作、以及内存管理等。

并行集合会启动多个线程来执行,默认情况下,会根据cpu核数以及jvm的设置来确定。如果有兴趣,可以选择两台cpu核数不同的机器分别运行如下代码:

(1 to 10000).par.map(i => Thread.currentThread.getName).distinct.size

这段代码可以获得线程的数量。

有人提问这种线程数量的灵活判断究竟取决于编译的机器,还是运行的机器?答案是和运行的机器有关。这事实上是由JVM的编译原理决定的。JVM的编译与纯粹的静态编译不同,Java和Scala编译器都是将源代码转换为JVM字节码,而在运行时,JVM会根据当前运行机器的硬件架构,将JVM字节码转换为机器码。这就是所谓的JIT(just-in-time)编译

Scala还有很多优势,包括模式匹配、隐式转换、类型类、更好的泛型协变逆变等,当然这些特性也是造成Scala变得更复杂的起因。我们需要明智地判断,控制自己卖弄技巧的欲望,在代码可读性与高效精简之间取得合理的平衡。

题外话

在推荐Scala时,提出质疑最多的往往不是Java程序员,而是负责团队的管理者,尤其是略懂技术或者曾经做过技术的管理者。他们会表示这样那样的担心,例如Scala的编译速度慢,调试困难,学习曲线高,诸如此类

编译速度一直是Scala之殇,由于它相当于做了两次翻译,且需要对代码做一些优化,这个问题一时很难彻底根治。

调试困难被吐槽得较激烈,这是因为Scala的调试信息总是让人难以定位。虽然在2.9之后,似乎已有不少改进,但由于类型推断等特性的缘故,相较Java而言,打印的栈信息仍有词不达意之处。曲线救国的方式是多编写小的、职责单一的类(尤其是trait),尽量编写纯函数,以及提高测试覆盖率。此外,调试是否困难还与开发者自身对于Scala这门语言的熟悉程度有关,不能将罪过一味推诿给语言本身。

至于学习曲线高的问题,其实还在于我们对Scala的定位,即确定我们是开发应用还是开发库。此外,对于Scala提供的一些相对晦涩难用的语法,我们尽可以不用。ThoughtWorks技术雷达上将“Scala, the good parts”放到Adopt,而非整个Scala,寓意意味深长。

通常而言,OO转FP会显得相对困难,这是两种根本不同的思维范式。张无忌学太极剑时,学会的是忘记,只取其神,我们学FP,还得尝试忘记OO。自然,学到后来,其实还是万法归一。OO与FP仍然有许多相同的设计原则,例如单一职责,例如分而治之。

对于管理者而言,最关键的一点是明白Scala与Java的优劣对比,然后根据项目情况和团队情况,明智地进行技术决策。我们不能完全脱离上下文去说A优于B。世上哪有绝对呢?

日渐成熟的Scala技术栈

Scala社区的发展

一门语言并不能孤立地存在,必须提供依附的平台,以及围绕它建立的生态圈。不如此,语言则不足以壮大。Ruby很优秀,但如果没有Ruby On Rails的推动,也很难发展到今天这个地步。Scala同样如此。反过来,当我们在使用一门语言时,也要选择符合这门语言的技术栈,在整个生态圈中找到适合具体场景的框架或工具。

当然,我们在使用Scala进行软件开发时,亦可以寻求庞大的Java社区支持;可是,如果选择调用Java开发的库,就会牺牲掉Scala给我们带来的福利。幸运的是,在如今,多数情况你已不必如此。伴随着Scala语言逐渐形成的Scala社区,已经开始慢慢形成相对完整的Scala技术栈。无论是企业开发、自动化测试或者大数据领域,这些框架或工具已经非常完整地呈现了Scala开发的生态系统。

快速了解Scala技术栈

若要了解Scala技术栈,并快速学习这些框架,一个好的方法是下载typesafe推出的Activator。它提供了相对富足的基于Scala以及Scala主流框架的开发模板,这其中实则还隐含了typesafe为Scala开发提供的最佳实践与指导。

那么,是否有渠道可以整体地获知Scala技术栈到底包括哪些框架或工具,以及它们的特性与使用场景呢?感谢Lauris Dzilums以及其他在Github的Contributors。在Lauris Dzilums的Github上,他建立了名为awesome-scala的Repository,搜罗了当下主要的基于Scala开发的框架与工具,涉及到的领域包括:
https://github.com/lauris/awesome-scala
Database
Web Frameworks
i18n
Authentication
Testing
JSON Manipulation
Serialization
Science and Data Analysis
Big Data
Functional Reactive Programming
Modularization and Dependency Injection
Distributed Systems
Extensions
Android
HTTP
Semantic Web
Metrics and Monitoring
Sbt plugins

持久化

归根结底,对数据的持久化主要还是通过JDBC访问数据库。但是,我们需要更好的API接口,能更好地与Scala契合,又或者更自然的ORM。如果希望执行SQL语句来操作数据库,那么运用相对广泛的是框架ScalikeJDBC,它提供了非常简单的API接口,甚至提供了SQL的DSL语法。例如:

val alice: Option[Member] = withSQL {
    select.from(Member as m).where.eq(m.name, name)
  }.map(rs => Member(rs)).single.apply()

如果希望使用ORM框架,Squeryl应该是很好的选择。该框架目前的版本为0.9.5,已经比较成熟了。Squeryl支持按惯例映射对象与关系表,相当于定义一个POSO(Plain Old Scala Object),从而减少框架的侵入。若映射违背了惯例,则可以利用框架定义的annotation如@Column定义映射。框架提供了org.squeryl.Table[T]来完成这种映射关系。

因为可以运用Scala的高阶函数、偏函数等特性,使得Squeryl的语法非常自然,例如根据条件对表进行更新:

update(songs)(s =>
  where(s.title === "Watermelon Man")
  set(s.title := "The Watermelon Man",
      s.year  := s.year.~ + 1)
)

分布式系统

Finagle的血统高贵,来自过去的寒门,现在的高门大族Twitter。Twitter是较早使用Scala作为服务端开发的互联网公司,因而积累了非常多的Scala经验,并基于这些经验推出了一些颇有影响力的框架。由于Twitter对可伸缩性、性能、并发的高要求,这些框架也极为关注这些质量属性。Finagle就是其中之一。它是一个扩展的RPC系统,以支持高并发服务器的搭建。我并没有真正在项目中使用过Finagle,大家可以到它的官方网站获得更多消息。

对于分布式的支持,绝对绕不开的框架还是AKKA。它产生的影响力如此之大,甚至使得Scala语言从2.10开始,就放弃了自己的Actor模型,转而将AKKA Actor收编为2.10版本的语言特性。许多框架在分布式处理方面也选择了使用AKKA,例如Spark、Spray。AKKA的Actor模型参考了Erlang语言,为每个Actor提供了一个专有的Mailbox,并将消息处理的实现细节做了良好的封装,使得并发编程变得更加容易。AKKA很好地统一了本地Actor与远程Actor,提供了几乎一致的API接口。AKKA也能够很好地支持消息的容错,除了提供一套完整的Monitoring机制外,还提供了对Dead Letter的处理。

AKKA天生支持EDA(Event-Driven Architecture)。当我们针对领域建模时,可以考虑针对事件进行建模。在AKKA中,这些事件模型可以被定义为Scala的case class,并作为消息传递给Actor。借用Vaughn Vernon在《实现领域驱动设计》中的例子,针对如下的事件流:

b3befbf88fce807450e3a893c11c6035.png

我们可以利用Akka简单地实现:

case class AllPhoneNumberListed(phoneNumbers: List[Int])
case class PhoneNumberMatched(phoneNumbers: List[Int])
case class AllPhoneNumberRead(fileName: String)

class PhoneNumbersPublisher(actor: ActorRef) extends ActorRef {
    def receive = {
        case ReadPhoneNumbers =>
        //list phone numbers

        actor ! AllPhoneNumberListed(List(1110, ))
    }
}

class PhoneNumberFinder(actor: ActorRef) extends ActorRef {
    def receive = {
        case AllPhoneNumberListed(numbers) =>
            //match

            actor ! PhoneNumberMatched()
    }
}

val finder = system.actorOf(Prop(new PhoneNumberFinder(...)))
val publisher = system.actorOf(Prop(new PhoneNumbersPublisher(finder)))

publisher ! ReadPhoneNumbers("callinfo.txt")

若需要处理的电话号码数据量大,我们可以很容易地将诸如PhoneNumbersPublisher、PhoneNumberFinder等Actors部署为Remote Actor。此时,仅仅需要更改客户端获得Actor的方式即可。

Twitter实现的Finagle是针对RPC通信,Akka则提供了内部的消息队列(MailBox),而由LinkedIn主持开发的Kafka则提供了支持高吞吐量的分布式消息队列中间件。这个顶着文学家帽子的消息队列,能够支持高效的Publisher-Subscriber模式进行消息处理,并以快速、稳定、可伸缩的特性很快引起了开发者的关注,并在一些框架中被列入候选的消息队列而提供支持,例如,Spark Streaming就支持Kafka作为流数据的Input Source。

HTTP

严格意义上讲,Spray并非单纯的HTTP框架,它还支持REST、JSON、Caching、Routing、IO等功能。Spray的模块及其之间的关系如下图所示:

192d2a3946debd01546fe318cf294f72.png

我在项目中主要将Spray作为REST框架来使用,并结合AKKA来处理领域逻辑。Spray处理HTTP请求的架构如下图所示:

90dc40241044583284415369e93d058e.png

Spray提供了一套DSL风格的path语法,能够非常容易地编写支持各种HTTP动词的请求,例如:

trait HttpServiceBase extends Directives with Json4sSupport {
     implicit val system: ActorSystem
     implicit def json4sFormats: Formats = DefaultFormats
     def route: Route
}

trait CustomerService extends HttpServiceBase {
     val route =
          path("customer" / "groups") {
               get {
                    parameters('groupids.?) {
                         (groupids) =>
                              complete {
                                   groupids match {
                                        case Some(groupIds) =>
                    ViewUserGroup.queryUserGroup(groupIds.split(",").toList)
                                        case None => ViewUserGroup.queryUserGroup()
                                   }
                              }
                    }
               }
          } ~
          path("customers" / "vip" / "failureinfo") {
               post {
                    entity(as[FailureVipCustomerRequest]) {
                         request =>
                              complete {
                                   VipCustomer.failureInfo(request)
                              }
                    }
               }
          }
}

个人认为,在进行Web开发时,完全可以放弃Web框架,直接选择AngularJS结合Spray和AKKA,同样能够很好地满足Web开发需要。

Spray支持REST,且Spray自身提供了服务容器spray-can,因而允许Standalone的部署(当然也支持部署到Jetty和tomcat等应用服务器)。Spray对HTTP请求的内部处理机制实则是基于Akka-IO,通过IO这个Actor发出对HTTP的bind消息。例如

IO(Http) ! Http.Bind(service, interface = "0.0.0.0", port = 8889)

可以编写不同的Boot对象去绑定不同的主机Host以及端口。这些特性都使得Spray能够很好地支持当下较为流行的Micro Service架构风格。

Web框架

正如前面所说,当我们选择Spray作为REST框架时,完全可以选择诸如AngularJS或者Backbone之类的JavaScript框架开发Web客户端。客户端能够处理自己的逻辑,然后再以JSON格式发送请求给REST服务端。这时,我们将模型视为资源(Resource),视图完全在客户端。JS的控制器负责控制客户端的界面逻辑,服务端的控制器则负责处理业务逻辑,于是传统的MVC就变化为VC+R+C模式。这里的R指的是Resource,而服务端与客户端则通过JSON格式的Resource进行通信。

若硬要使用专有的Web框架,在Scala技术栈下,最为流行的就是Play Framework,这是一个标准的MVC框架。另外一个相对小众的Web框架是Lift。它与大多数Web框架如RoR、Struts、Django以及Spring MVC、Play不同,采用的并非MVC模式,而是使用了所谓的View First。它驱动开发者对内容生成与内容展现(Markup)形成“关注点分离”。

Lift将关注点重点放在View上,这是因为在一些Web应用中,可能存在多个页面对同一种Model的Action。倘若采用MVC中的Controller,会使得控制变得非常复杂。Lift提出了一种所谓view-snippet-model(简称为VSM)的模式。

7a79c1396335f2b1fe03544a99cb2235.png

View主要为响应页面请求的HTML内容,分为template views和generated views。Snippet的职责则用于生成动态内容,并在模型发生更改时,对Model和View进行协调。

大数据

Spark...与许多专有的大数据处理平台不同,Spark建立在统一抽象的RDD之上,使得它可以以基本一致的方式应对不同的大数据处理场景,包括MapReduce,Streaming,SQL,Machine Learning以及Graph等。这即Matei Zaharia所谓的“设计一个通用的编程抽象(Unified Programming Abstraction)。

由于Spark具有先进的DAG执行引擎,支持cyclic data flow和内存计算。因此相比较Hadoop而言,性能更优。在内存中它的运行速度是Hadoop MapReduce的100倍,在磁盘中是10倍。

由于使用了Scala语言,通过高效利用Scala的语言特性,使得Spark的总代码量出奇地少,性能却在多数方面都具备一定的优势(只有在Streaming方面,逊色于Storm)。

还有Kafka、Flink(Java Scala混合)。

- END - 猜你喜欢:

数据中台实战系列笔记

浅谈OLAP系统核心技术点(建议收藏)

HBase基础面试题总结

Hive基础面试题总结

MapReduce和YARN基础面试题总结

HDFS基础面试题总结

e8924cda9315cda4c4a0ac5919e46bad.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值