2014年6月16日月曜日

Programming in Scala, First Edition: Chapter 21

21. Implicit Conversions and Parameters
  既存classにどうやって機能を追加するか?
    これは難しい問題。
    各言語で色々な方法が試されている。
    rubyではmodule, smalltalkではpackage
      良く分からない他人のclassに手を入れるのは危険
    C#3.0ではstatic extension method
      影響は限定的だが、制約が多くて不便
  scalaではimplicit conversionとparameter
    the interesting, non-trivial partsにfocus出来る。

21.1 Implicit conversions
  typeがmismatchした時にcompilerが変換関数を探して自動的に適用する機能。
  分数を扱う簡易Rational classを定義する。
    class Rational(val n: Int, val d: Int) {
      def this(n: Int) =  this(n, 1)
      def + (that: Rational) = new Rational(n * that.d + d * that.n , d * that.d)
      override def toString = this.n + "/" + this.d
    }
  1 + 1/2 を実施。
    scala> (new Rational(1)) + (new Rational(1, 2))
    res0: Rational = 3/2
  receiverがIntだとtype errorになって使えない。
  Intの + methodのparameterにRationalが定義されていないから。
    scala> 1 + (new Rational(1, 2))
    <console>:9: error: overloaded method value + with alternatives:
      (x: Double)Double <and>
      (x: Float)Float <and>
      (x: Long)Long <and>
      (x: Int)Int <and>
      (x: Char)Int <and>
      (x: Short)Int <and>
      (x: Byte)Int <and>
      (x: String)String
     cannot be applied to (Rational)
                  1 + (new Rational(1, 2))
                    ^
  RationalにIntを足してもtype errorになって使えない。
  Rationalの + methodのparameterにIntが定義されていないから。
    scala> (new Rational(1, 2)) + 1
    <console>:9: error: type mismatch;
     found   : Int(1)
     required: Rational
                  (new Rational(1, 2)) + 1
                                         ^
  implicit conversionを使うときは下記をimportする。
  importしないとwarningが出る。implicitの使い過ぎの抑制のため。
    scala> import scala.language.implicitConversions
  Int=>Rational型変換関数をimplicitで宣言する。
    scala> implicit def int2rational(x: Int) = new Rational(x)
  receiver Intに対応出来るmethodがないと、compilerが変換関数を検索。
  int2rational(1)に置き換えて実行される。
    scala> 1 + (new Rational(1, 2))
    res3: Rational = 3/2
  receiver Rationalに対応出来るparameterがないと、compilerが変換関数を検索。
  int2rational(1)に置き換えて実行される。
    scala> (new Rational(1, 2)) + 1
    res4: Rational = 3/2
  parameterの型変換 > receiverの型変換の優先順位で実施される。

21.2 Rules for implicits
  Marking Rule
    implicitで定義されている場合のみ。
    variable, function, object何でもimplicitに出来る。
  Scope Rule
    single identifierとしてscope内にある場合のみ。
    変換関数をimportでscope内に入れる必要がある。
    更にsingle identifierとなるように入れる必要がある。
      path.convertはNG。convertになるようにimportする。
    companion objectは例外としてsingle identifierではなくても検索される。
      source, target type両方のcompanion objectがimport不要。
    importしていない所にあるimplicitまでは把握しきれない。
  Non-Ambiguity Rule
    変換関数が一意に決まる場合のみ。
    二つ以上の候補がある場合は適用されない。
    ?? より内側のscopeにあるなど、優先度がある??
  One-at-a-time Rule
    一回につき一段の変換しない。
    convert1(convert2(x))のような多段の変換はしない。
    compile時間が長くなるし、プログラマが把握しきれなくなるから。
    implicit parameterを持つimplicitは例外。(後述)
  Explicits-First Rule
    無変換でtype checkが通ったら変換しない。
    型変換関数をexplicitに書いてもOK。
    冗長さと明確さのトレードオフ。ケースバイケースで判断する。
 implicit conversion(変換関数)にも任意の名前を付けられる。
    implicitに適用されているときはtypeしか見ないので名前な何でも良い。
    explicitに適用したいときに使う。
      自分の考えている変換関数がそこで使えるかの確認にも使える。
    特定の変換関数のみをimportする時にも使える。

21.3 Implicit conversion to an expected type
  type違いの時にtype変換関数があると適用される。
  3つのimplicitの中で一番優先的に適用される。
  通常は制約の多い(低機能)typeからより適用範囲(高機能)の広いtypeに変換する。
    Int -> doubleへの変換はOK。逆はnot recommended。
    ** より機能追加されたsub typeを定義して、それに変換する。

21.4 Converting the receiver
  receiverにmemberがなかったらmemberがあるtypeへの変換関数が適用される。
  既存class階層に新しいclassを追加しやすくなる。
  DSLをサポートしやすくなる。
  Interoperating with new types
    21.1のRationalの例を参照。
  Simulating new syntax
    文法を拡張した様に見せかける場合に有用。
    Mapの`->`は組み込むの文法ではなくて単なるmethod!!
      Map(1 -> "one", 2 -> "two", 3 -> "three")
      Predefのclass ArrowAssocで->とimplicit any2ArrowAssocが定義されている。
      1 -> "one"は
      any2ArrowAssoc(1).->("one")と変換されて
      (1, "one")になる。
    "rich wrappers"はライブラリが文法を拡張した様に見せかける時によく使う。
    receiverに存在しないmethodを適用しているときはimplicitの可能性が高い。
    Richxxxといclassがあったら、xxxにimplicitで機能追加している可能性が高い。
    これはDSLを定義する際に有用
      他の言語だとexternal DSLが必要な場合でもscalaならinternalに定義出来る。

21.5 Implicit parameters
  curry化した最後のパラメータリストそのもが省略されているときに、パラメータのtypeのobjectがあったら適用される。
    適用したくない時はplace holder _を付けて省略ではない事を伝える必要あり。
  implicit parameterの型は特殊であるものにすべき。
    補完対象を型だけで決めるので重複や想定外の適用を避けるため。
    型定義からimplicit parameterの役割が分かるような型を付けるべき。

21.6 View bounds
  implicit parameterで渡される関数でimplicit conversionが出来る。
    この場合はanonymousで良い。
    [T <% Ordered[T]]
      T => Ordered[T] がanonymousの関数値として適用されて、使用される。
  implicit parameterは補完する/される両方で使われる。
    省略した時に他のimplicit宣言されている値で補完される。
    implicit parameterのscope内でimplicitが必要であれば適用される。
      外側のscopeに同じsignatureのimplicitが定義されていてもhideされる。
  implicitは主な使用目的はtypeのconversion
    implicit parameterは値の補完にも使えるが、通常は型変換関数値の補完。
  View bounds and upper bounds
    <%は<:の適用範囲を更に広げたものと考える事が出来る。
    sub typeの場合はそのまま使用される。
    sub typeでない場合は変換関数が検索、適用されて使用される。
    sub typeであるか変換関数があるかどちらかの条件を満たしていれば適用できる。

21.7 Debugging implicits
  implicitは強力だが正しく使うのもdebugするのも難しい場合がある。
  implicitが適用されない理由が分からない場合
    implicitを使わずにexplicitで書いてみる。
      エラーになればimplicitの問題ではない。
      エラーにならなかったらimplicitの適用ルールを確認する。
  どのimplicitが使われているかはcompile option -Xprint:typerで出力出来る。
    scalac -Xprint:typerだとcomiple時に適用される。
    scala -Xprint:typerでREPLでの適用結果も出力できる。
      沢山出力されるので注意が必要。
  ?? どういうimplicitが宣言済か把握するには?
  ?? implicitを抑制する方法は??

21.8 Conclusion
  implicitは実行時ではなくてcompile時に変換、チェック出来る。
  implicitは強力だが乱用は避けるべき。
  implicitを使う前にinheritance, mixin, overloading等で代替出来ないかを考える。
  全部がダメで、更にコードが無駄冗長な場合は、implicitの使い所になるかも。

0 件のコメント:

コメントを投稿