LoginSignup
2
2

More than 5 years have passed since last update.

implicit修飾子で始める暗黙の型変換,暗黙クラス,暗黙パラメータ

Last updated at Posted at 2019-02-03

Introduction

先日、Scalaの言語仕様の理解を進めていく中で、型クラスを実現するためのimplicit修飾子について学んでいました。調べていく中でどうやらHaskellでは、言語仕様レベルで型クラスの機能があるようですが、Scalaでは、implicit修飾子を使って少し工夫して実装していく必要があるようです。
そして、implicitってそもそもどのようなケースで使うんだっけ?という疑問があったので、ここに綴ります。
この記事では、implicitってそもそも何だっけ?という疑問についてのアンサーを明らかしつつ、implicitがどのようなケースで使えるのかというところまで記事にしています。
次回は型クラスについての記事を書きます。
マサカリ大歓迎です。

implicit修飾子とは

そもそもScalaのimplicit修飾子は、型クラスを実現するために作られたScalaの機能の1つです。
implicitとは「暗黙的な」といったニュアンスがあり、Scalaでは、「暗黙の型変換」、「暗黙クラス」、「暗黙のパラメータ」といった使用パターンがあります。この3つの中の、「暗黙のパラメータ」の一般的に使われるケースとして型クラスがあるのですが、今回は、この3つの使うケースを明らかにしていきたいと思います。

1.暗黙の型変換とは

ある型に対して別の型が適合しなかった場合に起動する、型の変換処理をユーザーが定義できる機能です。

これだけだと理解としては漠然としているので、コードに起こしてみます。

ちなみに、暗黙の型変換は、主に下記の2つの場合に適用されます。
・互換性のない型が渡される場合
・存在しないメソッドが呼び出される場合

互換性のない型が渡される場合

implicit def intToBoolean(n: Int): Boolean = n ! = 0
if(1) {
  println("1 is true")
}

if文で、期待されている型はBooleanなのですが、今回、Int型が渡されています。
ですが、これはエラーを吐かず実行され、1 is trueが出力されます。
これは、グローバルでimplicit修飾子で定義されたintToBooleanメソッドが呼び出されて、暗黙的にif文に渡したInt型の1を、n! = 0で判定して、trueを返しているからです。
型を変換するメソッドを予め、implicit修飾子を使用してグローバルで定義しておくと、暗黙的に定義した、メソッドが呼び出されて型を変換します

ただし、既存の型への暗黙の型変換は、型安全性を壊す恐れがあるため、現在は推奨されていません。

ある既存の型に対して存在しないメソッドを追加したい場合

ある型Aに対してメソッドmが呼び出された時、Aから別の型Bへの暗黙の型変換が定義されており、Bにメソッドmが定義されている時に適用されます。この場合の型は既存クラス全般を含みます。

このケースでは、既存の方に対してメソッドが追加されたかのように見せかけることができます。

現在使われている暗黙の型変換は、下記のようこのような既存の型に対して、メソッドが追加されたかのように見せかけるために使われます。実際にコードを見てみましょう。  

この例では、正の整数かどうか判定してBoolを返すメソッドisPositiveメソッドをInt型に追加するケースを見ていきます。

class RichInt(val self: Int){
  def isPositive: Boolean = self > 0
}
implicit def enrichInt(self: Int):RichInt = new RichInt(self)

このように定義があると、以下のような形でメソッドが呼び出すことができるようになります。

1.isPositive // true

上記をコンパイルするとこのようになります

new RichInt(1).isPositive

つまり、Int型の1が呼び出されたので、暗黙的にenrichIntメソッドが呼び出されている事がわかります。

このメソッドでは、Int型をクラスパラメータとして受け取ったRichInt型のインスタンスが呼び出されるので、Int型の1に対して、isPositiveメソッドが使えるわけですね。

このように既存の型に対してメソッドを追加したい場合は、拡張したいメソッドを型として定義して、
そのインスタンスを返すimplicit修飾子を使ったメソッドを定義すると、型に対してメソッドを拡張しているように見せかける事ができます。

このように、既存の型に対してメソッドを追加する目的で暗黙の型変換を定義することを「enrich my librayパターン」と呼びます。

2.暗黙クラスとは

上記では、暗黙の型変換のenrich my libraryパターンを使っていましたが、既存の型に対してメソッドを追加するためには、以下の2つを同時に定義する必要がありました。
・新しいクラス(既存の型に追加したいメソッドを定義するクラス)
・追加したいメソッドを定義したクラスのインスタンスを返すメソッド(暗黙の型変換を行うメソッド)

ただ、既存の型に対してメソッドを追加するために、上記の2つを行うのは非常に冗長です。
これをより簡潔に書くために、暗黙クラスは使われます。

暗黙クラスは、クラス定義の際にimplicitを追加することで2つの定義を自動的に生成する機能で、以下のように使うことができます。

implicit class RichInt(val self:Int){
  def isPositive: Boolean = self > 0
}
1.isPositive

暗黙クラスは、enrich my libraryパターンが多様されるようになったため、Scala2.10で導入された機能です。現在、使われているScalaのバージョンは、ほとんどが2.10以降であるため、既存の型にメソッドを追加したい場合は、暗黙クラスを使うと良いでしょう。

また、暗黙クラスは通常のクラスと違い、トップレベルに置くことができないので、パッケージオブジェクトや、他のオブジェクトの下に置くことが多いです。

3.暗黙のパラメータ

暗黙のパラメータによって、プログラマは明示的に引数を指定しない場合に暗黙的に渡される引数を指定できます。

暗黙的な状態の引き渡し

本来型クラスのためにも使われますが(このセクションの次から型クラスについて説明します)下記のような使い方もできますよって話です。

暗黙のパラメータの型クラス以外の使い方としては、暗黙的な状態の引き渡しです。漠然としているので、下記のコードを見てみます。

implicit val context = 1
def printContext(implicit ctx: Int): Unit = {
  println(ctx)
}
printlnContext //1 引数を指定せず暗黙的に変数contextが呼び出される

このプログラムでは、1が出力されます。
これをさらに拡張して、contextを他のメソッドにも暗黙的に引き渡してみましょう。

def printContext(implicit ctx: Int): Unit = {
  println(ctx)
}

def printContext2(implicit ctx: Int): Unit = {
  prilntln(ctx)
}

implicit val context = 2
printContext2 //2

上記を見てわかると思いますが、同じ変数(状態)を複数のメソッドの引数として、暗黙的に引き渡されていることがわかります。
実用レベルで言うと、ただのInt型の値を共有することは少ないですが、DBのコネクションなどで、あちこちで使い回されるので、使い回される「状態」は、implicitで定義しておくと、引数で渡す手間が省けます。

暗黙のパラメータ

暗黙のパラメータは、型クラスの機能を実現するために使われます。Introducitonでも記した通り、Haskellの型クラスの機能をScalaでも取り入れるために、implicitによる暗黙のパラメータの機能が用意されています。

以降では、「リストの要素の値を全て合計してその値を返す」というメソッドの例を通じて、暗黙のパラメータが一般的に解決してくれる問題について記していこうと思います。

暗黙のパラメータを使わない方法での実装

まずは、型クラスの便利さを知ってもらうために、型クラスを使わない場合の実装について、見ていきます。

def sumInt(list: List[Int]): Int = list.foldLeft(0){
  (x, y) => x + y
}

また、double型のリストやString型のリストも実装します。
String型に関しては、「文字列の合計」を「文字列の結合」と考えたものです。

def sumDouble(list: List[Double]): Double = list.foldLeft(0){ 
  (x, y) => x + y
}

def sumString(list: List[String]): String = list.foldLeft(0){
  (x, y) => x + y
}

ここまでで、気づいたこととして、

・他の型に対しても同様のメソッドを定義しようとすると要素の方が増えるのに対して、際限なくコードが重複する。
・また2次元ベクトルのような型に対して定義しようと思うと、要素の種類の2乗という莫大なパターンに対して、対応する必要がある。

と、様々なデメリットが発生します。

これらのデメリットを解決するためにトレイトを使ってコードを共通化する必要があります。
つまり、抽象化したインターフェースを用意してあげて、継承先で具象化したメソッドを定義し、呼び出し元を共通化することで解決することができます。

trait Adder[T]{
  def zero: T
  def plus(x: T, y: T): T
}

object IntAdder extends Adder[Int] {
  def zero = 0
  def plus(x: Int, y: Int) = x + y
}

def sum(list: List[T])(adder: Adder[T]) :T = {
  list.foldLeft(adder.zero){(x, y) => adder.plus(x,y)}
}

sum(List(1,2,3)(IntAdder)) //6

これによりsumの定義を変更せずに、要素の型に応じたAdderの定義を追加するだけで、sumの挙動を変更できるようになりました。ポリモーフィズムを使って、呼び出すメソッドの共通化ができます。これにより、上記のようなデメリットは解消されます。

これまでが、通常のポリモーフィズムを使った実装です。ですが、これでは、Int型のリストの合計値を計算することがわかっているのに、明示的にIntAdderを渡さなければならないので煩雑です。

この具象化したオブジェクトである、Adderを自動的にコンパイラがどのAdder引数に渡せよいか推論してくれればベターです。これが暗黙のパラメータの役割です。

手順としては、推論して欲しいパラメータにimplicit修飾子をおいてあげれば良いです。
また、そのパラメータとして渡すオブジェクトにもimplicitをおいてあげましょう。

trait Adder[T]{
  def zero: T
  def plus(x: T, y: T): T
}

implicit object IntAdder extends Adder[Int] {
  def zero = 0
  def plus(x: Int, y: Int) = x + y
}

def sum(list: List[T])(implicit adder: Adder[T]) :T = {
  list.foldLeft(adder.zero){(x, y) => adder.plus(x,y)}
}

sum(List(1,2,3)) //6 <- コンパイラが推測して、IntAdderをパラメータとして暗黙的に渡してあげている。

ちなみに他の型に対してのsumを提供したい場合でも、同様に、implicitをつけて、Adder[T]を定義してあげると、メソッドの呼び出し時に、そのオブジェクトを暗黙的に推測してくれます。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2