Case Class
当要定义复杂的数据类型时,可以使用Case classes。如下面所示,定义一个JSON数据表示:
{
“firstName”: “John”,
“lastName”: “Smith”,
“address”: {
“streetAddress”: “21 2 nd Street”,
“state”: “NY”,
“postalCode”: 10021
},
“phoneNumbers”: [
{ “type”: “home”, “number”: “212 555 -1234” },
{ “type”: “fax”, “number”: “646 555 -4567” }
]
}
通过Scala的case class可以抽象为:
abstract class JSON
case class JSeq (elems: List[JSON]) extends JSON
case class JObj (bindings: Map[String, JSON]) extends JSON
case class JNum (num: Double) extends JSON
case class JStr (str: String) extends JSON
case class JBool(b: Boolean) extends JSON
case object JNull extends JSON
所以,可以定义上面的JSON变量为:
val data = JObj(Map(
"firstName" -> JStr("John"),
"lastName" -> JStr("Smith"),
"address" -> JObj(Map(
"streetAddress" -> JStr("21 2nd Street"),
"state" -> JStr("NY"),
"postalCode" -> JNum(10021)
)),
"phoneNumbers" -> JSeq(List(
JObj(Map(
"type" -> JStr("home"), "number" -> JStr("212 555-1234")
)),
JObj(Map(
"type" -> JStr("fax"), "number" -> JStr("646 555-4567")
)) )) ))
Pattern Matching
如果我们想要用JSON的格式进行打印要怎么做呢?Scala提供的Pattern Matching语法可以非常方便和优雅的写出递归语法。如下定义了打印函数:
abstract class JSON {
def show: String = this match {
case JSeq(elems) => "[" + (elems map (_.show) mkString ", ") + "]"
case JObj(bindings) =>
val assocs = bindings map {
case (key, value) => "\"" + key + "\": " + value.show
}
"{" + (assocs mkString ", ") + "}"
case JNum(num) => num.toString
case JStr(str) => "\"" + str + "\""
case JBool(b) => b.toString
case JNull => "null"
}
}
Function
有一个地方需要讨论一下,以下pattern matching
代码块中返回的类型是什么?
{ case (key, value) => key + ”: ” + value }
在前面的打印代码中,map
函数需要的参数类型是JBinding => String
的函数类型,其中JBinding
是String
和JSON
的pair
,也就是type JBinding = (String, JSON)
。
Scala也是一门面向对象语言,其中所有具体的类型都是一种class
或trait
。函数类型也不例外,比如说JBinding => String
的类型其实是Function1[JBinding, String]
,其中Function1
是一个trait
,JBinding
和String
是类型参数。
下面是trait Function1
的大体表示:
trait Function1[-A, +R] {
def apply(x: A): R
}
其中[-A, +R]
表示的是范型中的逆变和协变,以后会在其它文章中介绍。
综上,上面的pattern matching
代码块其实是一个Function1
类型的实例,即:
new Function1[JBinding, String] {
def apply(x: JBinding) = x match {
case (key, value) => key + ”: ” + show(value)
}
}
将函数定义成trait
的好处是我们可以继承函数类型。
例如Scala中的Map
类型继承了函数类型,如下:
trait Map[Key, Value] extends (Key => Value)
就能通过map(key)
的形式,也就是函数调用来由key得到value。
Scala中的Sequences也是继承了函数类型,如下:
trait Seq[Elem] extends (Int => Elem)
所以可以通过elems(i)
的形式来由序列的下表访问对应的元素。
Partial Matches
通过上面的知识可以知道,下面的pattern matching
代码块,
{ case "ping" => "pong" }
可以得到一个String => String
的函数类型,即:
val f: String => String = { case "ping" => "pong" }
但是如果调用f(”pong”)
将会返回MatchError
的异常,这显而易见。那么问题来了,“Is there a way to find out whether the function can be applied to a given argument before running it?”
在Scala中可以这么解决,定义PartialFunction
,如下所示:
val f: PartialFunction[String, String] = { case "ping" => "pong" }
f.isDefinedAt("ping") // true
f.isDefinedAt("pong") // false
PartialFunction
和Function
的区别就是PartialFunction
定义了isDefinedAt
函数。如果我们定义{ case "ping" => "pong" }
是一个PartialFunction
类型,那么Scala编译器将会展开为:
new PartialFunction[String, String] {
def apply(x: String) = x match {
case "ping" => "pong"
}
def isDefinedAt(x: String) = x match {
case "ping" => true
case _ => false
}
}
总结
这一节中表达JSON数据格式的例子非常有趣,我把完整的代码放在下面,Scala的代码非常简洁。
abstract class JSON {
def show: String = this match {
case JSeq(elems) => "[" + (elems map (_.show) mkString ", ") + "]"
case JObj(bindings) =>
val assocs = bindings map {
case (key, value) => "\"" + key + "\": " + value.show
}
"{" + (assocs mkString ", ") + "}"
case JNum(num) => num.toString
case JStr(str) => "\"" + str + "\""
case JBool(b) => b.toString
case JNull => "null"
}
}
case class JSeq(elems: List[JSON]) extends JSON
case class JObj(bindings: Map[String, JSON]) extends JSON
case class JNum(num: Double) extends JSON
case class JStr(str: String) extends JSON
case class JBool(b: Boolean) extends JSON
case object JNull extends JSON
object Main {
def main(args: Array[String]) {
val data = JObj(Map(
"firstName" -> JStr("Yu"),
"lastName" -> JStr("Gong"),
"address" -> JObj(Map(
"streetAddress" -> JStr("NY"),
"state" -> JStr("NY")
)),
"phoneNumbers" -> JSeq(List(
JObj(Map(
"type" -> JStr("home"), "number" -> JStr("12233")
)),
JObj(Map(
"type" -> JStr("fax"), "number" -> JStr("22222")
))
))
))
println(data.show)
}
}
稍微思考一下,如果用传统的面向对象语言(比如Java)来对JSON数据格式进行抽象,可以如何定义呢?
也可以定义基类JSON
和子类JSeq JObj JNum JStr JBool JNull
,如果要实现打印函数,可能就需要在每个子类中实现自己的打印函数,也就是写六个show
函数。
如果你有什么想法和思考,欢迎前来讨论。