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


SScalaコップ本の21章の続きをやっていきますよー、今回は暗黙のパラメーターについてやりますデス(`・ω・´)

暗黙のパラメーター

暗黙のパラメーターとは、暗黙の型変換による挿入がパラメーターリストの対して行われるものらしいですな(`・ω・´)。例えばパラメータを補完することでsomeCallをsomeCall(a)(b)にとか、new SomeClass(a)をnew SomeClass(a)(b)に置き換えたり出来るみたいデス。ただし、暗黙のパラメータの利用のためには各変換ルールのimplicit宣言の他に各パラメータ利用位置でもimplicit宣言が必要みたいです。

とりあえずサンプルを動かしてみますよー。まずはパラメータ用のクラス(型)の PreferredPromptクラスを用意します。PreferredPromptクラスはユーザが選んだシェル文字をカプセル化するクラスです(´・ω・`)

// クラス定義
class PreferredPrompt(val preference:String)

次にPreferredPromptを利用するオブジェクトGreeterを定義します。Greeter内のgreetメソッドでは第2パラメータとしてPreferredPromptをとります。今回はあとからこの部分を補完出来るようにimplicit宣言してやります。

object Greeter {
  // 第2パラメータは補完大賞なのでimplicit宣言します
  def greet(name:String)(implicit prompt:PreferredPrompt){
    // 名前(第1パラメータ)を基にした文字列を出力します
    println("Welcome, " + name + ". The system is ready.")
    // プロンプトを出力しますね
    println(prompt.preference)
  }
}

//// まずは補完なしで実際に動かしてみますよ
// PreferredPromptオブジェクトを生成します
scala> val bobsPrompt = new PreferredPrompt("relax> ")
bobsPrompt: PreferredPrompt = PreferredPrompt@a83c2a

// プロンプトを出力しますよ
scala> Greeter.greet("Bob")(bobsPrompt)
Welcome, Bob. The system is ready.
relax> 

次に暗黙のパラメーターを試してみます。まずは補完用オブジェクトを定義してやります。

object JoesPrefs{
  // 変数promptにPreferredPromptオブジェクトを適用するルールです
  implicit val prompt = new PreferredPrompt("Yes, master> ")
}

// インポートしてスコープ内に暗黙の型変換ルールを定義します
scala> import JoesPrefs._
import JoesPrefs._

// パラメータを省略して実行してみました
scala> Greeter.greet("Joe")
// おぉ!できましたよ(`・ω・´)
Welcome, Joe. The system is ready.
Yes, master>

なお、暗黙の型変換による補完では変数名ではなくて型で判定してるくさいです。暗黙の"型"変換ゆえですかね(`・ω・´)でも混乱しそうな気もするので補完対象のimplicit valは同名の方がヨサゲな気もしますな。

スコープ注意

ちなみに前回もやったとおり、インポートせず(スコープに入れず)に実行すると暗黙の型変換が適用できないので要注意です(´・ω・`)

// implicitの置き換えできねーよ(#゚Д゚)って怒られました
scala> Greeter.greet("Joe")
<console>:7: error: no implicit argument matching parameter type PreferredPrompt was found.
       Greeter.greet("Joe")
               ^
複数パラメーターを持つ暗黙のパラメータリスト

複数のパラメータをパラメータリストの形式で補完することも出来るみたいです。例えばSomeCall(a)をSomeCall(a)(b,c,d)に置き換えるみたいな使い方ですね

とりあえずサンプルやってみましょ(`・ω・´)

// パラメータとして利用する2つのオブジェクトを定義しますよ
class PreferredPrompt(val preference:String)
class PreferredDrink(val preference:String)

// 2つのオブジェクトを利用するメソッドを持つオブジェクトを定義します
object Greeter{
  def greet(name:String)(implicit prompt:PreferredPrompt, drink: PreferredDrink){
    println("Welcome, " + name + ". The system is ready.")
    print("But while you work, ")
    println("why not enjoy a cup of " + drink.preference + "?")
    println(prompt.preference) 
  }
}

// 暗黙の型変換ルールを定義しますね
object JoesPrefs{
  implicit val prompt = new PreferredPrompt("Yes, master> ")
  implicit val drink = new PreferredDrink("tea")
}

んじゃ、実行してみますかねー

// まずは同一スコープ内に定義するためにインポートします
scala> import JoesPrefs._
import JoesPrefs._

// とりあえず暗黙のパラメータを試してみます
scala> Greeter.greet("Joe")
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
Yes, master> 

// 同一スコープ内のオブジェクトなのでこんなふうにも出来るみたいです
scala> Greeter.greet("Joe")(prompt, drink)
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
Yes, master> 

// もちろん各パラメータを明示的に指定しても使えます
scala> Greeter.greet("Joe")( new PreferredPrompt("No, sir> "),           
     |                       new PreferredDrink("beer"))
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of beer?
No, sir> 

おお!できたっすな(`・ω・´)

暗黙のパラメータの使用上の注意

上記のサンプルではクラス定義の際にpreferenceフィールドを介してpromptやdrinkに与えられたのはStringであるにもかかわらず、変換時にはStringを利用していないのです、これは暗黙の型変換時に偶然の一致がおきないように通常使わない特別な型を使うように鳴っているからだそうです。今回であればPreferredPromptやPreferredDrinkがその特別な型に該当するとのことです(´・ω・`)

暗黙のパラメータによる情報の提供

暗黙のパラメータ利用のもう一つの目的は型の情報を提供することだそうです。ちなみにHaskell様の型クラスもおなじような用途で使われるみたいですね(`・ω・´)

それではサンプルをやってみましょうかねー、まずは暗黙のパラメータを使わないサンプルですでパラメータリスト中の最大値を返す関数デス(´・ω・`)

// 上限境界で型を制限しますよ
def maxListUpBound[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 = maxListUpBound(rest)
    // 再帰処理の結果大きい方を返します
    if(x > maxRest) x else maxRest
  }
}

上記の関数では上限境界をつかっているため要素型がOrderedのサブ型になっていないと使えないという弱点がありマス(´・ω・`)。たとえばIntとかは使えないので汎用性が低いわけですネ。

ここで、汎用性を持たせるためにパラメータとしてList[T]と一緒にTをOrdered[T]に変換する関数を取るように変更したサンプルを登場させマス。

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
  }
}

上記サンプルではT => Ordered[T]型のordererによってT型の(この場合はT型の順序づけの方法)を提供しているらしいデス。ここでT型はList[T]というパラメータリストに含まれているため、コンパイラーがコンパイル時にTの知識を持っていることからT => Ordered[T]型の暗黙の定義がスコープ内にあるかどうかの判定が出来るので第2パラメータのordererを暗黙のうちにわたせるとか…(´・ω・`)…うん、ちょっとおっ付いてないけども型を基準に情報をわたしてやって変換してるっぽいってのはなんとなくわかったような...

...とりあえずサンプル動かしてみますかね(´・ω・`)

// 整数リストです
scala> maxListImpParm(List(1, 3, 15, 2))           
res12: Int = 15

// 小数リストでもOKです
scala> maxListImpParm(List(1.3, 10.4, 1.9, 3.2))   
res13: Double = 10.4

// 文字列リストもなんのその
scala> maxListImpParm(List("one", "two", "three"))
res14: java.lang.String = two

// あたりまえだけど別型を混ぜるとダメみたいです(´・ω・`)
scala> maxListImpParm(List(1.3, 10.4, 1.9, 3.2, 1))
<console>:17: error: no implicit argument matching parameter type (AnyVal) => Ordered[AnyVal] was found.
       maxListImpParm(List(1.3, 10.4, 1.9, 3.2, 1))
       ^

上記の3つの成功パターンではコンパイラがInt、Double, Stringソレゾレのordered関数を挿入しているみたいです。なお、Scala標準ライブラリーではいろんな共通型向けのordererメソッドを提供しているみたいです。

そして驚愕の真実

ここまで作成したmaxListImpParmは実は暗黙のパラメータを使っていないらしいです。実際に暗黙のパラメータを使ったパターンは次の節でやっていきマス

暗黙のパラメータのためのスタイルの原則

先ほどもさらっと触れたのですが、暗黙のパラメータの型の名前は原則的に独自の名前を使うのがいいみたいです。例えばさっきのサンプルでやったpromptに対するPreferredPromptみたいな感じですね。

ただし、あくまで原則なのでためしに反例サンプルを上げてみます

// 特殊な名前の型を使わないで書いてみます
def maxListPoorStyle[T](elements:List[T])
      (implicit orderer:(T, T) => Boolean): T

上記のような書き方の関数を使う場合は呼び出し元で(T, T) => Boolean型というかなり汎用性の高い型のordererパラメーターを供給する必要があるのですが、この方は汎用性が高すぎて何を目的としているのかがさっぱりわからないのです(´・ω・`)なので、きっちりと目的なんかがわかりやすいimplicit orderer:T => Ordered[T]的な書き方のほうが推奨されるとのことです。

またimplicit orderer:T => Ordered[T]的な書き方は型が明確なので標準ライブラリーに組み込んでも問題はおきないのですが、(T, T) => Boolean的な汎用性の高過ぎる型は様々なところで多大な影響を及ぼしてしまうので危険デス、とのこと(´・ω・`)まあ、そうだろうな。

なので、暗黙のパラメータの型名は独自のものを使うのが言語設計者のオススメよ!とのことです。

いじょー

とりあえず時間切れでここまでデス、次回は可視境界からやっていきますよー。次回で21章終われるかしら?終われたらいいなぁ…頑張ります(´・ω・`)