2015/09/06

[Scala In Depth] Scala Case Classes

[Scala In Depth] Scala Case Classes

[Scala In Depth] Scala Case Classes

這篇同步發佈在我的BlogGist

這篇文章,基本上是參考這篇Scala Case Classes In Depth來寫的,閱讀吸收後以我的例子與描述方式以中文寫下。

大家對Case Class基本的認識

  • 定義一個簡單的類型(class)並在宣告的時候直接定義好有哪些參數。
case class User(region: String, id: String, name: String)
  • 可以簡單的用來建立某個類型(class)的物件,因為case class已經實作了apply,所以可省略new這個宣告方式,來建立新物件。
val joe = User("TW","123","Joe")
joe: User = User(TW,123,Joe)
  • 每個參數的宣告都被加上了val的前綴(prefix),也就是這個class基本上每個property都是immutable(不能被修改的)。
val region = joe.region
// 下面這行在compile會錯誤
joe.region = "US"
<console>:23: error: reassignment to val
  • 當然有一些基本的implementation,像是hashCode, euqalstoString…等,而在scala中,==基本上就是執行equal,所以在case class用==去比較的時候,是用整個結構會去比的(而非比reference)。
scala> val joe2 = User("TW","123","Joe")
joe2: User = User(TW,123,Joe)

scala> joe == joe2
res: Boolean = true

scala> val joe3 = User("TW","124","Joe")
joe3: User = User(TW,124,Joe)

scala> joe == joe3
res54: Boolean = false
  • 要複製的時候,可以使用copy,若有要修改的屬性,再copy當中宣告即可
scala> joe.copy(region="US")
res: User = User(US,123,Joe)
  • 有實作unapply,所以在pattern matching可以直接使用。
scala> val User(region,id,name) = joe
region: String = TW
id: String = 123
name: String = Joe

scala> joe match {
     |   case User(_,id,_) => println(s"id:[$id]")
     | }
id:[123]
  • 如果在宣告case class的時候,並沒有要給任何的參數,就像是定義ADT(Algebraic Data Type)的時候,我們有可能會宣告到一個沒有任何參數的class,這時候可以用case object來宣告。
sealed trait Life[+T]
case class Alive[T](value: T) extends Life[T]
case object Dead extends Life[Nothing]

scala> def check(life:Life[User]) = life match {
     |   case Alive(user) => println(user.name)
     |   case Dead => println("dead")
     | }
check: (life: Life[User])Unit

scala> check(Alive(joe))
Joe

scala> check(Dead)
dead

一些case class進階的認識

  • 如果你需要建立一個function,而這個function要傳入所有產生這個class的參數,最後要產生這個class的object的話,你可以使用apply
// 這裡的 _ 是因為要把apply這個function指派給變數來用,如果不加上 _ 的話,會被當成是要執行這個function而又因為後面沒有帶所需要的參數造成compile錯誤。
scala> val userCreator = User.apply _
userCreator: (String, String, String) => User = <function3>

scala> val uerica = userCreator("TW","66","Uerica")
uerica: User = User(TW,66,Uerica)
  • curried,如果你要把上面這個用法,拆成是curry的話,可以使用curried的方式,把這些參數給拆開來,方便組合使用。
scala> val curriedUser = User.curried
curriedUser: String => (String => (String => User)) = <function1>
// 上面其實就變成了一種這樣 (region: String)(id: String)(name: String) : User 的function

scala> val twUserCreator = curriedUser("TW")
twUserCreator: String => (String => User) = <function1>

scala> val samael = twUserCreator("111")("Samael")
samael: User = User(TW,111,Samael)
  • tupled,前面提到的apply是建立一個function並傳入對應class properties數量的參數,而若想要傳入的是一個tuple,就可以使用tupled
scala> User.tupled
res67: ((String, String, String)) => User = <function1>
//上面這個與之前的apply不同,多了一組刮號,因為這傳入的是tuple

scala> val tupledJoe = ("TW","123","Joe")
tupledJoe: (String, String, String) = (TW,123,Joe)

scala> User.tupled(tupledJoe)
res: User = User(TW,123,Joe)
  • unapply,當我們需要一個function,是傳入某個類型的物件,然後希望回傳的是Option[TupleN[A1, A2, ..., AN]],將N個參數分別包成tuple,然後用Option包起來(這邊用Option有可能是因為要handle這物件可能不存在的情況),這種時候就能直接使用unapply
scala> val toOptionOfTuple = User.unapply _
toOptionOfTuple: User => Option[(String, String, String)] = <function1>

scala> toOptionOfTuple(joe)
res: Option[(String, String, String)] = Some((TW,123,Joe))

當使用curried的形式來建立一個case class時…

這篇文章提到了一點,就是我們可以用curried的形式來建立一個case class,就像是下面的例子。

scala> case class Person(fingerPrint: Long)(name: String, dress: String)
defined class Person

scala> val j = Person(123123l)("Joe","Suit")

至於什麼時候要這樣用?以及該怎麼用?用了會發生什麼事?,我們分別來說明。

什麼時候要這樣用?

如果真的有一種情況,就是在你的設計需求上,希望一個物件的比對是只依照某個部份的property,例如我的例子是只要指紋是一樣的,就是同一個人,不論他的名字、裝扮如何。

scala> val j = Person(123123l)("Joe","Suit")

scala> val q = Person(123123l)("Andy","Casual")

scala> j==q
res: Boolean = true

該怎麼用?用了會發生什麼事?

curried的部份,並沒有實作前面提到case class的那些特性,也就是說,下面這幾點都會有問題:

  • 沒有實作val的前綴,所以不能直接存取這些properties
scala> j.name
<console>:24: error: value name is not a member of Person
       j.name
         ^

所以我們必須自己加上val的宣告

scala> case class Person(fingerPrint: Long)(val name: String, val dress: String)
defined class Person

scala> val j = Person(123123l)("Joe","Suit")
j: Person = Person(123123)

scala> j.name
res: String = Joe
  • 當然用copy的時候,也就知道後面curried的部份也沒有實作copy,所以你必需帶所有的參數。
scala> j.copy()(name = "David")
<console>:25: error: not enough arguments for method copy: (name: String, dress: String)Person.
Unspecified value parameter dress.
       j.copy()(name = "David")
               ^

scala> j.copy()(name = "David", dress = "Dirty")
res80: Person = Person(123123)
  • 其他的像是tupled當然也都不能用嘍!

因為case class繼承Product這個trait而所得到的能力

  • def productArity: Int,這個function可以得到這個case class的object有多少個properties。
scala> joe.productArity
res: Int = 3
// 這3個就是region, id, name
  • def productElement(n: Int): Any,取得某個指定的Element。注意:這裡的回傳type是Any哦!
scala> joe.productElement(2)
res: Any = Joe
  • def productIterator: Iterator[Any],把每個properties iterate出來。
    這裡提醒一些不熟scala的朋友,iterator這裡若用map會發現怎麼沒有被執行到,要先toList或是用foreach才會執行,這是因為iterator的特性。
scala> joe.productIterator.foreach(println)
TW
123
Joe
  • def productPrefix: String,以字串的形式取得case class object的型態。
scala> joe.productPrefix
res: String = User

My World