LL脳がscalaの勉強を始めたよ その80


Scalaコップ本の21章の残りをやっていきますかねー、今回は可視境界と暗黙の型変換のデバッグについてやっていきますデス

可視境界

まずは前回作成した変換用関数をパラメータとして取ることで汎用化したリスト中の最大値を取る関数について、暗黙のパラメータを使うように修正してやりますよ。

ちなみに前回書いたコードはこんな感じで、せっかく暗黙のパラメータを使うチャンスはあるのに使っていないというモノです。

def maxListImpParm[T](elements:List[T])
  // T型の順序関係を明らかにするパラメータを追加します
  (implicit orderer:T => Ordered[T]):T  = elements match {
  case List() => throw new IllegalArgumentException("empty list!")
  case List(x) => x
  case x :: rest => {
    // 再帰的に処理デス
    val maxRest = maxListImpParm(rest)(orderer)
    // 単体要素もOrdered[T]型に変換します
    if(orderer(x) > maxRest) x else maxRest
  }
}

それでは暗黙のパラメータを適用してみますが、具体的にはordererの呼び出しをばっさり削除するだけです。実際に削除したコードは下記のようになります(´・ω・`)

def maxList[T](elements:List[T])
  (implicit orderer:T => Ordered[T]):T= elements match {
  case List() => throw new IllegalArgumentException("empty list !")
  case List(x) => x
  case x :: rest => {
    // カリー化された第2パラメータに暗黙のうちにordererが渡される
    val maxRest = maxList(rest)
    // 暗黙のうちにxはorderer(x)に変換されるデス
    if (x > maxRest) x else maxRest
  }
}

  
//// 実行してみますよ
// 整数リストで最大値
scala> maxList(List(1,4,2,3))
res0: Int = 4
// 小数リストで最大値
scala> maxList(List(1.3, 4.3, 4.9, 21.2 , 30.0))
res1: Double = 30.0
// 文字列リストで最大値
scala> maxList(List("apple", "banana", "orange"))
res2: java.lang.String = orange

上記コードを読み込んだコンパイラはx > maxRestの箇所で型がマッチしないことに気づいて暗黙の型変換としてのimplicit orderer:T => Ordered[T]を適用することになるみたいです。同様に暗黙の型変化的パラメータ補完を実行してmaxList(rest)をmaxList(rest)(orderer)と変換するとのことデス。

ちなみにordererはパラメータ以外の実行コード中からなくなってしまうので、上記コードは下記のようにordererの名前を好きに書き換えても問題ないみたいですね(´・ω・`)

def maxList[T](elements:List[T])
  // 暗黙の型変換定義でしか使わないのでお好きな分かりやすい名前を適用できマス
  (implicit converter:T => Ordered[T]):T= elements match {
  case List() => throw new IllegalArgumentException("empty list !")
  case List(x) => x
  case x :: rest => {
    val maxRest = maxList(rest)
    if (x > maxRest) x else maxRest
  }
}
可視境界の適用

Scalaでは上のような暗黙のパラメータ適用パターンがとても多いそうなので、記述の簡略化のために可視境界を適用したパラメータ部の省略を許可しているみたいです。とりあえず可視境界を使ったサンプルを書いてみますねー

// 可視境界を適用してメソッドヘッダーを短くしました(`・ω・´)
// 暗黙の型変換ルール用パラメータ部を省略しております
def maxList[T <% Ordered[T]](elements:List[T]):T = elements match {
  case List() => throw new IllegalArgumentException("empty list!")
  case list(x) => x
  case x :: rest => {
    val maxRest = maxList(rest)
    if(x > maxRest) x else maxRest
  }
}

ここでT <% Ordered[T]は「Ordered[T]として扱える任意のTを使える」と読むみたいで、渡された型TがOrdered[T]のサブ型でなくてもTからOrdered[T]に暗黙の型変換ができるのであれば渡してもOK!的な意味合いらしいです(´・ω・`)なお、可視境界適用パターンでの暗黙の型変換はPredefで宣言されている次の暗黙の恒等関数を利用するみたいです。

// 単純に渡されたオブジェクトをそのまま返しますデス(´・ω・`)
implicit def identity[A](x:A): A = x

ちなみに上限境界と可視境界はそれぞれの記号が

  • 上限境界
    • 記法: T
    • TがOrdered[T]であることを要求
  • 可視境界
    • 記法: T <% Ordered[T]
    • TがOrdered[T]として(暗黙の型変換トカ使っていいから)扱えることを要求

この適用範囲の違いは覚えておくことにしようc⌒っ゚д゚)っφ メモメモ...

暗黙の型変換のデバッグ

暗黙の型変換は強力なんだけども、デバッグしづらい機能でもあるらしいのでそのヒントを挙げていきますよー

暗黙の型変換がなぜか適用されない場合

コンパイラーが暗黙の型変換を見つけられないときは明示的に書いてみるのがいいみたいです。その結果エラーが表示されるようならその対処をしましょうよ…と(´・ω・`)まあそうだよね。

とりあえずコップ本で挙げられている次のような例を見てみますデス

  • ケース
    • StringからListへの変換が上手くいかない(´;ω;`)
  • 原因
    • 変換で使っているstringWrapperは実際にはStringからRandomAccessSeqへの変換を行うのに、間違って使っている

まずは変換が適用されなくてアレ?(´・ω・`)ってなっているコードです

scala> val chars:List[Char] = "xyz"              
// stringWrapperが適用されて暗黙の型変換が行われる(間違い)はずなのに
// エラーが出るんだけども原因がわからないっす(´;ω;`)
<console>:4: error: type mismatch;
 found   : java.lang.String("xyz")
 required: List[Char]
       val chars:List[Char] = "xyz"
                              ^

とりあえずデバッグ的にstringWrapperを明示的に書いてみますよ

scala> val chars:List[Char] = stringWrapper("xyz")
<console>:4: error: type mismatch;
// あれ?RandomAccessSeqじゃなくRichStringってでた
// とりあえずList要求に対してRichStringって解釈されていることはわかったデス
 found   : scala.runtime.RichString
 required: List[Char]
       val chars:List[Char] = stringWrapper("xyz")
                              ^

...まあ、コップ本と結果は異なったものの、エラーから程度の探索はできそうです。また仮にエラーが出ずにスルッと処理が行われてしまった場合は暗黙の型変換のスコープなんかを歌が合うのがヨサゲですね。

適用された暗黙の型変換を調査する

コンパイラーによって挿入された暗黙の型変換の内容を調べるためには、コンパイラーに対して-Xprint:typerオプションを指定するとよさそうです。んじゃ、実際に試してみますかねー

とりあえずコンパイラに処理させるコードは次のような感じです

object Mocha extends Application {
  class PreferredDrink(val preference:String)
  implicit val pref = new PreferredDrink("mocha")
  def enjoy(name:String)(implicit drink:PreferredDrink){
    println("Hi " + name + ". enjoy a " + drink.preference + "!")
  }
  // 実際はここで暗黙の型変換によるパラメータ補完が行われます
  enjoy("reader")
}

それでは-Xprint:typeオプション付きでコンパイルした結果です。おまけが大量についているので読みづらいですが暗黙の型変換が挿入された形跡が発見できますね(´・ω・`)

[[syntax trees at end of typer]]// Scala source: cmpile.scala
package <empty> {
  final object Mocha extends java.lang.Object with Application with ScalaObject {
    def this(): object Mocha = {
      Mocha.super.this();
      ()
    };
    class PreferredDrink extends java.lang.Object with ScalaObject {
      <paramaccessor> private[this] val preference: String = _;
      <stable> <accessor> <paramaccessor> def preference: String = PreferredDrink.this.preference;
      def this(preference: String): Mocha.PreferredDrink = {
        PreferredDrink.super.this();
        ()
      }
    };
    private[this] val pref: Mocha.PreferredDrink = new Mocha.this.PreferredDrink("mocha");
    implicit <stable> <accessor> def pref: Mocha.PreferredDrink = Mocha.this.pref;
    def enjoy(name: String)(implicit drink: Mocha.PreferredDrink): Unit = scala.this.Predef.println("Hi ".+(name).+(". enjoy a ").+(drink.preference).+("!"));
    // (Mocha.this.pref)が保管されております
    Mocha.this.enjoy("reader")(Mocha.this.pref)
  }
}

こんな感じでデバッグを行うのがヨサゲですね(`・ω・´)

いじょー

今回で暗黙の型変換的21章はおしまいです。次回は22章リストにまつわるエトセトラをやります(`・ω・´)