読者です 読者をやめる 読者になる 読者になる

( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

Scala2.10.0のDependent method typesと型クラスを組み合わせた『The Magnet Pattern』がヤバい件

これが……型の力かッ……!!

f:id:yuroyoro:20130123192116j:plain

spray | Blog » The Magnet Patternという記事で、「The Magnet Pattern」というデザインパターンが紹介されている。


これは、メソッドオーバーロードで解決していた問題を、型クラスとDependent method typesを組み合わせて置き換えることで、オーバーロードの際の様々な制約(Type Erasureなど)を突破し、より柔軟な拡張性を得ることができるというもの。このパターンでは、引数の型に応じて異なる結果型を返すようにできる。


この記事で、今まで何のために使われるのかわからんかったDependent method typesの有効性が理解でき、あらためて型の力を思い知った。
以前に"Generalized type constraints"(Scalaで<:<とか=:=を使ったgeneralized type constraintsがスゴすぎて感動した話 - ( ꒪⌓꒪) ゆるよろ日記) を知った時以来の感動だったので、勢いで書いてみた。

Dependent method types ってなんぞ?

簡単に言うと、引数の値に応じて結果型が変わるメソッドを定義できるということ。

Scala 2.10 に dependent method types というのが入るらしいよ - scalaとか・・・


以下の例では、fメソッドは、引数のFooトレイトのResult型に応じて結果型が変わる

trait Foo{
  type Result
  def bar:Result
}

// 引数のFooトレイトのResult型に応じて結果型が変わる
def f(obj:Foo):obj.Result = obj.bar


ResultをStringで定義してるstringFooオブジェクトと、Intで定義しているintFooオブジェクトを用意し、

val stringFoo = new Foo {
  type Result = String
  def bar:Result = "foo"
}

val intFoo = new Foo {
  type Result = Int
  def bar:Result = 99
}


fメソッドにそれぞれ渡すと、引数の値に応じて結果型が変わっていることがわかる。もちろん型安全なので、全てコンパイル時に型チェックが行われる。

scala> f(stringFoo)
res0: stringFoo.Result = foo

scala> f(intFoo)
res1: intFoo.Result = 99

scala> f(stringFoo).getClass
res2: Class[_ <: stringFoo.Result] = class java.lang.String

scala> f(intFoo).getClass
res3: Class[intFoo.Result] = int

The Magnet Patternが解決する問題

引数の型に応じてメソッドの振る舞いを変えたい場合、一つの手段としてメソッドをオーバーロードする、という手がある。 しかし 元記事では、オーバーロードでは以下のような問題が発生する、と述べている。

  • type erasureにより引数の型が衝突する場合がある
  • メソッドからFunctionオブジェクトに"lift"できない(println _のような)
  • package objectでは使えない(2.9以前)
  • 似たようなコードが多発
  • デフォルト引数の利用に制限がある
  • 引数の型推論に制限がある


The Magnet Patternは、この問題のいくつかを解決し、さらなるメリットをもたらす

  • type erasureによる型の衝突は解決
  • Functionオブジェクトへのliftは、全ての結果型が同一であれば可能
  • package objectでも定義できる
  • 実装をDRYにできる
  • 危険なimplicit conversionの定義を避けることが出来る
  • 引数の型に応じて異なる結果型を返すことが可能


このパターンは、拡張性に柔軟をもたらす一方で、DrakSideもあると。

  • オーバーロードに比べて実装が細かく見通しが悪くなる
  • 名前付きパラメータは利用できない
  • by-name(名前渡し)パラメータとimplict conversionの組み合わせで重複してby-nameパラメータが評価されることがある
  • Magnet Parttenで定義するメソッドは引数宣言が必須
  • デフォルト引数は定義できない
  • 引数に対しての型推論はできない(引数に無名関数を渡す場合などで)

The Magnet Patternの例

元記事では、例としてSprayにおけるURLルーティングを定義するDSLの実装をあげている。 非常にわかりやすいのでそっちを見てもらうのがいいのだけど、それだとあんまりなので例を書いてみる。


ここでの例は、3個のIntまたはStringから日付変換を行う関数toDateを考える。

通常のオーバーロードでの実装

object M {
  def toDate(year:Int, month:Int, date:Int): java.util.Date = {
    val c = java.util.Calendar.getInstance
    c.set(year, month - 1, date, 0, 0, 0)
    c.getTime
  }

  def toDate(s:String): java.util.Date =
    (new java.text.SimpleDateFormat("yyyy/MM/dd")).parse(s)
}


まぁ見たとおりです。

scala> M.toDate(2013,1,21)
res40: java.util.Date = Mon Jan 21 00:00:00 JST 2013

scala> M.toDate("2013/01/21")
res41: java.util.Date = Mon Jan 21 00:00:00 JST 2013

The Magnet Patternで書き換えてみる

The Magnet Patternでは、オーバーロードを行う代わりに、メソッドの引数にある型(仮にmagnet型と呼ぶ)を一つだけとるようにする。 そして、そのmagnet型へのimplicit convesionを定義することで、オーバーロードと同様に引数の型に応じた振る舞いを定義できる。


さらに、The Magnet Patternでは引数の型に応じて結果型を変えることが可能である。 "magnet型"に抽象型で結果型を持たせ、Depenent method typesを利用して、引数がIntの場合はDateではなくCalndarを返すようにしている。

scala> :paste
// Entering paste mode (ctrl-D to finish)

// toDateは引数に"magnet型"を取るようにする。変換の実装はconvertメソッドに任せる
// 結果型は、"magnet型"が持つ抽象型Resultに依存させている
def toDate(magnet:DateMagnet):magnet.Result = magnet.convert

// toDate関数で使われる"magnet型"のtrait
trait DateMagnet{
  type Result           // 変換結果の結果型は抽象型で持つ
  def convert():Result  // 変換を行うメソッド
}

// "magnet型"のコンパニオンオブジェクトに、
// それぞれの引数の型に合わせた"magnet型インスタンス"を返すimplicit defを
// 定義しておく
object DateMagnet {

  // 3つのIntからDateMagnetのインスタンスへ
  implicit def fromInt(tuple:(Int, Int, Int)) = new DateMagnet {
    type Result = java.util.Calendar
    def convert():Result  = {
      val (year, month, date) = tuple
      val c = java.util.Calendar.getInstance
      c.set(year, month - 1, date, 0, 0, 0)
      c
    }
  }

  // StringからDateMagnetのインスタンスへ
  implicit def fromString(s:String) = new DateMagnet {
    type Result = java.util.Date
    def convert():Result  =
      (new java.text.SimpleDateFormat("yyyy/MM/dd")).parse(s)
  }
}

// Exiting paste mode, now interpreting.

warning: there were 2 feature warnings; re-run with -feature for details
toDate: (magnet: DateMagnet)magnet.Result
defined trait DateMagnet
defined module DateMagnet

scala> toDate(2013, 1, 21)
res0: java.util.Calendar = java.util.GregorianCalendar[time=?,areFieldsSet=false,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Asia/Tokyo",offset=32400000,dstSavings=0,useDaylight=false,transitions=10,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2013,MONTH=0,WEEK_OF_YEAR=4,WEEK_OF_MONTH=4,DAY_OF_MONTH=21,DAY_OF_YEAR=25,DAY_OF_WEEK=6,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=10,HOUR_OF_DAY=0,MINUTE=0,SECOND=0,MILLISECOND=88,ZONE_OFFSET=32400000,DST_OFFSET=0]

scala> toDate("2013/01/21")
res1: java.util.Date = Mon Jan 21 00:00:00 JST 2013


実装のポイントは、

  • 引数は"magnet型"を一つとるようにする
  • implicit conversionで引数の型に応じた"magnet型"のインスタンスへ変換する
  • 型に応じた振る舞いは、"magnet型"のメソッドに実装する
  • Dependent method typesを利用することで、"magnet型"の抽象型を差し替えて結果型を変えることができる

上記4点である。ひとことでいうと、オーバーロードで実装されている型毎の処理を型クラスに移した、といえる。


まとめ

メソッドのオーバーロードでは、Type Erasureによる制約などがあるが、 The Magnet Patternではimplicit convesionを定義することであとで対応する型を増やしたり、 結果型を変えたりと、様々な柔軟性を得ることができる。 一方で、実装が複雑になる、コードの見通しが悪くなる、などのデメリットもある。


興味深いのは、このThe Magnet Patternは型クラスやDepenent method typesを利用した、従来のOOPでは実現できない新しいプログラミングパラダイムにおけるデザインパターンであるという事実だ。
デザインパターンGoFからまだまだ進化していると言える。


これが……型の力だッッッ!!!!