Locked History Actions

scala/this.type

this.type

チェインメソッドコールのためのthis.type

http://scalada.blogspot.com/2008/02/thistype-for-chaining-method-calls.htmlの翻訳

Scalaは他言語から借りてきたパワフルなメカニズムを持つが、しかし小さいユニークな機能はみすごされているようだ。 this.type。この記事では、簡単な説明とデモを見せる。

this.typeはシングルトンタイプであり、これは特定のオブジェクトを表現するタイプである。オブジェクトのクラスではない。 これはクラス階層におけるチェインメソッドコールを行う場合の問題を解決してくれる。 例えば、method1がスーパークラスAで定義されており、method2が派生クラスBで定義されているときの "value.method1.method2"といった呼び出しだ。

  class A {def method1: A = this }
  class B extends A {def method2: B = this}
  val b = new B

クラス階層を定義してBのインスタンスとしてbを作成した。 メソッドコールのチェインを見てみよう。 以下はうまくいく。method2はBを返し、それはmethod1を持つからだ。

  b.method2.method1

しかし以下はだめ。

  b.method1.method2

method1の返値はAだから、コンパイラはオブジェクトがmethod2を持たないと考える。

Bにおいてmethod1をオーバライドすればこの問題を解決することはできる。 そこでは、スーパークラスのmethod1を呼び、返値をBにすればいい。

  class B extends A{
    override def method1: B = {super.method1; this };
    ...
  }

しかし、これではメソッドチェインの必要なすべてのAのサブクラスで必要になってしまう!

※訳注はじめ:以下のような方法もある。めんどくさい上に複雑

class A[T<:A[T]] { def method1: T = this.asInstanceOf[T] }
class B extends A[B] { def method2: B = this }

※訳注おわり

Scalaなら別の方法がある。this.typeを使って、method1から返されるオブジェクトがbであることをコンパイラに教えるのだ。 そうして、コンパイラはmethod1から返されるオブジェクトがmethod2を持つことを知ることができる。

  class A { def method1: this.type = this }
  class B extends A { def method2: this.type = this }
  // in method2 this.type is not a necessity
  // unless there's subclass of B.

としてやると、

  val b = new B
  b.method1.method2

が動くようになる。

もっと詳しい情報はScalable Component Abstractionsにある。

同じリンクの中に説明があるが、Scalaには、linear mixin compositionというものもある。 詳細には立ち入らないが、しかしアイデアとしてはこうだ。ミックスインするトレイトに一部実装をする(?)というものだ。 この記事では、我々はライブラリを作成するのだが、それが後でどのように拡張されるのかわからないし、欲しい部分だけを取得したいと考えるからだ(?)。 さらに、引き続き作成されるトレイトについて統一的に扱いたい、スーパークラスのメソッドだけではなく。 これは、トレイトとthis.typeによって実現される。

※訳注:話が抽象的でよくわからない。

中心となるのはCommandCenterである。これは、示された計算をキューに保持するのだが、それらは単なるUnit=>Anyという関数だ。

  trait CommandCenter{
    protected var queue = List[Unit => Any]()
    def <+(computation: => Any): this.type = {
      queue :::= List[Unit=>Any]({x => computation})
      this
    }
  }

CommandCenterを作成して、計算をチェインしてみよう。つまり、

  val cmd = new CommandCenter{}
  cmd <+ Console.println("hi") <+ {network.send("bye")}

これらの計算を後で実行したいのは明らかだろうが、このトレイトを汚染したくはないのだ(トレイトの中に実行メソッドを入れたくない)。

Certainly we'd like to execute those computations later, but we don't want to pollute the same trait, because it's different responsibility. Here's first a helper class, which just forces the evalution of computations that are in the list.

  def executeAll(list: List[Unit => Any]) 
  = list foreach {_()}

Here's the trait for actual execution:

  trait ExecuteCommands extends CommandCenter{
    def execute: Unit = executeAll(queue)
  }

Now we'd like to extend this thing so that we can make a group of computations and give them names by which we can later ask them to be evaluated.

  trait GroupedCommands extends CommandCenter {
    import scala.collection.mutable.HashMap
    protected val groups = HashMap[String, List[Unit=>Any]]()
    def >>(name: String) = {
      groups += ((name, queue))
     queue = List()
    }
  }

'>>' method ends the construction of computations; it takes all the computations in the queue and names it as a group, and clears the original queue so that new computations can be binded together.

We have also different trait for execution group computations, but we skip that (you can see it in the complete source code).

Now we can create a test case. First we have a construct method that only creates the computations. We restrict ourself from knowing more than grouping build operations i.e. GroupedCommands trait.

  def construct(command: GroupedCommands) = {
    var (x, y) = (1.0, 1.0)
    def printx = Console.println("x: " + x)
    command <+ printx <+ { x *= 2 } <+ printx >> "doubleX"
    command <+ printx <+ { x /= 2 } <+ printx >> "halfX"
  } 

Of course <+ is kinda useless, we could just make a one big computation with {comp1; comp2; ... compN}, but this is only an example, though by partioning them to be differentiable, one could use for example time delay between evaluations. Here's the execute part:

  def execute(command: GroupExecution) = {
    command execute("doubleX")
    command execute("doubleX")
    command execute("halfX")
  }

And finally the test that gives observable results:

  val command = new CommandCenter 
                with GroupedCommands with GroupExecution
  construct(command)
  execute(command)

It prints:

  x: 1.0, x: 2.0, x: 2.0, 4.0, x: 4.0, x: 2.0

Here's the complete source code.

So now you know of this.type. Spread the word, for rarely anyone mentions it.

this.typeの問題点

this.typeはクラスではないためか、以下はエラーになってしまう。

class Sample {
  def sample: this.type = wrapper{
    this
  }
  def wrapper[T](fn: => T): T = fn
}