1. 概要

このチュートリアルでは、Scala3で導入されたマルチバーサル平等と呼ばれる新しい平等機能について学習します。 普遍的な平等の不利な点とそれらを克服する必要性について簡単に見ていきます。

いくつかの簡単な例を使用して、そのアプリケーションの重要な詳細と実用的な側面のいくつかを掘り下げます。

2. 普遍的な平等のデメリット

Scalaは、強く静的に型付けされた言語です。 しかし、型安全性に欠ける特定の領域があります。 Equalityはその1つです。 Scala 2までは、Javaのような他のプログラミング言語と同様の普遍的な平等があります。 ユニバーサルイコールまたはルーズイコールを使用すると、タイプが異なる場合でも、任意の2つの変数を相互に比較できます。 これにより、プログラムで意図しない問題が発生する可能性があります。

異なるクラスに属する2つのオブジェクトを比較するシナリオを見てみましょう。

case class Square(length: Float)
case class Circle(radius: Float)
val square = Square(5)
val circle = Circle(5)

println(square == circle) // prints false. No compilation errors

このような比較は、2つの異なるタイプのオブジェクトを比較するため、意味がありません。 プログラムは問題なく正常にコンパイルされますが、実行時に問題が発生し始めます。 このようなケースは、主にプログラマーのミスの結果として発生します。

開発が進むにつれて、後の段階で問題を修正することは常に困難になります。

したがって、可能な場合はコンパイル時にエラーをキャッチすることをお勧めします

3. マルチバーサル等式または厳密な等式

Scalaの開発者は、ゆるい平等によって引き起こされる問題を早い段階で認識していました。 その後、この問題を修正するためにいくつかの試みが行われました。 コンパイラは、いくつかの安全でない同等性の比較に対して警告メッセージを表示します。 しかし、このメカニズムは広範ではなく、複数の抜け穴があります。

Scala 3は、ユニバーサルイコリティによって引き起こされる問題に対処するために、マルチバーサルイコリティと呼ばれる新機能を導入しています。 strict等式とも呼ばれます。 この新機能を使用すると、等式比較をよりタイプセーフに行うことができ、したがって、多くの意図しないプログラミングエラーを減らすことができます。 デフォルトでは、厳密な等価性チェックはコンパイラーによって無効にされています。

オプションでscala.language.strictEqualityをインポートすることで、を有効にすることができます。

4. CanEqualインスタンスを使用して比較を有効にする

ただし、場合によっては、異なるタイプに属するオブジェクトの等価比較を有効にする必要があります。 このような場合、CanEqualと呼ばれる特別なtype-classのインスタンスを作成する必要があります。タイプ。

CanEqualインスタンスを作成する方法は2つあります。

4.1. 型クラスの派生を使用する

2つの方法の1つ、そして最も簡単な方法は、型クラスの派生手法を使用してインスタンスを作成することです。 コンパイラは、バックグラウンドで指定されたインスタンスを自動的に作成します。

import scala.language.strictEquality	//Enables Multiversal-Equality
case class Circle(radius: Float) derives CanEqual
val circle1 = Circle(5)
val circle2 = Circle(5)
println(circle1 == circle2)	//No compilation errors & prints true.

4.2. 与えられたインスタンスの使用

より多くの制御と柔軟性が必要な場合は、比較するタイプに対して独自のインスタンスを定義できます。 たとえば、特定のケースでは、2つの異なるタイプのオブジェクトを比較する必要がある場合があります

このようなインスタンスは、型クラスの派生を使用して定義することはできません。 そのようなシナリオを見てみましょう。 特定の受信者に電子メールと手紙を送信するメッセージングシステムがあるとします。 ビジネスロジックでは、電子メールと手紙の比較が必要になる場合があります。

最初のステップとして、すべての汎用フィールドを含むMailという新しいtraitを定義しましょう。

trait Mail() {
  val fromName: String
  val toName: String
  val subject: String
  val content: String

  override def equals(that: Any): Boolean =
    that match
      case mail:Mail =>
        if this.fromName == mail.fromName
          && this.toName == mail.toName
          && this.subject == mail.subject
          && this.content == mail.content
        then true else false
      case _ =>
        false
}

次に、2つのケースクラスを定義してみましょう。1つはメールとレター用で、Mail特性から拡張します。

case class Email(fromName: String, toName: String, subject: String, content: String, toEmailId:String) extends Mail
case class Letter(fromName: String, toName: String, subject: String, content: String, toAddress:String) extends Mail

私たちのユースケースでは、電子メールオブジェクトとレターオブジェクトを比較する必要があります。 strict-equalityが有効になっていて、オブジェクトが異なるクラスに属しているため、クラスにCanEqual指定のインスタンスを定義しない限り、コンパイラはエラーをスローします。

それでは、emailオブジェクトとletterオブジェクト、およびそれらのCanEqualインスタンスを作成してこれを実行しましょう。

val email = Email("John", "Annie", "Hii", "How are you", "[email protected]")
val letter = Letter("John", "Annie", "Hii", "How are you", "16th Street, ParkLane, LA")

given CanEqual[Email, Letter] = CanEqual.derived

これで、等式比較を安全に呼び出すことができます。

println(email == letter)  // Compiles successfully and prints true

5. CanEqualデフォルトインスタンス

CanEqualオブジェクトには、多くのデフォルトの実装があります。 これらは主にScalaのプリミティブタイプで利用できます。 それに加えて、 java.langパッケージのNumber Boolean Character 、および Seq scala.collectionSetにもデフォルトのインスタンスがあります。

これらすべてのタイプで再帰インスタンスを使用できます。つまり、等式比較の両方のオブジェクトが同じタイプに属します。 たとえば、 Int データ型の再帰インスタンスは、タイプ CanEqual [Int、Int]になります。

さらに、2つの異なるタイプに対しても定義されたインスタンスがあります。 CanEqual [Float、Double] は、Float値をDoubleと比較するための例です。 このようなインスタンスは、数値タイプのすべての組み合わせに対して定義されているため、毎回明示的にインスタンスを定義しなくても、それらのいずれかをシームレスに比較できます。

6. 結論

このチュートリアルでは、Scala3で導入された新しい等式制約機能について学習しました。 私たちは、普遍的な平等の危険性とそれが私たちのプログラムで引き起こす可能性のあるエラーを見てきました。 次に、 strictEquality コンパイラスイッチをインポートすることにより、マルチバーサルイコリティがその問題をどのように解決するかを確認しました。 次に、 CanEqual 型クラスインスタンスに移動し、型間の比較を明示的に有効にする方法を学びました。

最後に、Scala3ライブラリによって提供されるCanEqualインスタンスのすべてのデフォルト実装を確認しました。

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