Help us understand the problem. What is going on with this article?

Scala implicit修飾子 まとめ

More than 3 years have passed since last update.

Scalaには他の言語ではなかなか見かけない,謎の修飾子「implicit」が存在します。
僕もScala始めたばかりの時には,意味がわからず困惑しました。ぐぐっても個々の使い方は載っていても,まとまって解説しているところはあまりなく・・・。
そこで,多少なりScalaをかじった僕がまとめておこうと思います。
Scalaレベルはポケモンでいうと,まだ20〜30Lvくらいなので,ご了承ください。

1. implicitの概要

implicitとは「暗黙的な」という意味です。逆の意味をもつ単語はexplicit。
C#を使っている人は,これらの言葉を聞いたことはあるでしょう。
(しかし,ScalaのそれとC#のそれでは,全く意味は異なりますが・・・。)

implicitには様々な使い方があります。その中で全てに共通するのは,「暗黙的に何かをしてくれる」という点です。
これだけ聞くと,「それって,意図しない動作をして,危険じゃない?」と思う型もいらっしゃると思います。
その通りです。implicitはむやみに使うのは危険です。しかし,上手く使いこなせれば,とても美しいコードが書けるようになります。現に,Scalaの標準ライブラリやPlayFrameworkではしばしばこのimplicitが使われています。
この辺りは具体例として,後に解説を掲載します。

2. 3つのimplicitの使い方

2-1. 暗黙の型変換

2-1-1. 型が合ってない時に暗黙の型変換

Scalaは強い型付け言語です。コンパイル時に型があっていないとそれだけでブチ切れます。

Main.scala
object Main {

    def logInt(i: Int) = println(i)

    def main(args: Array[String]) = {
        logInt(1)
        logInt(2)
        logInt(3)
        logInt(0.2f)
    }

}
[error]  found   : Float(0.2)
[error]  required: Int
[error]         logInt(0.2f)
[error]                ^
[error] one error found
[error] (compile:compileIncremental) Compilation failed

こういう時に,勝手に都合のいいように型を変換してくれるのがimplicitです。
implicitな関数を定義してみましょう。

Main.scala
object Main {

    implicit def floatToInt(f: Float): Int = f.toInt
    def logInt(i: Int) = println(i)

    def main(args: Array[String]) = {
        logInt(1)
        logInt(2)
        logInt(3)
        logInt(0.2f)
    }

}
1
2
3
0

このように,コンパイル時にFloatからIntに変換できる関数があるかを探して,勝手にやってくれます。
他のimplicitの使い方でもそうですが,implicitな何かを使えるのはスコープ内です。
main関数の中ではfloatToIntを直接呼び出せたりする位置にいます。なので,使うことができます。
しかし,Mainオブジェクトの外ではもちろんこの暗黙の型変換は起こりません。

なので,いろんなところで使いたいimplicitがある場合,僕は以下のような方法をとります。

CastImplicits.scala
object CastImplicits {
    implicit def floatToInt(f: Float) = f.toInt
    implicit def doubleToInt(d: Double) = d.toInt
    implicit def stringToInt(s: String) = s.toInt
}
Main.scala
import CastImplicits._
import scala.language.implicitConversions._

object Main {

    def logInt(i: Int) = println(i)

    def main(args: Array[String]) = {
        logInt(1)
        logInt(0.2f)
        logInt(1.234)
        logInt("777")
    }

}

0
1
777

Scalaではこのように,importすることができるのはクラスだけでなく,object等に実装された関数や変数,さらには型エイリアスもimportすることができます。
これは他の状況でも使えるので,是非使ってください。

import scala.language.implicitConversions._

暗黙の型変換を行う場合,これをimportしないと,コンパイル時Warningが止まりません。
その理由はこちらを参考して下さい。僕は何もわかっていません。

なぜ implicit conversion を定義するのに import scala.language.implicitConversions が必要なのか

2-1-2. メソッドがない時に暗黙の型変換

暗黙的に型が変換されるのは型が実装されていない時だけではありません。
次のような場合でも,コンパイラが暗黙に型変換をできないかを考えてくれます。

class SuperInt (val i: Int) {
    def square = i * i
}

implicit def intToSuper(i: Int): SuperInt = new SuperInt(i)

println(1000.square) // -> 1000000

ただのIntにsquareというメソッドは実装されていません。
普通ならここでコンパイルエラーですが,implicitでIntを変換できることがわかります。
そして,intはSuperIntに変換できるみたいです。そして,そのSuperIntはsquareメソッドを持っています。
よって,コンパイルは成功,実行時にも期待していた結果が返ってきます。

2-1-3. 個人的な考え

そもそも,Scalaのimplicitはこのように使うものではないと思っています。多少,タイピング量を減らすことはできると思いますが,危険です。
プログラムが大きくなれば,重大なバグを生み出す可能性があります。
僕は暗黙の型変換ではほとんどimplicitを使うことはありません・・・。
現に,公式的にもあまり暗黙の型変換は利用するべきでないと言ってるようですので・・・。
しかし,唯一僕が使う箇所があります。

2-1 (実用例) JavaのコレクションをScalaで扱う

Scalaと生きていると,どうしてもJavaと向き合わなければならないことがあります。
Scalaでは実装されていなくて,Javaで実装されているライブラリを使うことは割とあります。
そして,同時にScalaと生きていると,Scalaのコレクション操作になれてしまいます。
リストをつくるのに今更,

ListSample.java
List<int> list = new ArrayList<int>();
list.add(1);
list.add(2);
list.add(3);

とか,したくないですもんね。したくないですよね。嫌ですよね。
Javaそこまでやってないので,もしかしたらもっと楽な方法があるかもしれませんが・・・。
たとえリストを楽に作れたところで,そこからが問題です。
Javaのコレクションに対しては,Scalaで培ったコレクション操作を一切することができなくなります。これは大問題ですね。
Scalaのコードの中にミュータブルな部分がでてくるだけで体がかゆくなりますよね。

そこで登場するのが,暗黙の型変換です。

import scala.collection.JavaConversions._

これを記述することで,JavaのコレクションとScalaのコレクションを双方向かつ暗黙的に変換してくれます!
故に,java.util.Listには実装されていないメソッドでも・・・

Main.scala
val javaList: java.util.List[Int] = new java.util.ArrayList[Int]()
javaList.add(1)
javaList.add(2)
javaList.add(3)
println(javaList.map(_ * 100).sum)

このように,ScalaのListに変換されているので,上手く実行されます。
もちろん,逆もしかりです。何かのライブラリでjava.util.Listを受け取るように作られている場合でも,
Scalaのリストを渡せば,自動的にjava.util.Listにしてくれるので,変換を気にすることはありません。
これはなかなか使えます。

ここで1つ疑問が浮上します。JavaのリストからScalaのリストへ変換する実装はどうなっているのだ?
要素をコピーして,新しいオブジェクトを作っているのなら,コストが・・・。

大丈夫です。詳しくはこちら。要は型変換してるだけです。

scalaでjavaのList使うときの話

2-2. 既存の型を拡張する(implicit class)

静的型付け言語では,動的型付け言語よりも既存のクラスを拡張するのが難しいかもしれません。
まあ,あくまでも糖衣構文って感じですからね。なければ実装できないものってあんまり思いつきません。

とはいえ,ソースコードが美しいに越したことはありません。拡張できたら,美しくなることは沢山あります。

implicit classがやってくれることは,2-3-2でやったこととほとんど同じです。
「あるオブジェクトメソッドが実装されていない場合,それを変換できるimplicit関数があれば,それで変換する。」

implicit classの場合,少しアプローチが異なるだけです。「そもそも変換を前提に定義する」といったところでしょうか・・。

2-2-1. 実装されてなければ変換する

では早速。

object TimeImplicits
{
    implicit class TimeInt(i: Int) {
        def days = i * 1000 * 60 * 60 * 24
        def hours = i * 1000 * 60 * 60
        def minutes = i * 1000 * 60
        def seconds = i * 1000
    }
}

implicit classは,ソースコードに直接置くことはできません。
なにかobjectに包んでください。そして,今まで同様,importして使えるようにします。
また,implicit classのコンストラクタの引数は必ず1つです。
implicit classのコンストラクタの引数は,変換の対象みたいなものですからね。
複数対象が存在することはあり得ないのです。

では,利用してみましょう。

import TimeImplicits._
import scala.language.implicitConversions

println("3秒間だけ待ってやる")
Thread.sleep(3.seconds)
println("くらえ!")

ちょっとかっこいいコードができるようになります。
これを使えば,他の言語の便利な機能を取り入れたりすることも簡単です。
例えば,Rubyの5.times等の実装もできるでしょう。

2-2-2. 個人的な考え

やってることは2-1とほとんど変わらないですね。

なんなら,implicit classを普通のclassにして,implicit関数で変換してもいけますよね。
その2つのやり方の差を僕は知りません。C#ではクラス拡張よく使いますが,Scalaではそもそもあんまりやらないので,
詳しいことはわかっていません・・・。ゴメンなさい。
個人的には,変換方法としては同じかなと思っています。

詰まる所,implicit classも型変換だったわけですが,implicit関数とは違うところがあります。
それは,implicit classでは暗黙的に既存の型へ変換することができないという点です。
当然です。classとして定義した段階で,それはもう新しいものなのです。
なので,「既存の型の拡張」として,implicit classを紹介しました。既存の型の値を受け取って,やりたい事をできるようにする。
しかし,implicit関数は既存の型へ変換する事もできます。2-1-1の例が顕著な例です。
あのような事はimplicit classでは起こりえません。

まとめると,いい意味でも悪い意味でもimplicit関数の方ができる事は多い,といったところでしょうか。

2-2 (実用例) ScalaのStringはjava.lang.Stringだって?

Scalaを触って間もない頃,疑問に思った事があります。

val s: java.lang.String = "Hello Implicit!"

Scalaの文字列はjavaの文字列使ってるだけか〜
そうかそうか〜
むむ,じゃあなんで.toIntとか使えるんだ・・・?

まあ,今やこれの答えは簡単ですよね。
Scalaの内部にはStringLikeというクラスがあります。何か標準でimportされている変換方法で
変換されて,便利なメソッドを使えるということですね。

2-3. implicit引数とimplicit変数

2-1, 2-2とは相対するimplicitの使い方です。Scala公式ではこの使い方を推しているようです。
まあ,そもそも暗黙の段階で,どれも怖いんですけど・・・。

implicitパラメーターは提供するAPIをシンプルにすることにしばしば使われています。

2-3-1. implicitは値と引数をつなげるキーワード

    implicit val default = 100
    def squareInt(implicit x: Int) = x * x

    def main(args: Array[String]) = {
        println(squareInt)
    }

簡単な例を出すとこうなります。さて,コンソールには何が表示されるでしょう?
implicitがなかった場合,squareIntの型,つまりFunction1の型名が表示されるでしょう。
しかし,今回はimplicitがあります。以下のルールにより,10000と表示されます。

  1. 関数の引数群にimplicitをつけることができる。
  2. implicitな引数に具体的な値を渡さなかった場合,コンパイラはimplicitが付いている値・関数から型の合うものを探し,渡してくれる。(探す範囲はその呼び出しのスコープ内からです。)
  3. 値の候補はimplicit def, implicit val, implicit objectである。
  4. よって,関数が正しく引数を受け取って実行される。

さて,主な注意点もいくつか。

  1. 候補が1つに定まらない場合,コンパイルエラーとなる。
  2. 2. 引数が1つの関数にしか,implicit引数を定義することはできない。

[追記]複数のimplicitも可能でした。

def hoge(implicit x: Int, s: String): String = x * s

このように指定すると,implicitが付いている引数軍はすべてimplicitになります。
故に,implicit値をInt, Stringと用意してhogeと呼び出すか,
直接値を代入してhoge(10, "Foo")として呼び出すかのどちらかになります。

試してみましたが,例えばIntのimplicit値だけ作っておいて,
hoge("Hello")とかはできません。

こんな風に自由度を持たせるためにはカリー化して

def hoge(implicit x: Int)(implicit s: String) = x * s

とするのが良いのかな。

[追記]
こんなことできませんでした。カリー化した場合は最後の引数群のみimplicitにできます。
適当なことを書いてしまい,申し訳ございません。

2-3-2. 複数の引数をもたせたい場合

さて,引数が1この場合は上記の例により,implicitパラメータが繋がって,関数が実行されることはわかりました。
ここからが本当のimplicitパラメータです!

カリー化

ここからはカリー化を使います。Scalaにおけるカリー化された関数の定義は以下のようにできます。

def curryFunc(x: Int, s: String)(z: Float)(xs: List[Int]): Int = ...

val f1 = curryFunc(10, "Hello") // Float -> List[Int] -> Int
val f2 = f1(0.1f) // List[Int] -> Int
val value = f2(List(1, 2, 3)) // Int

カリー化の詳しいことは調べてくださいね。

カリー化を使えば,最後の引数をimplicitにし,スコープ内にimplicitな値があれば,それ以外の部分を埋めるだけで使えるようになるということですね!

最後の引数がimplicit
implicit val defaultList = List(1, 2, 3)
def curryFunc(x: Int, s: String)(z: Float)(implicit xs: List[Int]): String = (s * x) + z + xs.sum

println(curryFunc(1, "Hoge")(0.3f)) // -> Hoge0.36

さらに楽しい例はこちら
implicitな値を使った例

疲れたのでここまで・・・
お疲れ様でした。

miyatin0212
Game and Web developer
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした