1. 序章

Scalaの最も強力で広く議論されている機能の1つは、その暗黙的機能です。 強力であるかもしれませんが、経験豊富な開発者であっても、その複雑さに関しては依然として多くの不満があります。 これらの問題のために、Scalaコンパイラチームは新しいScala3のimplicit を再設計し、より明確で使いやすくしました。

このチュートリアルでは、implicitの使用法がScala3でどのように再設計されたかを学びます。

2. Scala2暗黙的欠点

Scala2のimplicitの主な欠点は次のとおりです。

  • 無関係な機能のためのキーワードimplicitの使用
  • 暗黙の変換による意図しない変換
  • 不明瞭なコンパイラエラーメッセージ

これらが原因で、経験豊富な開発者でさえ、暗黙的に関連する問題のいくつかをデバッグするのが非常に難しい場合があります。 その結果、Scala 3では、主な焦点の1つは、既存の暗黙的な機能を再設計することでした。

3. 新しいキーワード

再設計の一環として、implicitの代わりに2つの新しいキーワードがScala3に導入されました。 ただし、 implicitキーワードはScala3でも引き続きサポートされており、今後のリリースで削除される予定です。

3.1. 与えられた

give キーワードは、暗黙の値のインスタンスを定義するために使用されます

given timeout: Int = 10 

Scala 3では、giveインスタンスに名前を付ける必要はありません。 名前なしで直接値を割り当てることができます

given Int = 10

3.2. 使用

using キーワードは、メソッドに暗黙のパラメーターを渡すために使用されます

def execute(url:String)(using timeout: Int):String = "{}"

implicit値を次のように明示的に指定することもできます。

execute("http://www.baeldung.com")(using 4)

4. スコープから暗黙の値を召喚します

Scala 2では、implicitly メソッドを使用して、スコープから使用可能なimplicit値を呼び出すことができます。 たとえば、スコープから実行コンテキストを取得するには、次のように記述します。

val ctx = implicitly[ExecutionContext]

Scala 3では、このメソッドは削除され、summonに置き換えられました。

val ctx = summon[ExecutionContext]

5. 暗黙の変換

暗黙的な変換は、明示的な変換を行わずに値をあるデータ型から別のデータ型に変換する方法です

処理時間データを処理する必要があると仮定しましょう。 メソッドは次のように定義できます。

case class Second(value: Int)
object TimeUtil {
  def doSomethingWithProcessingTime(sec: Second): String = {
    // impl logic
    s"${sec.value} seconds"
  }
}

秒単位の値がある場合は、 doSomethingWithProcessingTime()を呼び出す前に、ケースクラスSecondでラップする必要があります。 暗黙の変換は、この明示的な型変換を回避する方法を提供します。

5.1. Scala2での使用法

それでは、Scala2でimplicit変換を実装する方法を見てみましょう。 implicitキーワードを使用してメソッドを定義できます。

object ImplicitConversion {
  implicit def intToSecond(value: Int): Second = Second(value)
}

doSomethingWithProcessingTime()を呼び出す前に、implicit変換をスコープに取り込むことができます。 その後、 Second でラップする代わりに、Int値を使用してメソッドを呼び出すことができます。

import ImplicitConversions._
val processingTime = 100
//auto conversion from Int to Second using intToSecond() from scope
TimeUtil.doSomethingWithProcessingTime(processingTime)

この使用法は単純に見えますが、暗黙の変換により、大規模なプロジェクトで意図しないエラーが発生する可能性があります。 これらのimplicit変換を無意識のうちにインポートし、予期しない動作を引き起こす可能性があります。

5.2. Scala3での使用法

この欠点を回避するために、Scala3はConversionタイプクラスを導入しました。 giveキーワードを使用して定義できます。

object ImplicitConversion {
  given Conversion[Int, Second] = Second(_)
}

上記のコードは、IntからSecondへのConversionインスタンスを定義します。 give インスタンスをインポートすると、Int値がSecondに暗黙的に変換されます。

object Usage {
  import ImplicitConversion.given
  val processingTime = 100
  //auto conversion from Int to Second using given
  TimeUtil.doSomethingWithProcessingTime(processingTime)
}

変換をスコープに取り込むには、ImplicitConversion.givenを使用する必要があることに注意してください。 ワイルドカードを使用してインポートしても、giveインスタンスはスコープに表示されません。 このようにして、指定されたインスタンスを誤ってスコープにインポートすることはありません。

SecondからIntへの変換を実装することもできます。

given Conversion[Second, Int] = _.value

6. 拡張メソッド

拡張メソッドを使用すると、サードパーティタイプにメソッドを追加できます。 Pimp My Library Pattern とも呼ばれ、既存のタイプを変更せずに拡張する非常に強力な方法です。

6.1. Scala2での使用法

Scala 2では、implicitキーワードを使用して拡張メソッドを実装します。 たとえば、Int値をSecondケースクラスに簡単に変換する場合は、implicitクラスを使用できます。

object Extension {
  implicit class IntExtension(value: Int) {
    def toSecond() = Second(value)
  }
}

これで、暗黙クラスをインポートし、 toSecond()メソッドをIntクラスで定義されているかのように使用できます。

import Extension._
val second: Second = 100.toSecond()

6.2. Scala3での使用法

Scala 3 は、拡張メソッドを実装するためのextensionキーワードを導入しました。

object Extension {
  extension(sec: Int) def toSecond() = Second(sec)
}

Scala 2と同じ方法で、拡張メソッドをインポートして使用できます。

import Extension._
val sec: Second = 10.toSecond()

拡張メソッドで型引数を使用することもできます。 任意の数値に対して拡張機能メソッドを作成するとします。

object NumericExtensions {
  extension [T:Numeric](v1: T)
    def add(v2: T): T = summon[Numeric[T]].plus(v1,v2)
}

7. 結論

この記事では、Scala3でのimplicit機能の再設計について説明しました。

いつものように、記事の完全なソースコードは、GitHubから入手できます。