Scala

同じ問題をオブジェクト指向な方法と関数型な方法で解いてみる

Scalaを使って同じ問題をオブジェクト指向なアプローチで解く方法と関数型なアプローチで解く方法とを例示出来るといいなーと思って書いてみます。

はじめに

こんなコードがあったとします。
制約としては同じ通貨どうししか足し合わせられないようにしたい。
違反した場合はコンパイルエラーに。

object Main extends App {

  trait Currency

  case class Yen(value: Int) extends Currency {
    def +(other: Yen) = Yen(value + other.value)
  }

  case class Dollar(value: Int) extends Currency {
    def +(other: Dollar) = Dollar(value + other.value)
  }

  val y1 = Yen(300)
  val y2 = Yen(500)
  val y3 = y1 + y2
  println(y3) // Yen(800)

  val d1 = Dollar(3)
  val d2 = Dollar(5)
  val d3 = d1 + d2
  println(d3) // Dollar(8)

  val x = y1 + d1 // コンパイルエラー

次に、具体的な通貨クラスを指定するのではなくて任意の同じ通貨クラスどうしを足し合わせられるようなちょっと抽象的な関数を追加したくなったとする。
以下を定義する。

def add[C <: Currency](c1: C, c2: C) = c1 + c2

が、これはコンパイルエラー。
Currencyに + メソッドなんて定義されてないから。

じゃ、定義するか。

...というように進むのがOOPに慣れ親しんだプログラマが自然に思いつくアプローチといったところでしょうか。
データに操作を増やして、親側に共通のメソッドを追加して抽象的なプログラムに耐えられるようにしていく。
少なくとも自分はこんな感じでした。

で、いざやろうとするとこれはなかなか難しい事に気づきます。
2つの通貨クラスの親のCurrencyに定義する+メソッドはどうやって自分を継承した子クラスの型情報を取ればいいだろう??

雰囲気的には以下のような感じで動けばいいのですが、、、、これはうまく動きません。

trait Currency {
  def +(other: this.type): this.type
}

Way1

この方向でいくとするとこのようにちょっと冗長だけど型情報Selfを増やしてやらないと解決は厳しそう。

trait Currency[Self] {
  def +(other: Self): Self
}

case class Yen(value: Int) extends Currency[Yen] {
  override def +(other: Yen): Yen = Yen(value + other.value)
}

case class Dollar(value: Int) extends Currency[Dollar] {
  override def +(other: Dollar): Dollar = Dollar(value + other.value)
}

def add[C <: Currency[C]](c1: C, c2: C): C = c1 + c2

ま、でも、これで無事動きました。

Way2

今度は別のアプローチで。

まず通貨クラスはこんな感じに定義します。

sealed trait Currency

case class Yen(value: Int) extends Currency
case class Dollar(value: Int) extends Currency

先のadd関数を次のように少し変える。

def add[C <: Currency](c1: C, c2: C)(implicit addable: Addable[C]): C = addable.run(c1, c2)

Addableを定義。

trait Addable[T] {
  def run(v1: T, v2: T): T
}

implicit val y = new Addable[Yen] {
  def run(v1: Yen, v2: Yen) = Yen(v1.value + v2.value)
}

implicit val d = new Addable[Dollar] {
  def run(v1: Dollar, v2: Dollar) = Dollar(v1.value + v2.value)
}

これでaddは正しく動くようになります。

ただしこのままだとadd関数を使った足し合わせは出来ても val y3 = y1 + y2 のような処理が書けないので以下を追加。

implicit class AddableOp[T: Addable](v1: T) {
  def +(v2: T): T = implicitly[Addable[T]].run(v1, v2)
}

これで動きました。

こっちは通貨クラスをあくまでデータ型として扱い、データどうしの操作はデータ型毎に用意した関数として定義するようになっています。

さいごに

別のアプローチがあるとか、もっといい書き方があるとかあればコメントお願いします。
また、こういう事例がたくさんあるようなサイトとかあれば知りたい。