Case Class
當要定義復雜的數(shù)據(jù)類型時,可以使用Case classes。如下面所示,定義一個JSON數(shù)據(jù)表示:
{
“firstName”: “John”,
“l(fā)astName”: “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語法可以非常方便和優(yōu)雅的寫出遞歸語法。如下定義了打印函數(shù):
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函數(shù)需要的參數(shù)類型是JBinding => String的函數(shù)類型,其中JBinding是String和JSON的pair,也就是type JBinding = (String, JSON)。
Scala也是一門面向對象語言,其中所有具體的類型都是一種class或trait。函數(shù)類型也不例外,比如說JBinding => String的類型其實是Function1[JBinding, String],其中Function1是一個trait,JBinding和String是類型參數(shù)。
下面是trait Function1的大體表示:
trait Function1[-A, +R] {
def apply(x: A): R
}
其中[-A, +R]表示的是范型中的逆變和協(xié)變,以后會在其它文章中介紹。
綜上,上面的pattern matching代碼塊其實是一個Function1類型的實例,即:
new Function1[JBinding, String] {
def apply(x: JBinding) = x match {
case (key, value) => key + ”: ” + show(value)
}
}
將函數(shù)定義成trait的好處是我們可以繼承函數(shù)類型。
例如Scala中的Map類型繼承了函數(shù)類型,如下:
trait Map[Key, Value] extends (Key => Value)
就能通過map(key)的形式,也就是函數(shù)調用來由key得到value。
Scala中的Sequences也是繼承了函數(shù)類型,如下:
trait Seq[Elem] extends (Int => Elem)
所以可以通過elems(i)的形式來由序列的下表訪問對應的元素。
Partial Matches
通過上面的知識可以知道,下面的pattern matching代碼塊,
{ case "ping" => "pong" }
可以得到一個String => String的函數(shù)類型,即:
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的區(qū)別就是PartialFunction定義了isDefinedAt函數(shù)。如果我們定義{ 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
}
}
總結
這一節(jié)中表達JSON數(shù)據(jù)格式的例子非常有趣,我把完整的代碼放在下面,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)
}
}
稍微思考一下,如果用傳統(tǒng)的面向對象語言(比如Java)來對JSON數(shù)據(jù)格式進行抽象,可以如何定義呢?
也可以定義基類JSON和子類JSeq JObj JNum JStr JBool JNull,如果要實現(xiàn)打印函數(shù),可能就需要在每個子類中實現(xiàn)自己的打印函數(shù),也就是寫六個show函數(shù)。
如果你有什么想法和思考,歡迎前來討論。