型安全なサンタクロース クリスマス前にコンパイルエラー - Re.Ra.Ku アドベントカレンダー day 24

アドベントカレンダー24日め。本日はクリスマス・イブです! サンタクロースのみなさんは無事にプレゼントを用意できましたでしょうか?

ところで、クリスマスといえば、となりのヤングジャンプなどで連載されている「ブラックナイトパレード」、面白いですね。この作品で初めて知ったのですが、クリスマスは良い子には赤いサンタがやってきてプレゼントをくれる一方、悪い子のところには黒いサンタがやってきて石炭をくれるという文化があるそうですね(なぜ石炭なんだ……)。

今日はせっかくのクリスマス・イブなので、これをソフトウェアとしてScalaでモデリングしてみましょう!

まずは型を定義しよう

さて、まずはサンタクロース、プレゼント、子供の型をそれぞれ定義します。

trait SantaClaus

trait Child

trait Gift

サンタクロースには赤いサンタと黒いサンタがおり、子供には良い子と悪い子がいます。また、プレゼントにはおもちゃと石炭を用意しておきましょう。

trait SantaClaus
class RedSantaClaus extends SantaClaus {
  override def toString: String = "赤いサンタクロース"
}
class BlackSantaClaus extends SantaClaus {
  override def toString: String = "黒いサンタクロース"
}

trait Gift
class Toy extends Gift {
  override def toString: String = "おもちゃ"
}
class Coal extends Gift {
  override def toString: String = "石炭"
}

trait Child
class GoodChild extends Child {
  override def toString: String = "良い子"
}
class BadChild extends Child {
  override def toString: String = "悪い子"
}

それぞれがモデリングできました。今回はアプリケーションのエントリポイントに、giveGiftというメソッドを生やして、サンタが子供にプレゼントを上げるという行為をモデリングしてみましょう。

object Xmas {
  def main(args: Array[String]):Unit = println(giveGift(new RedSantaClaus, new GoodChild, new Toy))

  def giveGift(s: SantaClaus, c: Child, g: Gift) = {
    s"${s}が${c}に${g}をあげたよ!"
  }
}

このプログラムを走らせると、「赤いサンタクロースが良い子におもちゃをあげたよ!」が出力されますね。よかった、たかしくんもにっこりです。

しかし、よく考えてみましょう。このプログラムは本当にクリスマスをきちんとモデリングできているでしょうか。もしもmainメソッドを以下のように書き換えたら?

object Xmas {
  def main(args: Array[String]):Unit = println(giveGift(new RedSantaClaus, new GoodChild, new Coal)) // !!!!

  def giveGift(s: SantaClaus, c: Child, g: Gift) = {
    s"${s}が${c}に${g}をあげたよ!"
  }
}

「赤いサンタクロースが良い子に石炭をあげたよ!」と出力されました。これではせっかく良い子に過ごしていたたかしくんが可愛そうです。このような事故をなんとかして防ぐことはできないでしょうか……。

このような悲しい事故を防ぐためには、giveGiftの引数の型の組み合わせを制限してあげればよさそうです。Scalaではそのようなときに型クラスを利用することで型に制限をつけることができます。

scalaでの型クラスについては手前味噌ですがScala の implicit parameter は型クラスの一種とはどういうことなのかを参照してください。

まずは制約のためにTypeClassedXmasという型クラスを定義し、「赤いサンタと良い子とおもちゃ」の組み合わせ、「黒いサンタと悪い子と石炭」の組み合わせをそれぞれ型インスタンスにしましょう。

object Xmas {
  trait TypeClassedXmas[S <: SantaClaus, C <: Child , G <: Gift]
  implicit object GoodChildXmas extends TypeClassedXmas[RedSantaClaus, GoodChild, Toy]
  implicit object BadChildXmas extends TypeClassedXmas[BlackSantaClaus, BadChild, Coal]

  ...
}

そして、giveGiftメソッドは型パラメータを取りimplicit parameterでこの型クラスを受け取るようにします。

  def giveGift[S <: SantaClaus, C <: Child, G <: Gift](s: S, c: C, g: G)(implicit ev: TypeClassedXmas[S, C, G]): String = {
    s"${s}が${c}に${g}をあげたよ!"
  }

さて、これでgiveGiftの引数は「赤いサンタと良い子とおもちゃ」の組み合わせか「黒いサンタと悪い子と石炭」の組み合わせ以外を受け取ったときにコンパイルエラーとなるようになりました!

object Xmas {
  trait TypeClassedXmas[S <: SantaClaus, C <: Child , G <: Gift]
  implicit object GoodChildXmas extends TypeClassedXmas[RedSantaClaus, GoodChild, Toy]
  implicit object BadChildXmas extends TypeClassedXmas[BlackSantaClaus, BadChild, Coal]

  def main(args: Array[String]):Unit = {
    println(giveGift(new RedSantaClaus, new GoodChild, new Toy))
    println(giveGift(new BlackSantaClaus, new BadChild, new Coal))
    // println(giveGift(new BlackSantaClaus, new GoodChild, new Coal)) -> コンパイルエラー!!!
  }

  def giveGift[S <: SantaClaus, C <: Child, G <: Gift](s: S, c: C, g: G)(implicit ev: TypeClassedXmas[S, C, G]): String = {
    s"${s}が${c}に${p}をあげたよ!"
  }
}

これであわてんぼうの黒いサンタも赤いサンタも、間違えたプレゼントを間違えた子供にプレゼントしようと思ったら、クリスマス実行前にコンパイルエラーになるようになりましたね!

今年もすべての子どもたちが幸せになれるクリスマスだと良いですね。それでは、We wish you a merry Xmas and a happy New Year!