Scala 新手眼中的十种有趣用法

·1224 字·6 分钟
Scala jvm scala
n3xtchen
作者
n3xtchen
Sharing Funny Tech With You

TL;DR

作为一个初学者,经过一个月系统的系统学习,用惯了动态语言的我来说,Scala 编译器型语言的编程体验真的是太棒了。作为阶段性的总结,我将给出我对 Scala 最佳初体验的 Top 10:

漂亮的操作系统调用方式 #

Scala 2.9里也提供类似功能:新增加了package: scala.sysscala.sys.process, 这些代码最初由 SBT (a simple build tool for Scala)项目贡献,主要用于简化与操作系统进程的交互与调用。现在看看用法:

scala> import scala.language.postfixOps
import scala.language.postfixOps

scala> import scala.sys.process._
import scala.sys.process._

scala> "java -version" !
java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)
res2: Int = 0

当你引入 scala.sys.process 后,scala 会为你给字符串或数组动态注入 ! 方法来调用系统命令。

这是我见过调用 shell 最爽最优雅的方式之一,不是吗?现在看个复杂点的例子,爬取一个页面:

scala> import java.io.File
import java.io.File

scala> import java.net.URL
import java.net.URL

scala> new URL("http://www.scala-lang.org/") #> new File("scala-lang.html") !
res4: Int = 0

这里我们又学到一个新的操作符 #>,结果重定向。这条命令等价于如下 Bash

$ curl "http://www.scala-lang.org/ > scala-lang.html

看看结果:

scala> "ls" !
...
scala-lang.html
...
res6: Int = 0

参考: http://www.scala-lang.org/api/rc2/scala/sys/process/package.html

管道(Pipeline) #

scala> import scala.language.implicitConversions
import scala.language.implicitConversions

scala> object Pipeline {
     |   implicit class toPiped[V](value:V) {
     |     def |>[R] (f : V => R) = f(value)
     |   }
     | }
defined module Pipeline

scala> import Pipeline._
import Pipeline._

scala> 1 |> ((i:Int)=> i*10)
res3: Int = 10

这样就可以将,函数返回的值作为后面函数参数,一条链式调用看起来是那么的优雅(你觉得呢?)。

短短几行的代码,足以让你领教到 隐式转化(implicit)的威力吧!因为这个话题比较大,就不在这里作详细阐述了。

使用 {...} 替代 (...) 的语法糖 #

声明一个多参数表函数,如下

scala> def m[A](s: A)(f: A=> String) = f(s)
m: [A](s: A)(f: A => String)String

你可以这样调用它:

scala> m(100)(i => s"$i + $i")
res2: String = 100 + 100

你可以使用 {...} 替代 (...) 的语法糖,就可以把上面改写成下面的模式

scala> m(100){ i => s"$i + $i" }
res3: String = 100 + 100

竟然可以如此优雅优雅地调用函数,看起来就像标准的块代码(像 iffor 表达式)

创建自己的字符解释器 #

import scala.util.parsing.json._

object Interpolators {
  implicit class jsonForStringContext(val sc: StringContext) {
    def json(values: Any*): JSONObject = {
		val keyRE = """^[\s{,]*(\S+):\s*""".r
		val keys = sc.parts map {
			case keyRE(key) => key
			case str => str
		}
		val kvs = keys zip values
		JSONObject(kvs.toMap)
	}
  }
}

import Interpolators._

val name = "Dean Wampler"
val book = "Programming Scala, Second Edition"

val jsonobj = json"{name: $name, book: $book}" 
println(jsonobj)

哈哈,有意思吧!

在大部分情况下,中缀(Infix)标记和后缀(Postfix)标记可省略 #

1 + 2

这段语句大家再熟悉不过了,但是在 Scala 中,所有的表达式都是方法,实际上完整的写法,如下:

1.+(2)

即,+ 是整型对象(在 Scala 中,一切都是对象)的一个方法,如果了解到这里,会不会觉得能写成之前的样子很神奇。这里是 Scala 一个特性:

  • 如果一个对象有一个带有一个参数的方法, 它的中缀标记可以省略,在这里,点号和括号都可以省略。

类似的,如果一个方法没有参数表,你调用它的时候,可以它的后缀标记(即括号)可以省略:

scala> 1 toString	// 1.toString()
warning: there were 1 feature warning(s); re-run with -feature for details
res2: String = 1

因为忽略后缀,有时候会让人很迷惑,方法?属性?傻傻分不清楚!所以在版本 2.10 之后,如果没有明确告诉编译器的话,会给出一个警告:

scala> import scala.language.postfixOps
scala> 1 toString
res2: String = 1

警报解除,神奇吧?现在我们看一个复杂的例子:

def isEven(n: Int) = (n % 2) == 0
List(1, 2, 3, 4) filter isEven foreach println
// 等同于 .filter(isEven).foreach(println)

简便的类声明: #

Java 党可以看过来,不论是 IDE 和文本编辑器党,代码中到处充斥着大量的 settergetter

// src/main/java/progscala2/basicoop/JPerson.java
package progscala2.basicoop;

public class JPerson {
  private String name;
  private int    age;

  public JPerson(String name, int age) {
    this.name = name;
    this.age  = age;
  }

  public void   setName(String name) { this.name = name; }
  public String getName()            { return this.name; }

  public void setAge(int age) { this.age = age;  }
  public int  getAge()        { return this.age; }
}

看看,Scala 可以这么写:

class Person(var name: String, var age: Int)

有没有像流泪的感觉,哈哈,That is it!如果想要覆盖某些 settergetter,只要类声明中,覆盖相应方法即可。

var,自然有 val,如果你的对象属性都是不可变,那还可以使用如下声明:

case class Person(name: String, age: Int)

用 Java 的同学会不会很心动啊?^_^

密封类型(Sealed)强制其子类只能定义在同一个文件中 #

seals 关键字可以用在 classtrait 上。

第一个作用如题,举个栗子,Scala 源码中 List 的实现用到 sealed 关键字:

scala> class NewList extends List
<console>:7: error: illegal inheritance from sealed class List
       class NewList extends List

这样子,妈妈再也不用担心我的类被人滥用。

如果就这么个功能,怎么能称得上 Top 10 呢?在看一个黑魔法:

scala> sealed abstract class Drawing
defined class Drawing

scala> case class Point(x: Int, y: Int) extends Drawing
defined class Point

scala> case class Circle(p: Point, r: Int) extends Drawing
defined class Circle

scala> case class Cylinder(c: Circle, h: Int) extends Drawing
defined class Cylinder

如果你如之前,少写了其中一個案例:

scala> def what(d: Drawing) = d match {
     |     case Point(_, _)    => "点"
     |     case Cylinder(_, _) => "柱"
     | }
<console>:14: warning: match may not be exhaustive.
It would fail on the following input: Circle(_, _)
       def what(d: Drawing) = d match {
                              ^
what: (d: Drawing)String

编译器在告訴你,有些模式的类型你沒有列在 match 運算式的案例串(Case sequence)之中。你应该每個都列出來才合理:

scala> def what(d: Drawing) = d match {
     |     case Point(_, _)    => "点"
     |     case Circle(_, _)   => "圆"
     |     case Cylinder(_, _) => "柱"
     | }
what: (d: Drawing)String

有時候,你使用別人密封過的案例类别,但也許你真的只想比对其中几個案例类型,如果不想要编译器饶人的警告,则可以在最后使用万用字元模式(_),例如:

scala> def what(d: Drawing) = d match {
     |     case Point(_, _)    => "點"
     |     case Cylinder(_, _) => "柱"
     |     case _              => "" // 作你想作的事,或者丟出例外
     | }
what: (d: Drawing)String

如果你真心不想要使用万用字元作额外处理,那么还可以可以使用 @unchecked 标注來告訴编译器住嘴:

scala> def what(d: Drawing) = (d: @unchecked) match {
     |     case Point(_, _)    => "點"
     |     case Cylinder(_, _) => "柱"
     | }
what: (d: Drawing)String

参考:http://openhome.cc/Gossip/Scala/SealedClass.html

有趣的权限控制 private[] #

protectedprivate 這表示权限限制到 x 的范围。

class Some {
    private val x = 10
    def doSome(s: Some) = s.x + x
}

对于大多数语言,访问控制就严格无非就这两种。在 Scala 中,可以更加严格,让 x 完全无法透过实例存取,则可以使用 private[this],這表示私有化至 this 实例本身才可以存取,也就是所謂物件私有(Object-private),例如以下就通不過编译了:

class Some {
    private[this] val x = 10
    def doSome(s: Some) = s.x + x  // 编译错误
}

作为入门,就知道这里就可以了,可以跳到下一个话题了!

如果你看过 Spark(一个分布式计算框架)的源码,会发现这样的权限控制符到处都是,所以这个还是有必要搞清楚的。

如果能看懂下面这段代码,那你对 Scala 的域访问控制算是了解了:

package scopeA {
  class Class1 {
    private[scopeA]   val scopeA_privateField = 1
    protected[scopeA] val scopeA_protectedField = 2
    private[Class1]   val class1_privateField = 3
    protected[Class1] val class1_protectedField = 4
    private[this]     val this_privateField = 5
    protected[this]   val this_protectedField = 6
  }

  class Class2 extends Class1 {
    val field1 = scopeA_privateField
    val field2 = scopeA_protectedField
    val field3 = class1_privateField     // ERROR
    val field4 = class1_protectedField
    val field5 = this_privateField       // ERROR
    val field6 = this_protectedField
  }
}

package scopeB {
  class Class2B extends scopeA.Class1 {
    val field1 = scopeA_privateField     // ERROR
    val field2 = scopeA_protectedField
    val field3 = class1_privateField     // ERROR
    val field4 = class1_protectedField
    val field5 = this_privateField       // ERROR
    val field6 = this_protectedField
  }
}

不懂也没关系,后面会花一个大篇幅来讲这块内容。

类型擦除和 implicit #

scala> :paste
object M {
  def m(seq: Seq[Int]): Unit = println(s"Seq[Int]: $seq")
  def m(seq: Seq[String]): Unit = println(s"Seq[String]: $seq")
}
<ctrl-d>
<console>:8: error: double definition:
method m:(seq: Seq[String])Unit and
method m:(seq: Seq[Int])Unit at line 7
have same type after erasure: (seq: Seq)Unit
       def m(seq: Seq[String]): Unit = println(s"Seq[String]: $seq")

由于历史原因,JVM 忘记了 参数化类型的参数类型。例如例子中的 Seq[Int]Seq[String]JVM 看到的只是 Seq,而附加的参数类型对于 JVM 来说是不可见,这就是传说中的类型擦除。于是乎,从 JVM 的视角来看,代码是这样子的:

object M {
  // 两者都是接受一个 Seq 参数,返回Unit
  def m(seq: Seq): Unit = println(s"Seq[Int]: $seq")
  def m(seq: Seq): Unit = println(s"Seq[String]: $seq")
}

这不就报错了吗?重复定义方法。那怎么办呢?(JAVA 语言的痛点之一)Scala 给你提供一种比较优雅的解决方案,使用隐式转换:

scala> :paste
object M {
	implicit object IntMarker 
	implicit object StringMarker
	
	// 对于 JVM 来说是,接受 Seq 参数和 IntMarker.type 型 i 参数,
	// 返回 Unit
	def m(seq: Seq[Int])(implicit i: IntMarker.type): Unit =
println(s"Seq[Int]: $seq")

	// 对于 JVM 来说是,接受 Seq 参数和 StringMarker.type 型 i 参数,
	// 返回 Unit
	// 
	def m(seq: Seq[String])(implicit s: StringMarker.type): Unit =
println(s"Seq[String]: $seq")
}
<ctrl-d>
scala> import M._
scala> m(List(1,2,3))	// 在调用的时候,忽略隐式类型
scala> m(List("one", "two", "three"))

异常捕捉与 Scalaz #

先来看看,异常捕捉语句:

try {
	source = Some(Source.fromFile(fileName))
	val size = source.get.getLines.size
	println(s"file $fileName has $size lines")
} catch {
	case NonFatal(ex) => println(s"Non fatal exception! $ex")
} finally {
  for (s <- source) {
    println(s"Closing $fileName...")
    s.close
  }
}

这里是打开文件的操作,并且计算行数;NonFatal 非致命错误,如内存不足之类的非致命错误,将会被抛掉。finnaly 操作结束后,关闭文件。这是 Scala 模式匹配的又一大应用场景,你会发现倒是都是模式匹配:

	 case NonFatal(ex) => ...

如果每条异常的处理语句只是单条的话,Scala 写起来应该会挺爽的。

Scalaz 验证 #

在传统的异常处理,无法一次性汇总运行过程中的错误。如果我们在做表单验证的时候,我们就需要考虑到这个场景。而传统的做法就是通过一层层 try ... catch ...,把错误追加到一个列表中来实现,而 scalaz 中提供一个适用于这个场景的封装,直接看例子吧:

import scalaz._, std.AllInstances._

/* 验证用户名,非空和只包含字母 */
def validName(key: String, name: String):
    Validation[List[String], List[(String,Any)]] = {
  val n = name.trim  // remove whitespace
  if (n.length > 0 && n.matches("""^\p{Alpha}$""")) Success(List(key -> n))
  else Failure(List(s"Invalid $key <$n>"))
} 

/* 验证数字,并且大于0 */
def positive(key: String, n: String):
    Validation[List[String], List[(String,Any)]] = {
  try {
    val i = n.toInt
    if (i > 0) Success(List(key -> i))
    else Failure(List(s"Invalid $key $i"))
  } catch {
    case _: java.lang.NumberFormatException =>
      Failure(List(s"$n is not an integer"))
  }
}

/* 验证表单 */
def validateForm(firstName: String, lastName: String, age: String):
    Validation[List[String], List[(String,Any)]] = {
  validName("first-name", firstName) +++ validName("last-name", lastName) +++
    positive("age", age)
}

validateForm("Dean", "Wampler", "29")
validateForm("", "Wampler", "0")
// Returns: Failure(List(Invalid first-name <>, Invalid age 0))
//告知你名字和年龄填写有误
validateForm("Dean", "", "0") 
// Returns: Failure(List(Invalid last-name <>, Invalid age 0)) 
// 告知你姓氏和年龄填写有误
validateForm("D e a n", "", "29")
// Returns: Failure(List(Invalid first-name <D e a n>, Invalid last-name <>)) 
// 告知你名字和姓氏填写错误

这方式还不错吧?

就到这里就结束了,写着写着,就写这么多了,赶紧收住。