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


Scalaコップ本の21章に入っていきますよー、21章は暗黙の型変換と暗黙のパラメータについてやりますね(`・ω・´)

暗黙の型変換

これまでScalaの特徴としてさんざん挙げられてきた暗黙の型変換を見ていきますかねー

まずはサンプルをやってみますよー、今回はScalaのコレクショントレイトの一つであるRandomAccessSeq [T]を暗黙の型変換を利用してString型で使ってみますよ(´・ω・`)ちなみにRandomAccessSeqを拡張するとtake, drop, map, filter, exists, mkString等のメソッドを持っているランダムアクセスシーケンスを作ることが出来るみたいです。

とりあえずこんな感じでStringをRandomAccessSeq[Char]のサブ型のように扱ってやることができるみたいです

// StringをRandomAccessSeq[Char]として扱って
// 文字列内に数値が存在するかを判定しますよ
scala> "abc123" exists (_.isDigit)                
res5: Boolean = true

これはコンパイラー様が水面下で次のようなコードを追加しているために実行出来ているみたいです(`・ω・´)

// implicit修飾子による暗黙の型変換を行なうWrapperを定義します
implicit def stringWrapper(s:String) = {
  // StringにもかかわらずRandomAccessSeq[Char]に変換します
  new RandomAccessSeq[Char] {
    // 足りないメソッドを追加してやります
    def length = s.length
    def apply(i:Int) = s.charAt(i)
  }
}

// 実際に実行されるコードも次のように解釈されます
scala> stringWrapper("abc123") exists (_.isDigit) 
res7: Boolean = true

implicitによるメソッド追加de暗黙の型変換、Σ( ゚Д゚) スッ、スゲー!!って思うっす(`・ω・´)ちなみにscalaでは上記stringWrapperがpredefで定義されているそうデス

C#との比較

暗黙の型変換(implicitでの処理)は既存クラスに新しいメソッドを追加できるC#の拡張メソッドと似ているらしいですが、C#の方は追加するメソッド以外の全てのメソッドを再定義する必要があるため生産性と保守性が著しく低下するとのことです。それに引き換えScalaは...(以下略)...でも、確かに外部のコードの再利用性を考えたらこのほうが便利だなぁ、と思うのです(`・ω・´)

もういっちょ型変換

また、Scalaのimplicitの利点としてはターゲット型(コードのどこかで必要となる型)への変換もサポートされるという利点もあるそうです。例えば次のようなランダムアクセスシーケンスの各要素をスペース区切りで出力するメソッドを考えると、Stringで利用出来るようになりますね

def printWithSpaces(seq: RandomAccessSeq[Char]) = seq mkString " "

// 実際にStringで使ってみますよ
scala> printWithSpaces("abcde")
res8: String = a b c d e

// 内部的にはこう解釈されるみたいです
scala> printWithSpaces(stringWrapper("abcde"))
res9: String = a b c d e

うん、ほんと便利ですな(`・ω・´)

implicitの規則

implicit定義は型エラーが発生した場合にコンパイラーがプログラムに追加するメソッドのことらしいデス。例えばx + yという処理で型エラーが発生した場合に、コンパイラーが適切なimplicit def convert(x:T) = ...を探してconvert(x) + yとして処理するとのことです。

このように行われる暗黙の型変換は以下の5つの原則によって管理されているみたいデス。

  • マーキングルール
    • implicitによって修飾された定義だけが暗黙の型変換に使われる
  • スコープルール
    • 挿入される暗黙の型変換は、単一の識別子としてスコープ内にあるか、変換のソース型やターゲット型として対応付けられていなければならない
  • あいまい回避ルール
    • 暗黙の型変換は、ほかに挿入すべき変換がないときに限って挿入される
  • 1度に1回ルール
    • 暗黙の型変換は1度しかじっこうされない
  • 明示的変換優先ルール
    • 書かれたままの状態でコードが型チェックをパスするときには、暗黙の型変換は行われない

ソレゾレを詳しく見ていきますかねー

マーキングルール

コンパイラが暗黙の変換として使用するのはimplicit修飾されたものだけなので、そこは明示的に修飾しましょうZE(`・ω・´)とのことです。

スコープルール

コンパイラが暗黙の型変換として利用するのはスコープ内のものだけだそうです。このようにスコープで制限することで、予想外の箇所から暗黙の型変換が呼び出されることを防止できるデスね。

また暗黙の型変換は単一の識別子としてスコープ内に入っている必要があるので、SomeVariable.convert(x)みたいな変換はダメだそうです(´・ω・`)ちなみにこのような形式の変換を使う場合にはimport SomeVariable.convert等でインポートしてconvert(x)単体にしてやればOKとのことです(´・ω・`)実際のところ多くのライブラリーはコレを応用してimport Preamble._をすれば必要な型変換が行われるようにPreambleオブジェクトを用意してるそうです。ナルホドc⌒っ゚д゚)っφ メモメモ...

ただし、ソース型(変換前の型)やターゲット型(要求された変換後の型)のコンパニオンオブジェクト内のimplicit定義は例外的にコンパイラの探索範囲に入るらしいので、関連のある型を定義する場合はこちらでやっちゃうのがよさそうデス。

コンパニオンオブジェクトにimplicitを組み込んだ例は次のようになりますね。下の例ではDollarからEuroへの変換時にdollarToEuroが呼ばれるようになります(`・ω・´)

// Dollarクラスのコンパニオンオブジェクトにimplicitを組み込みますよ
object Doller {
  implicit def dollarToEuro(x:Dollar):Euro = ...
}
class Dollar { ... }

ちなみにこのような形式の場合dollarToEuro変換はDollar型に関連づいていると言えるそうです。

あいまい回避ルール

x + yという処理にconvert1(x) + yとconvert2(x) + yという二つの変換候補が存在する場合、コンパイラはエラーを出力するとのことです。なので変換候補が2つ以上出てくるようなケースであれば、import時に制御するなどで明示的に候補を1つに絞る必要があるそうデス。もしくはconvert2(x) + yのように明示的に書いちゃえばOKとの説もありマス。

…まあ、あいまいにならないようにきちんと設計するのが大事な気もしますな(´・ω・`)

1度に1回ルール

暗黙の型変化はconvert1(convert2(x)) + yのような重ね掛けで実行されることはないそうです。確かに複雑になるだけなので防止するのが良い気がしますな…ただし暗黙の型変換に暗黙のパラメーターを取らせることで似た様な事ができるとのこと(;゚Д゚)(゚Д゚;(゚Д゚;)ナ、ナンダッテー!!...詳しくはあとでやりマス

明示的変換優先ルール

暗黙の型変換なしで動くコードに付いては、コンパイラが暗黙の型変換を利用することがないそうです。なので、明示的な変換コードを追加することで暗黙の型変換を回避することも可能だったりするとのことです。でもすべてを明示敵に書いたりすると冗長になりがちなのでケースバイケースですね(´・ω・`)

暗黙の型変換の名前ルール

暗黙の型変換には好きな名前をつけられマス。また命名することでimport時の(使う、使わない)制御が行ったりするので解りやすい名前をつけるのがいいですね(´・ω・`)

暗黙の型変換が使われる場所

暗黙の型変換は次のような3つの箇所で利用されます

  • 要求された型への暗黙の型変換
    • 異なる方が必要とされる文脈で使いたい型を使えるようにする変換
  • レシーバの変換
    • 手持ちの方には実行したいメソッドが定義されていない場合等に利用する変換
  • 暗黙のパラメータ
    • 呼び出された関数により多くの情報を与えるために使われる変換

次節以降でソレゾレの詳細を見ていきますかねー

要求された型への暗黙の型変換

コンパイラから見える型と要求される型が異なる場合には、コンパイラがこの暗黙の型変換を試すみたいです。例えば次のように倍精度浮動小数を整数に変換するのは精度が下がってしまうのでエラーになってしまいマス(´・ω・`)

// 整数変数に小数を突っ込みますデス
scala> val i:Int = 3.5
// 型違うYO!って怒られました
<console>:4: error: type mismatch;
 found   : Double(3.5)
 required: Int
       val i:Int = 3.5
                   ^

上記のような状況は暗黙の型変換を定義することで回避できますな

// 暗黙の型変換用メソッドを定義しました
scala> implicit def doubleToInt(x:Double) = x.toInt
doubleToInt: (Double)Int

// 変換されましたよ(`・ω・´)
scala> val i:Int = 3.5                             
i: Int = 3

ちなみに上記変換はコンパイラによって次のようなコードに置き換えられているわけです(´・ω・`)

scala> val i:Int = doubleToInt(3.5)
i: Int = 3

まあ、通常はDoubleからIntへの変換なんていう情報が劣化する変換はめったに使わないわけですが、逆のIntからDoubleへの変換なんかはかなり使いますな(´・ω・`)実際Scalascala.Predefオブジェクトには次のような型変換コードが定義されております

// 整数を倍精度浮動小数に変換します
implicit def int2double(x:Int):Double = x.toDouble

ここらへんの滑らかな変換はScalaの型システムに特殊ルールが定義してあるわけではなく、暗黙の型変換が適用されているだけなんですな(´・ω・`)ふむ

レシーバーの変換

暗黙の型変換はメソッド呼び出しのレシーバーにも適用されるですね。この機能は既存のクラスに新しいクラスを円滑に組み込むために、またScala言語内でDSLを定義するのに利用されるみたいです。

ちなみにレシーバーの変換はobjオブジェクトがとあるメソッドdoItを持っていない場合に、暗黙の型変換を駆使して異なる型のobjからdoItメソッドを呼び出して、あたかも元々のobjがdoItメソッドを持っているかのように振舞うものみたいです(´・ω・`)

新しい型の同時利用

まずは既存の型に新しい型を統合するサンプルを見てみます。サンプルとしては6章で扱ったRational(省略版)クラスを使ってみますよ

class Rational(n:Int, d:Int) {
    require(d != 0)
    private val g = gcd(n.abs, d.abs)
    val numer = n / g
    val denom = d / g
    def this(n:Int) = this(n, 1)
    override def toString = numer + "/" + denom
   
    // + メソッドを多重定義して
    // Rationalを引数にする処理とIntを引数にする処理を
    // 両方出来るようにしています
    //// 2つのRationalオブジェクトを加算するメソッド
    def + (other:Rational):Rational = {
        new Rational(
            this.numer * other.denom + other.numer * this.denom,
            this.denom * other.denom
        )
    }
     // 整数用を多重定義
    def + (i:Int):Rational = {
        new Rational(this.numer + i * this.denom, this.denom)
    }
    // 最大公約数を計算するメソッド
    private def gcd(a:Int, b:Int):Int = {
        if(b == 0) a else gcd(b, a % b)
    }
}

//// ちょっくら実行してみますよ
// Rationalオブジェクトを追加しますよ
scala> val oneHalf = new Rational(1,2)
oneHalf: Rational = 1/2

// Rational同士の加算です
scala> oneHalf + oneHalf 
res12: Rational = 1/1

// RationalへのIntの加算です
scala> oneHalf + 1
res13: Rational = 3/2

しかしながら上記クラスはレシーバーがIntの次のような式は定義されていないのでエラーになります(´・ω・`)

scala> 1 + oneHalf
// 色々さがしたけど適用できないっすよ!っというエラーですね
<console>:9: error: overloaded method value + with alternatives (Double)Double <and> (Float)Float <and> (Long)Long <and> (Int)Int <and> (Char)Int <and> (Short)Int <and> (Byte)Int <and> (java.lang.String)java.lang.String cannot be applied to (Rational)
       1 + oneHalf
         ^

このようなエラーを回避するためにIntからRationalへの暗黙の型変換を定義してやるですね

// 整数がきたらx / 1というRational(有理数)オブジェクトに変換します 
scala> implicit def intToRational(x:Int) = new Rational(x, 1)
intToRational: (Int)Rational

// 無事変換できましたな
scala> 1 + oneHalf
res15: Rational = 3/2

// 実際は下記のような処理になっておりマス
scala> intToRational(1) + oneHalf
res16: Rational = 3/2

暗黙の型変換を使って存在しないメソッドを呼び出してやりましたYO(`・ω・´)

新しい構文のシミュレーション

暗黙の型変換のもう1つの主な用途は新しい構文の追加のシミュレートだそうです。例えばMap生成時構文の"->"に対するサポートを考えてみます。

// Mapの生成構文です
Map(1 -> "one", 2 -> "two", 3 ->"three")

この->はScala.Predef内で定義されるArrowAssocクラスのメソッドなのですが、Scala.Predefでは同時にAnyからArrowAssocへの暗黙の型変換も定義されているので、コンパイラが->というメソッドを見つけるとレシーバ(この場合Int)からArrowAssocへの変換を行って実行するみたいですね(`・ω・´)

ちなみに上記動作の定義は次のような感じみたいです

package scala
object Predef {
  class ArrowAssoc[A](x:A) {
    // 実際の->の定義デス(`・ω・´) 
    def -> [B](y:B):Touple2[A,B] = Tuple2(x,y)
  }
  // 暗黙の型変換定義です
  implicit def any2ArrowAssoc[A](x:A):ArrowAssoc[A] = new ArrowAssoc(x)
  ...
}

IntとかStringを使ってるつもりでも、実際に->を使うときはArrowAssocに変換されたものを利用していたのですね(´・ω・`)なるほど。ちなみにこのようなパターンをリッチラッパーパターンと呼ぶらしく、応用範囲が広いため言語内DSLを作る場合にもかなり有用だとのことです。

いじょー

とりあえずキリがいいのでこのへんでー、次回は暗黙のパラメータをやりたいと思います