はじめに
こんにちは。マイクロアド21年卒サーバーサイドエンジニアの陶山(id:suyama_naoki)です。普段は主にDSP (UNIVERSE Ads) の開発を行なっています。
今回の記事では、UNIVERSE AdsでのScala 幽霊型の活用方法について共有させていただきます。
Scalaの幽霊型 (Phantom Type) とは
幽霊型 (Phantom Type) とは、コンパイル時には検査されるが、コンパイル後のプログラムでは消えているような型パラメータのことです。 コンパイル時の型検査を利用したデザインパターンとして認知されています。
具体例
以下はとあるドアを開け閉めするプログラムです。
ドアが開いているか閉まっているかの状態を表すDoorState
と開け閉めするメソッドを持つDoor
クラスを定義しています。
Door
クラスでは幽霊型を用いることによって以下の制約を設けています。
- ドアが閉じている場合のみ開くことができる
- ドアが開いている場合のみ閉じることができる
見ていただくと分かる通り、閉じているドアを開いたり、開いているドアを閉じるプログラムは成功しています。
しかし、開いているドアを開こうとすると型引数がClosed
ではないという内容のエラーが発生しています。
scala> sealed trait DoorState | sealed trait Open extends DoorState | sealed trait Closed extends DoorState trait DoorState trait Open trait Closed scala> case class Door[State <: DoorState]() { | def open(implicit ev: State =:= Closed): Door[Open] = { | println("Door opened!") | Door[Open]() | } | def close(implicit ev: State =:= Open): Door[Closed] = { | println("Door closed!") | Door[Closed]() | } | } class Door scala> Door[Closed]().open Door opened! val res0: Door[Open] = Door() scala> Door[Open]().close Door closed! val res1: Door[Closed] = Door() scala> Door[Open]().open ^ error: Cannot prove that Open =:= Closed.
このように、幽霊型を用いることによって型パラメータによる制約を設け、制約を守っていないプログラムがあった場合、コンパイルエラーにより気づくことができます。
UNIVERSE Adsでの幽霊型の活用方法
UNIVERSE Ads (弊社で開発しているDSP) では配信候補の広告の値段を比較する際に、幽霊型を用いて通貨の種類が同じ場合のみ比較できるようなモデリングをしています。
以下に広告の値段比較の簡易化したプログラムを載せています。
Currency.scala
通貨を表すクラスです。
sealed abstract class Currency(val id: CurrencyId, val code: String) object Currency { case object JPY extends Currency(CurrencyId(1), "JPY") case object USD extends Currency(CurrencyId(2), "USD") case object CNY extends Currency(CurrencyId(3), "CNY") case object KRW extends Currency(CurrencyId(4), "KRW") }
AdPrice.scala
広告の値段を表すクラスです。
幽霊型を用いることによって、比較するAdPriceの型パラメータが一致していないとコンパイルエラーがでるようにしています。
import domain.model.currency.Currency case class AdPrice[C <: Currency](amount: BigDecimal, currency: C) extends Ordered[AdPrice[C]] { override def compare(that: AdPrice[C]): Int = amount.compare(that.amount) }
AdPriceのcompareメソッドを呼び出す
以下のプログラムをコンパイルします。
adPrice1
とadPrice2
の型パラメータはCurrency.JPY.type
ですが、adPrice3
はCurrency.USD.type
ですね。
object Main extends App { val adPrice1: AdPrice[Currency.JPY.type] = AdPrice(5.0, Currency.JPY) val adPrice2: AdPrice[Currency.JPY.type] = AdPrice(10.0, Currency.JPY) val adPrice3: AdPrice[Currency.USD.type] = AdPrice(15.0, Currency.USD) adPrice1.compare(adPrice2) adPrice1.compare(adPrice3) }
コンパイルを実行すると、幽霊型による型検査が行われます。adPrice1
とadPrice2
の比較は問題ないですが、adPrice1
とadPrice3
の比較をしている箇所で型不一致のエラーが発生しました。
type mismatch; found : domain.model.selector.AdPrice[domain.model.currency.Currency.USD.type] required: domain.model.selector.AdPrice[domain.model.currency.Currency.JPY.type] adPrice1.compare(adPrice3)
幽霊型を用いたモデリングのおかげで、誤った実装をした際にコンパイルエラーによってバグを未然に防げるので嬉しいですね。
まとめ
今回の記事では、Scalaの幽霊型についての特徴やUNIVERSE Adsでの活用方法を共有させていただきました。
Scalaの幽霊型をこれから使用する方や、知識として学びたい方の参考になれば嬉しいです!