このトレーニングを用意した意図は、長くakkaを触り続けている私自身が、akkaの公式ドキュメントを独力で読み解いて、akkaを用いたアプリケーションを走らせるのはハードルが高いと、いつも感じていたからです。akkaの非同期処理、クラスタリングによる水平スケール、CQRSパターンの採用など技術的な面白さやメリットは理解してもらいやすいと思いますが、いざ自分で動かそうとしても、akkaに関してはその一歩を踏み出すハードルが高くないか?という懸念をいつも持っていました。akka公式ドキュメントは情報量が豊富で、ほとんどの機能についてもれなく、詳しく書かれているものの、初心者が一番最初に読んでわかりやすい文章ではなさそうだと私は感じています。(ただし、公式ドキュメントのIntroduction to the Exampleの部分は初心者にもわかりやすいと思います。)
class Main {
publicenum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
publicstaticvoid main(String[ ] args) {
Day today = Day.THURSDAY;
switch (today) {
case MONDAY :
case TUESDAY :
case WEDNESDAY:
case THURSDAY :
System.out.println("Low energy");
break;
case FRIDAY :
System.out.println("Happiest day of the week?");
break;
case SATURDAY:
System.out.println("The world is yours");
break;
case SUNDAY:
System.out.println("Time ticking to the hell");
break;
}
}
}
短いシンタックスenumeration/列挙体を定義でき、C言語のenumと比べても高機能なJavaのenumですが、Scala視点から見るとAlgebraic Data Typesの表現が出来ないことを物足りなく感じるかもしれません。そこを踏まえた上でいよいよScalaのenumeration/列挙体の歴史を紹介していきます。
sealedabstractclass Day
case object Monday extends Day
case object Tuesday extends Day
case object Wednesday extends Day
case object Thursday extends Day
case object Friday extends Day
case object Saturday extends Day
case object Sunday extends Day
Algebraic Data Typesを表現することも出来ます。
//実際のScala 2.13 Option定義を簡略化したものですsealedabstractclass Option[+A]
finalcaseclass Some[+A](x: A) extends Option[A]
case object None extends Option[Nothing]
この手法はやや記述量が多く、例えばHaskellで同等のenumeration/列挙体やAlgebraic Data Typesを実現する場合と比べるとその差がわかります。
object Colours extends Enumeration {
val Red, Amber, Green = Value
}
object Day extends Enumeration {
val Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday = Value
}
object Functions {
def f(x: Colours.Value) = "That's a colour" def f(x: WeekDays.Value) = "That's a weekday"
}
double definition:
def f(x: Playground.this.Colours.Value): String at line 12 and
def f(x: Playground.this.WeekDays.Value): String at line 13
have same type after erasure: (x: Enumeration#Value)String
object Day extends Enumeration {
val Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday = Value
}
def g(day: Day.Value) = day match {
//コンパイラはexhaustiveness check警告を出さないcase Day.Monday => println("Mon!")
case Day.Tuesday => println("Tue!")
}
//scala.MatchErrorが投げられる
g(Day.Thursday)
3つ目の方法はJavaのenumを利用する方法で、Scalaプログラムの中に.javaファイルを置いてそれを.scalaファイルからimportします。JavaとScalaを共存させるプロジェクトで、かつenumeration/列挙体をJava側で定義する必要があればこの選択肢を選ぶことになります。利点はJavaのenumそのままです。欠点はScalaプログラムの中で使う場合パターンマッチでexhaustiveness checkが効かない、Algebraic Data Typesとして使えないなどがあげられます。
In my personal opinion, when taken alone, neither of these criticisms is strong enough to warrant introducing a new language feature. But taking them together could shift the balance.
What's more, once we have eliminated case bodies we also have eliminated scope confusion. All that remains are the case parameters and extends clause.
Arguably, if we choose an ADT decomposition of a problem it's good style to write all methods using pattern matching instead of overriding individual cases. So this removes an unnecessary choice.
Web APIからの入力やデータベースからの入力はScalaアプリケーションと外部の境界になるので、チェックを行いStringやIntなどの型からScalaのenumで表す型へと変換する必要があります。しかし、チェックが必要なごく一部の処理とその他大部分のShoppingCategory型に変換されたあとの安全な処理を明確に分けることができます。
enum ShoppingCategory {
case Coats, Jackets, KnitWear, Shirts, Pants
}
def doSomething(category: ShoppingCategory): Unit = category match {
case ShoppingCategory.Coats => …
case ShoppingCategory.Jackets => …
case ShoppingCategory.KnitWear => …
case ShoppingCategory.Shirts => …
case ShoppingCategory.Pants => …
}
// [warn] -- [E029] Pattern Match Exhaustivity Warning:
// [warn] 12 | def doSomething(category: ShoppingCategory): Unit = category match {
// [warn] | ^^^^^^^^
// [warn] | match may not be exhaustive.
// [warn] |
// [warn] | It would fail on pattern case: Socks
// [warn] one warning found
enumの利用例
その他のenumの利用例を見てみましょう。例えばenumで次のように曜日を定義できます。
enum Day {
case Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
}
//play-jsonのDotty対応が完了すればimplicitではなくgivenを使えるはずimport scala.language.implicitConversions
implicit val readsColor: Reads[Color] = Reads[Color] {
case JsString(str) => str match {
case Color(color) => JsSuccess(color)
case _ => JsError(str + " is not a valid color")
}
case json: JsValue => JsError(json.toString + " failed to convert to color")
}
caseclass Pen(
owner: String,
color: Color
)
implicit val reads: Reads[Pen] = (
(JsPath \ "owner").read[String] and
(JsPath \ "color").read[Color]
)(Pen.apply _)
val penJson = Json.obj("owner" -> "Alice", "color" -> "red")
println(penJson.validate[Pen])