Scala 3.0 enumの紹介 (前半) enumの利点と利用例

マーベリック株式会社、技術広報のリチャード 伊真岡です。今回はScala 3.0の主要機能の一つenumについて紹介します。

2019年12月現在Scalaの最新バージョンは2.13ですが、2020年中にScala 3.0のリリースが予定されています。正式リリースに向けてScala 3.0用コンパイラはDottyというプロジェクト名で開発されていて、多くのScala 3.0向け機能がすでにDottyから利用可能となっています。

そんなScala 3.0で導入されるenumは、Scalaの創始者であるEPFL(スイス連邦工科大学ローザンヌ校)のMartin Odersky教授が「Scala初心者に勧めたい3.0新機能ランキングの中で1位」と述べています。

enumを使いこなすことはScalaプログラマにとって、特にScala初心者にとって大きな力となるでしょう。enumに近い機能はScala 2.xでも利用可能でしたが、Scala 3.0のenumではより便利で簡潔な記述ができます。もしScala 3.0のenum「のみ」について知りたい方はDotty公式のenumのページを読んでください。この記事はenumの背景や利用例から説明するので少し回りくどくなっています。

また記事は前後半にわかれていて、前半である今回の記事はenumの利点や利用例を解説し、後日公開する後半ではenumの歴史を振り返り、なぜScala 3.0で新しくenumを実装する必要があったのかを説明します。Algebraic Data Typesに関する話題は後半の記事で触れます。

enumの利点

Scala 3.0のenumは昔からプログラミングの世界で利用されてた列挙体と呼ばれる概念を改めてScalaの中で実装したものです。列挙体は歴史あるプログラミング言語であるC言語やPascalでも古くから利用可能でした。多くの言語で利用可能な列挙体、そのScala 3.0実装であるenumはどんな場面で利用するのが効果的なのでしょうか?それは変数の取りうる値を制限したいときです。

例を挙げて説明してみましょう。いまWebアプリケーションの画面があってドロップダウンリストから属性を選び、その値をバックエンドであるScalaアプリケーション側で処理する機能を考えます。ドロップダウンリストから属性を選ぶので、属性は予め決められた少数の選択肢の中から選ぶことになります。つまりドロップダウンリストの選択肢以外は属性として不正な値です。

f:id:maverick-techblog:20191209044057p:plain

選択肢のデータをString型でこのように表現できます。

Webアプリケーション上での表示 Stringで表現したシステム内部での属性値
コート ”Coats”
ジャケット ”Jackets”
ニット(セーター) ”KnitWear”
シャツ ”Shirts”
パンツ ”Pants”

しかしString型を使ってしまうと、上記以外の不正なStringが属性値として間違って使われてしまう可能性があります。

// 属性をStringで表現すると、ありとあらゆる不正なStringが可能!
“”                               //空String
“Socks”                          //靴下は売っていない
“あqwせdrftgyふじこlp;” //全くの不正String

ソースコード上のあらゆる場所でこの属性を表すString変数があらわれるたびに「このデータは不正なStringになってないだろうか?」という心配がつきまといます。そのため例えば下記のisValid(shoppingCategory)のような 形で、属性値を表すStringであるshoppingCategoryが正しい属性値か不正な値かをチェックする必要があります。

def doSomething(shoppingCategory: String, ...): Result = {
  if(isValid(shoppingCategory))
    ... //Exceptionをthrowする?
    ... //あるいはEitherのLeftを戻り値として返す?
}

このdoSomethingはshoppingCategoryが不正な値であったときにExceptionをthrowするか、あるいはEitherのLeftを戻り地として返却する実装を持つとします。するとdoSomethingが呼ばれるすべての場所でtry-catchでExceptionをハンドルするかEitherのLeftをハンドルします。これは面倒ですし、ソースコードの見た目も読みづらくなります。

f:id:maverick-techblog:20191209044934p:plain

このようにあらゆる場所で不正な値のチェックを行うのは大変です。Web APIと外部の境界、データベースとScalaアプリケーションとの境界など、境界部分のみでStringのチェックを行い、それ以外の場所では不正なStringが入り込む余地を残さないのが理想的でしょう。しかしそういった理想的な状態のソースコードを保つことは難しく、いつしか不注意なソースコードの変更で不正なStringが混入する可能性があります。いったん不正なStringを混入させてしまったら、あらゆる場所でのチェックが必要になり、チェックを行った際エラー処理まで考えなくてはなりません。

こういった時にenumを使うとプログラムの安全性がたかまり、コンパイラの助けによって不正な値を型レベルで防いでくれます。以下のように選択可能な属性値を定義しておくと、ShoppingCategory型の変数は不正な値を取ることはありえません。

// Scala 3.0 または Dotty
enum ShoppingCategory {
  case Coats, Jackets, KnitWear, Shirts, Pants
}

Web APIからの入力やデータベースからの入力はScalaアプリケーションと外部の境界になるので、チェックを行いStringやIntなどの型からScalaのenumで表す型へと変換する必要があります。しかし、チェックが必要なごく一部の処理とその他大部分のShoppingCategory型に変換されたあとの安全な処理を明確に分けることができます。

f:id:maverick-techblog:20191209044948p:plain

またScalaのenumはパターンマッチと相性がよく、以下のように書くと網羅的に場合分けを記述できます。コンパイラがexhaustiveness checkを行って場合分けの漏れを防いでくれるので、enumを使ったパターンマッチ網羅性に起因するエラーを未然に防ぐことができます。

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 => …
}

パターンマッチ網羅性に起因するエラーを見るために、下記のようにShoppingCategoryにSocksを加えてみましょう。オンラインショッピングストアで新しく靴下(Socks)の取り扱いを始めた想定です。

// 最後にSocksを追加
enum ShoppingCategory {
  case Coats, Jackets, KnitWear, Shirts, Pants, Socks
}

//ここでコンパイルエラー!Socksのcaseが含まれていない
def doSomething(category: ShoppingCategory): Unit = category match {
  case ShoppingCategory.Coats => …
  case ShoppingCategory.Jackets => …
  case ShoppingCategory.KnitWear => …
  case ShoppingCategory.Shirts => …
  case ShoppingCategory.Pants => …
}

このとき、上記のコードはSocksに対するcaseが含まれていないので以下のようなコンパイラWarningが表示されます。コンパイラの設定によってこれをWarningからエラーに変え、より安全にできます。

// [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
}

Java標準のDayOfWeekですでに曜日の定義があるのでありがたみは薄いかもしれませんが、何らかの理由でDayOfWeekへの依存を避けたい場合、例えば階層型アーキテクチャを採用していて、ドメイン層にはすべて自前の型を使うルールがあるといった場合には利用できます。

この際enumでは自動的にordinalという0から始まるInt値が付与されるので、「月曜日は火曜日の前」といった比較を行うことができます。

val l = List(Day.Sunday, Day.Friday, Day.Tuesday, Day.Wednesday, Day.Saturday, Day.Monday, Day.Thursday)
    
println(l)
//List(Sunday, Friday, Tuesday, Wednesday, Saturday, Monday, Thursday)

println(l.sortWith{(day1, day2) => day1.ordinal < day2.ordinal})
//List(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)

さらにvaluesというメソッドも自動で付与され、当該のenumに属するすべての値をArrayにして取得できます。

Day.values.foreach(println) //Day.valuesはArray[Day]型
//Sunday
//Friday
//Monday
//Saturday
//Tuesday
//Wednesday
//Thursday

曜日だと7種類しかありませんが、もっと種類の多いもの、例えばHTTPのステータスコード200 Successや404 Not Found等をenumで表現できます。現在のScalaのWebフレームワークやライブラリではvalを多数並べて、ステータスコード自体はIntの値で表現している物が多いです。

実用上はvalとIntを使った定義で問題なく、不注意であったとしてもわざわざ不正なHTTPステータスコードのInt値を利用したコードを書く人は少ないでしょう。しかし、もしこれらのWebフレームワークやライブラリが誕生する前からScala 3.0が存在していたら、ステータスコードは型安全なenumを使って表現されていたかもしれません。

JSONのserialization, deserializationにおいてもenumを利用する機会は多いでしょう。Web APIを開発するとき、HTTPリクエストのボディにJSONを利用するとします。JSONのあるmemberが取りうる値を制限したいときjson-schemaであればこのようなschema定義をするでしょう。

{
  "type": "string",
  "enum": ["red", "amber", "green"]
}

Scala 3.0のenumを使えばScalaのコードで同様の制限を表現できます。Scalaで最もよく使われるJSONライブラリのひとつであるplay-jsonを使うとこう書けます。play-jsonの現行バージョンはまだDotty対応が完了していないので、下記のサンプルではimplicitを使っています。Dotty対応が完了したらgivenが使えるはずです。

enum Color {
  case Red, Amber, Green
}

object Color {
  //型レベルでこのパターンマッチがRed, Amber, Greenを網羅していると保証できない…
  def unapply(str: String): Option[Color] = str match {
    case "red" => Some(Red)
    case "amber" => Some(Amber)
    case "green" => Some(Green)
    case _ => None
  }
}

またunapplyメソッドのパターンマッチの右側がenumの値すべてを網羅しているか、コンパイラはチェックできないので、この記事の「enumの理論的側面」の項目で紹介しているようにテストコードと組み合わせて網羅性をチェックするとよいでしょう。

//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")
}
case class 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])

こうして不正な値が入り込む可能性をWeb APIとその外部との境界部分に限定します。境界部分で上記のようなコードによってStringからenumへの変換を行ってしまえば、Web APIの内部では型安全なenumによって属性値を表現できます。

あるいはMySQLのようなリレーショナル・データベースではenum型をデータベース内で利用できます。

CREATE TABLE items (
    name VARCHAR(40),
    size ENUM('coats', 'jackets', 'knit_wear', 'shirts', 'pants')
);

このときScala側のenum ShoppingCategoryとデータベース側のenumの対応を付ければ、安全かつ自然な属性表現ができます。

(enumの利用例としてFinite State Machineの実装を紹介しようと思ったのですが、記事が長くなってきたので後半の記事で改めて紹介します。)

enumを利用しない方がよい例

enumは万能のツールではありません。enumを使うのが適切でない例として、利用可能な値が頻繁に追加・削除される用途が挙げられます。

例えばタスク管理ツールなどのラベルや、進捗管理ツールの進捗ステージ名などにenumを使うとしましょう。これらは、それぞれのツールの利用者が様々なラベル名、進捗ステージ名を自由につけて使うことが予想されるので、ツールの利用者全体では利用可能な値の数が膨大になりますし、高頻度でそのラベル名や進捗ステージ名が更新されます。

f:id:maverick-techblog:20191209221504p:plain

こういったときはenumによってソースコード内で利用可能な値を制限するのではなく、素直にデータベース内にラベルや進捗ステージの名前を保存し、高頻度な更新に備えるのが良いでしょう。

enumの理論的側面

「enumの利点」で述べたようにenumを使うメリットは、型レベルで不正な値を防ぐことによってプログラムの安全性が上がることです。英語の情報になりますが、Quoraでもその点に触れている質問があり

”Strings (or Ints to represent certain meaning) are prone to errors”

という解説がなされています。「変数やパラメタ、あるいは戻り値に不正な値が使われる可能性」はソースコード上においてエラーの主要な原因のひとつです。先に述べたようにソースコード上のあらゆる場所で不正な値のチェックをしなければならないとしたら、それは非常に頭を悩ます問題です。

この点をうまく理論的に説明している資料として、Scalaとは別の言語ですがElmのドキュメントがあります。不正な値を型レベルで防ぐという点について、身近な例をまじえつつ集合論や濃度といった抽象度の高い概念を用いて説明しています。関数型プログラミングが好きな人にはElmのドキュメントの該当部分をよむと思考が整理されスッキリとした気分になるかもしれません。

Elmにおけるプログラミングの中で最も重要なテクニックのひとつは、コード中で可能な値を現実世界での正当な値に完全に一致させることです。これにより不正なデータの入り込む余地がなくなるため、…

また、取りうる値の範囲が型によって決まっているというのは、テストの面でも有利です。型によって取りうる値が制限されているため、そもそもテストを書かなくてよい場面も出てくるでしょう。それでもテストを書く必要がある場合以下のような単純なfor comprehensionですべてのケースを網羅できます。

enum Day {
  case Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
}  

enum ShoppingCategory {
  case Coats, Jackets, KnitWear, Shirts, Pants
}
for {
  day <- Day.values
  category <- ShoppingCategory.values
} doTest(doSomeMethod(day, category))

まとめ

今回の記事ではScala 3.0のenumを題材に、列挙体という概念の基本に立ち戻って使い方を紹介してきました。冒頭で言及したようにScala 2系でも列挙体として使うことのできる機能はあったので、この記事で紹介したことの多くはScala 2でも違った方法によって達成可能です。

そこで後半の内容である次回の記事では、Scala 3.0 enumが誕生した背景やScala 2系までで利用可能だった類似の機能との比較などを紹介します。

参考文献: