implicitパラメータとは何ぞや

はじめに

ぜんぜんわからない 俺たちは雰囲気でScalaを使っている どうも弊社です。

PlayでコントローラからビューからFuture[T].mapからお前らimplicitパラメータ好きすぎだろってくらい多用されています。これはゆるふわっと使っていると後でヤバい目に遭うなと思ったので軽く調べてみました。

ゆるふわ愛されimplicit

言語仕様のimplicitsセクションを見ながら確認します。困ったときは公式ドキュメントを読む。古事記にもそう書いてあります。

とりあえず以下が一番シンプルな形です。

object Hello {
  def main(args: Array[String]): Unit = {
    implicit val hoge = "jyuch"

    greet("Hello")
  }

  def greet(message: String)(implicit who: String): Unit = {
    println(message + " " + who)
  }
}

Scalaコンパイラimplicitパラメータが明示的に与えられていないときはメソッドの呼び出し箇所からプレフィックス無しでアクセス可能なimplicit付き識別子を探し出してパラメータを適用します。 Scalaコンパイラ頑張りすぎてない?大丈夫?過労死しない?顔色悪いんじゃない?

また、プレフィックスなしでアクセス可能なら検索対象になりうるということは、何でもかんでもワイルドカードでインポートすると誤爆する可能性もあるってことですよね。

当たり前と言えば当たり前ですが、候補となるパラメータが複数ある場合はどちらを使えばいいか判断ができないためコンパイルエラーとなります。

  def main(args: Array[String]): Unit = {
    implicit val hoge = "jyuch"
    implicit val fuga = "hoge"

    // Error:(8, 10) ambiguous implicit values:
    // both value fuga of type String
    // and value hoge of type String
    // match expected type String
    // greet("Hello")
    greet("Hello")
  }

こちらも当たり前と言えば当たり前ですが、トレイトの実装クラスや抽象クラスの実装クラスも適用します。

object Hello {
  def main(args: Array[String]): Unit = {
    implicit val fuga = new Fuga()

    greet("Hello")
  }

  def greet(message: String)(implicit who: Hoge): Unit = {
    println(message + " " + who.who)
  }
}

trait Hoge {
  val who = "hoge"
}

class Fuga extends Hoge {
  override val who = "jyuch"
}

また、シングルトンオブジェクトでも出来るようです。(これは意味の無い例ですが)

object Hello {

  implicit object Hoge {
    val who = "jyuch"
  }

  def main(args: Array[String]): Unit = {
    greet("Hello")
  }

  def greet(message: String)(implicit who: Hoge.type): Unit = {
    println(message + " " + who.who)
  }
}

また、パターンとして

  • compound type(hoge: Hoge with Fugaみたいなアレ)
  • parameterized type(S[T1, T2...Tn]みたいなアレ)
  • type projection S#U(ネストクラスをいい感じにするアレ)
  • エイリアス
  • implicit val aaa: Hoge => Fuga = { x: Hoge => Fuga(x.name) }def piyo(implicit a: Hoge => Fuga)ってなるアレ(名前を知らない)

も大丈夫なようです。*1

で、一番気になるのが暗黙の型変換と暗黙のパラメータを組み合わせて適用可能なのかといったところです。これが可能ならば暗黒プログラミングが捗る可読性が下がってつらいですね。

object Hello {

  implicit def hoge2Fuga(hoge: Hoge): Fuga = hoge match {
    case Hoge(x) => Fuga(x)
  }

  def main(args: Array[String]): Unit = {
    implicit val name = Hoge("jyuch")

    greet("Hello")
  }

  def greet(message: String)(implicit who: Fuga): Unit = {
    println(message + " " + who.name)
  }

  def piyo(implicit a: Hoge => Fuga) {}
}

case class Hoge(name: String)

case class Fuga(name: String)
Error:(14, 10) could not find implicit value for parameter who: example.Fuga
    greet("Hello")
Error:(14, 10) not enough arguments for method greet: (implicit who: example.Fuga)Unit.
Unspecified value parameter who.
    greet("Hello")

暗黙の型変換をぶちかましつつ暗黙の引数に突っ込むまではしないようです。

おわってない

と、ここまでで仕様書のImplicit Parametersセクションの半分です。

残り半分くらいでネストされた場合のimplicitパラメータの挙動について述べていますが、力尽きました。

おわりに

Scalaむずかしい

*1:2.12の言語仕様ではもう一つ箇条書きの項目があるのですが、何を言っているのか弊社には全然わかりませんでした