1. 序章

このチュートリアルでは、Scalaのポリモーフィズムのタイプと、関数型プログラミングの分野での拡張について説明します。

Scalaのような関数型プログラミング言語には、Javaのようなオブジェクト指向言語では利用できない機能があることに注意することが重要です。

これらの独自の機能により、プログラムに適用できるポリモーフィズムの可能性が高まります。 Javaでのポリモーフィズムに関するチュートリアルをチェックして、比較してください。

2. ポリモーフィズムとは

簡単に言うと、ポリモーフィズムとは、1つのオブジェクトが多くの形式を持っていること、またはオブジェクトが複数の動作を示す能力を持っていることを意味します。 コンピュータ用語では、これは同じインターフェイスまたはメソッドを使用して異なるデータ型を処理するように表示されます。 より正式な定義は次のとおりです。

ポリモーフィズムは、さまざまなタイプのエンティティへの単一のインターフェイスの提供です または、単一の記号を使用して複数の異なるタイプを表します。

次のセクションでは、さまざまなタイプのポリモーフィズムについて説明します。

3. パラメトリック多型

このセクションから1つのステートメントだけを取り上げる場合、パラメトリックポリモーフィズムは、Java、C#、Scalaなどの言語で使用されるジェネリックにすぎません。

メソッドシグネチャに角かっこで区切られた1つ以上の型パラメータが存在することで、Scalaのパラメトリックポリモーフィック関数を簡単に認識できます。これらにより、同じロジックを異なるデータ型に適用できます。

これを説明するために例を見てみましょう。 Listをペアワイズで反転するメソッドを作成していると想像してください。 List(1,2,3,4,5,6)を入力し、 List(2,1,4,3,6,5)を出力できるようにします。 。 長さが奇数の場合、最後の要素は、 List(1,2,3,4,5) List(2,1,4,3,5)になるようにその場所にとどまる必要があります。 )。

3.1. 素朴なソリューション

def pairWiseReverseInt(xs: List[Int]): List[Int] = xs.grouped(2).flatMap(_.reverse).toList

上記のシナリオのテストを作成しましょう。

it should "reverse an even length int list" in {
    val input = List(1,2,3,4,5,6)
    val expected = List(2,1,4,3,5,6)
    val actual = pairWiseReverseInt(input)
    assert(actual == expected)
}

私たちが書いたメソッドは整数配列ではうまく機能しますが、他のタイプでは再利用できません。 例として、文字列またはdoubleのリストをペアごとに逆にしたい場合は、追加のメソッドを作成し、ロジックを無意味に複製する必要があります。

def pairWiseReverseString(xs: List[String]): List[String] = arr.grouped(2).flatMap(_.reverse).toList

そしてそれのための簡単なテスト:

it should "pair-wise reverse a string list" in {
    val original = List("a","b","c","d","e")
    val expected = List("b","a","d","c","e")
    val actual = pairWiseReverseString(original)
    assertResult(expected)(actual)
}

3.2. DRYソリューション

パラメトリックポリモーフィズムは、型パラメーターの2番目の仮パラメーターリストを導入することにより、この不要なコードの重複を排除するのに役立ちます。 パラメトリックポリモーフィズムでは、ロジックはすべての異なるタイプで同じままです。 説明のために、上記のペアワイズリバースメソッドをすべてのタイプで機能するメソッドにリファクタリングします。

def pairWiseReverse[A](xs:List[A]): List[A] = xs.grouped(2).flatMap(_.reverse).toList

タイプパラメータを文字Aで表しましたが、 K T 、またはその他の文字を使用することもできます。案件。 すべての正式なパラメーターと同様に、メソッド呼び出し中に A を、 Int String などの具象型、またはその他の具象型に置き換えることが期待されます。場合:

it should "pair-wise reverse lists of any type" in {
    val originalInts = List(1,2,3,4,5)
    val expectedInts = List(2,1,4,3,5)
    val originalStrings = List("a","b","c","d","e")
    val expectedStrings = List("b","a","d","c","e")
    val originalDoubles = List(1.2,2.7,3.4,4.3,5.0)
    val expectedDoubles = List(2.7,1.2,4.3,3.4,5.0)

    assertResult(expectedInts)(pairWiseReverse[Int](originalInts))
    assertResult(expectedStrings)(pairWiseReverse[String](originalStrings))
    assertResult(expectedDoubles)(pairWiseReverse[Double](originalDoubles))
}

Scalaコンパイラーは、渡した関数呼び出し引数から型を推測できることに注意してください。 type引数を明示的に渡すことは必須ではありません。

4. サブタイプポリモーフィズム

サブタイプ多型の重要な概念は、リスコフの置換原則で定義されている置換可能性です。 パラメータタイプの少なくとも1つにサブタイプがある場合、つまり、少なくとも1つのタイプのスーパータイプである場合に、サブタイプポリモーフィズムを示すScala関数を認識できます。

このタイプの関係は、次のように記述されることもあります。 S <: T、 つまり、 S Tまたはのサブタイプです T :> S 、 意味 T のスーパータイプです S 。 これを包含多型と呼ぶこともあります。 パラメトリックポリモーフィズムとは異なり、このサブタイプの関係で関数を適用できるタイプの範囲を制限します。

次の例では、ShapeCircleおよびSquareサブタイプを作成します。 メソッドprintArea() Shape を受け入れますが、サブタイプのいずれかが渡された場合にも正しく機能します。

trait Shape {
    def getArea: Double
}
case class Square(side: Double) extends Shape {
    override def getArea: Double = side * side
}
case class Circle(radius: Double) extends Shape {
    override def getArea: Double = Math.PI * radius * radius
}

def printArea[T <: Shape](shape: T): Double = (math.floor(shape.getArea) * 100)/100

予想される動作をキャプチャするためのテストを追加しましょう。

"Shapes" should "compute correct area" in {
    val square = Square(10.0)
    val circle = Circle(12.0)

    assertResult(expected = 100.00)(printArea(square))
    assertResult(expected = 452.39)(printArea(circle))
}

上記の例では、サブタイピング関係は次のように記述されています。 T <:形状 、タイプの任意の引数を意味します T タイプの用語が使用されるコンテキストで安全に使用できます期待されています。 TShapeのサブタイプである変数のみに制約します。 したがって、関数 printArea()は、Shapeのサブタイプよりも多形性のサブタイプであると言えます。

5. アドホック多相性

アドホック多相性について考える最も簡単な方法の1つは、バックグラウンドで発生しているがデフォルトのケースがないswitchステートメントの観点からです。 この場合、コンパイラは、メソッドが受け取る入力のタイプに応じて、異なるコード実装を切り替えます。

パラメトリックポリモーフィズムとアドホック多相性には類似点があります。どちらもジェネリックスを使用して、コードがさまざまなタイプで機能できるようにします。 主な違いは、前者では、すべてのタイプで同じコードが実行されることです。 後者では、タイプに基づいて異なるロジックが実行される可能性があるため、前の段落のswitch-statementのアナロジーです。

Scalaがアドホック多相をサポートする方法を理解するために必要な3つの優れた概念があります。関数のオーバーロード、演算子のオーバーロード、および暗黙的です。

暗黙のクラスの穏やかな紹介をご覧ください。 次に、他の2つの概念について説明します。

5.1. 関数のオーバーロード

Scalaでアドホック多相が適用される組み込みの例を見てみましょう。 ソートしたい整数のリストがあると仮定します。

it should "sort integers correctly" in {
    val intList = List(3,5,2,1,4)
    val sortedIntList = intList.sorted
    assertResult(expected = List(1,2,3,4,5))(actual = sortedIntList)
}

Listでメソッドsortedを呼び出したところ、整数はプリミティブ型であるため、正確にソートする方法を知っていました。 List にも、ある別のタイプを並べ替える方法を知ってもらいたい場合はどうなりますか。たとえば、学生IDがカスタムクラスにラップされているとします。

case class StudentId(id: Int)

基になる型がIntであるのと同じように、Scalaコンパイラーは型StudentIdをソートする方法を知りません。 私たちのswitch-statementのアナロジーによると、実装されていないケースが発生しました。デフォルトのケースがないことを忘れないでください。

実際のところ、それを試してみましょう。

it should "sort custom types correctly" in {
    val studentIds = List(StudentId(5), StudentId(1),StudentId(4), StudentId(3), StudentId(2))
    val sortedStudentIds = studentIds.sorted
    assertResult(
      expected = List(
        StudentId(1), 
        StudentId(2),
        StudentId(3), 
        StudentId(4), 
        StudentId(5)
      )
    )(actual = sortedStudentIds)
}

このテストはコンパイルに失敗し、次のエラーが発生します。

No implicit arguments of type: Ordering[StudentId]

メソッドsortedは、知らないタイプの並べ替えに役立つことを期待していることがあります。 タイプOrderingは、Javaのコンパレータインターフェースと非常によく似た特性であり、タイプのソート戦略を提供するcompareToメソッドも宣言します。 。

このエラーを修正する慣用的な方法は、コードの範囲内にあるOrdering型の暗黙のクラスまたは値を作成することです。

これはimplicitsに関する記事ではないため、テスト内でStudentIdOrderingタイプの実装を提供することでエラーを修正しましょう。 これを行うには、順序付け戦略を定義し、それを引数としてsortedメソッドに渡します。

    ...
    val ord: Ordering[StudentId] = (x, y) => x.id.compareTo(y.id) 
    val sortedStudentIds = studentIds.sorted(ord)
    ...

5.2. 演算子のオーバーロード

演算子のオーバーロードは、関数のオーバーロードとそれほど違いはありません。 これはであり、引数のタイプに応じて、さまざまな演算子の実装が異なります

一般的な演算子、特に加算(+)演算子や減算(-)演算子などの算術演算子のセマンティクスはわかっています。 演算子のオーバーロードは、次のすべてのコードスニペットがコンパイルおよび機能する理由です。

val intSum = 1990 + 10
val dblSum = 13.37 + 15.81
val strConcat = "FirstName " + "LastName"

加算演算子は、整数、倍精度浮動小数点数、および文字列に対して同様に機能します。これは、これらのタイプごとにオーバーロードされるためです。 コンパイラーは、オペランドのタイプから演算子の正しい実装を推測できます。

これと同じ考えを採用し、既存のScalaオペレーターに過負荷をかけて、独自のニーズに対応することができます。 数学の問題を解決するソフトウェアを構築していると仮定すると、次のように操作を記述します。

a + b * c

または:

Add(a, Multiply(b, c))

どちらも同等ですが、最初のものは数学の領域でより表現力があり、ありふれたものに見えます。 したがって、演算子のオーバーロードは、ターゲットドメインに近い表記法を使用してプログラミングできるようにするための単なる構文糖衣であると言えます。

5.3. Scalaがオペレーターのオーバーロードをどのようにサポートするか

実際の技術用語では、演算子のオーバーロードは、メソッド名とオブジェクトでメソッドを呼び出す方法の柔軟性を備えたScalaの寛容さによって実際に可能になります

すなわち; Scalaでは、メソッドの命名にいくつかの演算子名を使用できます。 +およびは、Javaとは異なり、Scalaで作成するすべてのクラスの有効なメソッド名です。 複素数をデモとして処理するための抽象化を作成しましょう。

case class Complex(re: Double, im: Double) {
    def + (op: Complex): Complex = Complex(re + op.re, imaginary + op.im)
    def - (op: Complex): Complex = Complex(re - op.re, imaginary - op.im)
    override def toString: String = s"$re + ${im}i"
}

演算子をメソッド名として使用できることに注意してください。これは、コンテキストを考えると、実際には意図をより明確に伝えます。 次に、メソッド呼び出し構文にも柔軟性があります。

it should "add and subtract complex numbers successfully" in {
    val cmpl1 = Complex(8.0, 3.0)
    val cmpl2 = Complex(6.0, 2.0)

    val cmplSum = cmpl1.+(cmpl2)
    val cmplDiff = cmpl1 - cmpl2

    assertResult(expected = "14.0 + 5.0i")(actual = cmplSum.toString)
    assertResult(expected = "2.0 + 1.0i")(actual = cmplDiff.toString)
}

加算呼び出しのように後置表記を使用できますが、Scalaでは、(-)を中置演算子として呼び出す減算呼び出しのように、オブジェクト、メソッド名、および引数を分離することもできます。 どちらもScalaで有効な構文です。

6. 結論

この記事では、Scalaの3種類のポリモーフィズムについて説明しました。 完全なソースコードは、GitHubから入手できます。