Scalaの型クラス
1. 序章
このチュートリアルでは、Scalaの型クラスの概念について説明します。 型クラスは、関数型プログラミングで頻繁に使用される強力な概念です。 それらは、アドホック多相を実現するためにHaskellで最初に導入されました。 それらはScalaでネイティブにサポートされていませんが、traitsやimplicitクラスなどの組み込み機能を使用してそれらを実現できます。
2. 型クラスとは何ですか?
型クラスは、特性によって通常定義されるコントラクトを満たす型のグループです。 これらを使用すると、コードに触れることなく、関数をよりアドホック多相にすることができます。 この柔軟性は、型クラスパターンの最大の利点です。
より正式に言えば:
型クラスは、アドホック多相性をサポートする型システム構造です。 これは、パラメトリックポリモーフィック型の型変数に制約を追加することによって実現されます。 このような制約には通常、型クラスが含まれます
T
および型変数a
、およびそれを意味しますa
メンバーがに関連付けられたオーバーロードされた操作をサポートするタイプにのみインスタンス化できますT
この定義を解凍するには、使用されているさまざまな用語を識別し、それらを個別に定義する必要があります。
- Type System –これは、 type と呼ばれるプロパティを変数、式、関数などのさまざまなプログラミング構造に割り当てるためのルールを定義するコンパイラの論理システムです。 Int 、 String 、および Long は、ここで参照しているタイプの例です。
- アドホックおよびパラメトリックポリモーフィズム–これらについてはScalaのポリモーフィズムに関する記事で取り上げました。
次のセクションの例を使用して、型変数とそれらに適用される制約をよりよく理解します。
3. 問題の例
非常に恣意的な例を作成して、型クラスをどのように定義して使用するかを見てみましょう。 学校の情報システムにいくつかのオブジェクトを表示するための印刷メカニズムを構築するとします。
最初は、 StudentId しかありません。これは、型の安全性のために整数ベースの学生IDをラップします。 しかし、時間の経過とともに、元の実装に触れることなく、 StaffId のような他のいくつかのデータラッパー、さらにはScoreのようなビジネスドメインオブジェクトを追加する予定です。
型クラスパターンの柔軟性は、まさにここで必要なものです。 それでは、これらのオブジェクトを定義しましょう。
case class StudentId(id: Int)
case class StaffId(id: Int)
case class Score(s: Double)
問題を定義したので、先に進んで、それを解決するための型クラスを作成しましょう。 前のセクションで引用した正式な定義を参照して、実装に簡単にマッピングできるようにします。
4. 型クラスの定義T
このセクションでは、ソリューションのコントラクトまたは制約を提供する型クラスを定義します。
trait Printer[A] {
def getString(a: A): String
}
型クラスと呼んでいるのは特性だけであることに少し混乱するかもしれませんが、正式な定義はそれ以上のものがあることを示唆しているようです。 明確にするために、型クラスを、型クラスコンストラクターパターンと呼ばれるもののコンテキストで適用される特性と考えてみましょう。 特性自体または他の方法で使用された特性は、型クラスとしての資格がありません。
正式な定義の2番目の文で前に見たように、これは、パラメトリックポリモーフィック型の型変数に制約を追加することによって実現されます。 この場合、上記の型クラスは、無制限の型 A で定義したため、パラメトリックポリモーフィック型です。つまり、 A を拡張するときに、任意の型に置き換えることができます。
型変数を少し定義します。
5. プリンターの定義
型クラスを使用する理由は、プログラムをアドホック多相にするためです。そこで、適用する関数を定義しましょう。
def show[A](a: A)(implicit printer: Printer[A]): String = printer.getString(a)
データオブジェクトまたはラッパーを人間にわかりやすい方法で表示する必要がある場合は常に、上記の関数を呼び出します。 ただし、型変数(正式な定義から a )を定義していないため、まだ何も出力する権限がありません。
また、メソッドには2つのパラメーターリストがあることに注意してください。 最初のリストには、抽象的なものではありますが、通常の機能パラメーターが含まれています。 2番目のパラメーター・リストには、printerパラメーターの前にimplicitキーワードが付いています。 これは、暗黙のクラスに関する記事を読んだ後で意味があります。
6. 型変数の定義a
繰り返しますが、型クラスの正式な定義を参照すると、最後の文は次のようになります。… aは、メンバーがTに関連付けられたオーバーロード操作をサポートする型にのみインスタンス化できます。
この場合、これは単に、型変数が型クラス Printer をサブタイプ化するか、getStringメソッドをオーバーロードできるように直接インスタンス化する必要があることを意味します。 暗黙的に、型変数も暗黙的である必要があります。これは、showメソッドがそれを期待する方法だからです。
implicit val studentPrinter: Printer[StudentId] = new Printer[StudentId] {
def getString(a: StudentId): String = s"StudentId: ${a.id}"
}
この段階で、StudentIdタイプに対してshowを呼び出すことができます。
it should "print StudentId types" in {
val studentId = StudentId(25)
assertResult(expected = "StudentId: 25")(actual = show(studentId))
}
7. 型クラスの拡張
これまでのところ、動作する型クラスの実装があります。 このセクションでは、実際の型クラスの柔軟性について説明します。 型クラスのパターンにより、アドホック多相性を実現できることは前述しました。 これは、 show メソッドに触れることなく、処理できるタイプの範囲を増やすことができることを意味します。
新しい型変数を追加することで、より広い範囲を実現します。 この例では、次のテストに合格するように、StaffIdおよびScoreタイプを印刷できるようにする必要があります。
it should "custom print different types" in {
val studentId = StudentId(25)
val staffId = StaffId(12)
val score = Score(94.2)
assertResult(expected = "StudentId: 25")(actual = show(studentId))
assertResult(expected = "StaffId: 12")(actual = show(staffId))
assertResult(expected = "Score: 94.2%")(actual = show(score))
}
次に、必要な型変数を定義しましょう。 通常、これらをコンパニオンオブジェクトにグループ化します(この理由から、型クラスパターン暗黙オブジェクトと呼ぶ人もいます)。
object Printer {
implicit val studentPrinter: Printer[StudentId] = new Printer[StudentId] {
def getString(a: StudentId): String = s"StudentId: ${a.id}"
}
implicit val staffPrinter: Printer[StaffId] = new Printer[StaffId] {
def getString(a: StaffId): String = s"StaffId: ${a.id}"
}
implicit val scorePrinter: Printer[Score] = new Printer[Score] {
def getString(a: Score): String = s"Score: ${a.s}%"
}
}
それでおしまい。 新しいオブジェクトタイプをサポートする必要があるときはいつでも、Printerコンパニオンオブジェクトにタイプ変数を追加するだけで済みます。
8. 結論
この記事では、Scalaに適用される関数型プログラミングの型クラスパターンについて説明しました。
いつものように、ソースコードはGitHubでから入手できます。