Scala-Functions and Pattern Matching

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的函数类型,其中JBindingStringJSONpair,也就是type JBinding = (String, JSON)
Scala也是一门面向对象语言,其中所有具体的类型都是一种classtrait。函数类型也不例外,比如说JBinding => String的类型其实是Function1[JBinding, String],其中Function1是一个traitJBindingString是类型参数。
下面是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

PartialFunctionFunction的区别就是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函数。
如果你有什么想法和思考,欢迎前来讨论。

你可能感兴趣的:(Scala-Functions and Pattern Matching)