「ポリモーフィズム」という言葉は、ポリモーフィズムが柱の1つであるオブジェクト指向プログラミングをすぐに思い出します( 初心者向けのポリモーフィズム )。 (そして、明らかに、他の柱よりも重要です。)同様の効果を別の方法で達成することが可能であることが判明しました。 たとえば、型クラスを使用すると、祖先が変更できない既存の型に新しい機能を割り当てることができます。または、互換性のないクラスを持つデータ型を使用して、多重継承の問題を「解決」できます。
Habréには、型クラスの概念を提供する出版物がすでにいくつかあります。
- @IvanGolovach開発→ 「ScalaのFP:ファンクターとは?」 -2015年。
ここでは、ファンクターを検討する際に型クラスを検討します。 説明では、型クラスの例をいくつか示します。 - ミハイル・ポタニン@potan開発→ 「C ++の型クラス」 -2013 。
この出版物は、C ++で型クラスを実装しています。 どうやら、読者はすでに型クラスにある程度精通していると想定されているため、型クラス自体についてはあまり言及されていません。 - @VoidEx開発→ 「型のクラス、モナド」 -2009 。
Haskell型クラスについて説明します(C ++の実装例を使用)。 - @IvanP開発→ 「Haskellの多態性とクラス」 -2013 。
パラメトリックおよびアドホックポリモーフィズム、タイプクラス、およびいくつかの標準クラスについて説明します。 すべての説明はHaskell言語用です。 - 更新: @vuspenskiy開発-> Scalaの型クラス -2012 。
暗黙的に型クラスを使用する方法は、Comparator[T]
例を使用して簡単に説明します。
この投稿では、プログラマの日常的なツールの1つとして型クラスのビューを反映したいと思います。 さらに、Haskellでそれらなしでは何もできない場合、Salaでは次のことができます。 すごい 彼らの存在を知らずに生きることは容認できます。 ただし、詳しく調べると、このようなツールは再利用可能なプログラムを作成するときに非常に便利であることがわかります。 さらに、このツールに基づいたユニバーサルライブラリが多数あり、それらを使用するには、型クラスを理解する必要もあります。
不変のデータ型
関数型プログラミングでは、不変のデータ型が広く普及しています。 データを勝手に変更することはできないため、データを非表示にする理由はなく、データを非表示にする代わりに、データがパブリックであるオープン型が使用されるようになりました。 (したがって、OOPの3つの柱-ポリモーフィズム、継承、カプセル化-のうちの1つは、やや側に押しやられています。)
自由に利用可能なデータにより、外部処理アルゴリズムを使用できます。 処理アルゴリズムが異なるタイプの複数のオブジェクトを使用する場合、オブジェクト自体にアルゴリズムをバインドし、オブジェクト間の人為的な障壁を克服する必要はありません。
データ構造のセットの大部分は、2つのメカニズム( 代数データ型 )のみを使用してモデル化できることがわかります 。 まず、レコードまたはタプルの作成(「type-work」)。 第二に、1つの親タイプの代替実装の作成-列挙、インターフェースの継承、封印された特性-「タイプサム」。
例:
// - : sealed trait Form object Form { // - String X String case class Form1(number: String, title: String) extends Form // - UUID X String X Int case class Form2(id: UUID, documentation: String, count: Int) extends Form // == Unit ( ) case object BadForm extends Form }
ご覧のとおり、そのような代数データ型はデータの隠蔽や組み込みメソッドの存在を意味しません。 すべてのデータは直接利用でき、次のような外部アルゴリズムを使用してこれらのタイプを処理できます。
- ユーザーのHTMLビューを作成します
- ビジネスルールに従って検証する
- シリアライズ/デシリアライズ
- いくつかの指標を計算する
- ...
これらのタイプの処理はすべて個別に実装され、お互いについて何も知る必要はありません。 このようなアルゴリズムでは、パターンマッチングは、実際のデータにアクセスしてさまざまなサブタイプを処理する主な方法です。 パターンマッチングの助けを借りて、オブジェクトの特定のサブタイプを同時にチェックし、関心のあるフィールドの値を抽出します。
特定のタイプの外側にアルゴリズムを配置すると、次の利点があります。
- アルゴリズムのロジックはサブタイプごとに塗りつぶされていませんが、個別のモジュールにローカライズされています。
- 1つの処理メソッドのロジックは、各データクラス内の他の処理メソッドと混在していません。 さまざまな開発者によるさまざまなアルゴリズムのサポートの簡素化。
- DBMSモデルへの依存関係を、データモデルが宣言されているモジュールに追加する必要はありません。
- 既存のデータ型に新しい処理方法を簡単に追加できます。 これらは、独立したモジュールに「直交して」追加されます。
型クラス
データ型以外のアルゴリズムを実装したとします。 このアルゴリズムで型が直接使用されている場合、他の同様のデータにそれを再利用することはできません。 これは、そのようなアルゴリズムの方が簡単に書くことができるため、一方では悪くありませんが、一方で、その一般性は制限されています。 これは、一般的な場合、アルゴリズムの使用頻度が低くなり、明らかに、同じ経済的コストでテストが悪化するか、サポートコストが高くなることを意味します。
したがって、アルゴリズムを他のデータ型(既存および有望)に一般化するメカニズムが必要です。 これにより、多くの場合に同じアルゴリズムを使用でき、開発とテストのコストを回収できます。
OOPは、「類似」データの共通インターフェースを選択し、この共通インターフェースの観点からアルゴリズムを実装することを提案しています。 このインターフェイスを継承する具体的なクラスでは、これらの一般的なメソッドを実装するだけで十分です。 したがって、ある程度ポリモーフィックアルゴリズムを取得します。 ただし、「類似の」データインターフェイスの一部であるこれらの操作は、データ自体に実装する必要があります。
型クラスは、プログラムでさまざまな役割を果たすコードを分離する次のステップを表します。 データに対して実行する操作は、データの祖先ではない別のクラスに移動されます。 このデータ型のこのクラスのインスタンスは、データとともにアルゴリズムに渡されます。
例:
興味のあるアルゴリズムにデータ比較を順番に使用させます。 このような比較は、関数で表すことができます
def compare[T](a: T, b: T): Int
この関数をOrdering
型のクラスに配置します。
trait Ordering[T] { def compare(a: T, b: T): Int }
次に、汎用アルゴリズムをソートします。 使用しているデータのタイプを受け入れる必要があります。
def sort[T](list: List[T]): List[T]
要素はアルゴリズム内で比較されるため、型T
Ordering
クラスのインスタンスT
このアルゴリズムに渡す必要があります。
def sort[T: Ordering](list: List[T]): List[T]
または、同じです:
def sort[T](list: List[T])(implicit o: Ordering[T]): List[T]
アルゴリズムは、 compare
演算を呼び出す必要がある場合、 implicitly[Ordering[T]].compare(a,b)
を使用して、型のクラスのインスタンスを取得する必要があります。
データ型の型クラスのインスタンスのみを提供できます。
implicit object FormOrdering extends Ordering[Form] { def compare(a: Form, b: Form): Int = (a,b) match { case (Form1(numberA, titleA), Form1(numberB, titleB)) => numberA - numberB case (BadForm, BadForm) => 0 ... case _ => 0 } }
したがって、特定のアルゴリズムに関連するコードでデータを乱雑にすることなく、共通のアルゴリズムを実現します。
さらなる利便性
型自体でメソッドを直接利用可能にする方法は? たとえば、型クラスメソッドを明示的に呼び出さずに、 a compare b
メソッドを使用してアルゴリズム内のオブジェクトを比較したいとします。
これを行うには、Scalaの通常のpimp-my-libraryメカニズムを使用します。
implicit class OrderingOps[T:Ordering](a:T){ def compare(b:T): Int = implicitly[Ordering[T]].compare(a,b) }
したがって、 Ordering
インスタンスがあるすべてのタイプに対して、新しいcompare
メソッドが表示されます。
そのような要望が毎回発生する場合は、 simulacrumライブラリを使用できます。これにより、マクロを使用して必要なすべてのバインディングを備えた補助メソッドが作成されます。
import simulacrum.typeclass @typeclass trait Ordering[T]{ def compare(a: T, b: T): Int }
例:木を書き換えるための型クラス(方程式の記号解、プログラム最適化)
カスタムデータ構造の型クラスの例を考えてみましょう。 プログラムを最適化するために使用されるメカニズムの1つは、セマンティクスを維持しながらASTを書き換えることです。 この場合、ツリーのすべてのノードが(深さまたは幅で)トラバースされ、各ノードについて、対応するパターンが検索され(パターンマッチング)、パターンマッチングの場合、ノードは対応するルールに従って書き換えられます。
さまざまなタスク(方程式、プログラム)に対して、ASTツリーを構成するタイプは異なり、比較/最適化パターンも異なります。 ただし、回避策は同じです。
このアルゴリズムは、型クラスを使用した抽象化の候補です。 任意のタイプのツリーに、ツリートラバーサルアルゴリズムで使用されるいくつかの操作を追加する必要があります。
import simulacrum.typeclass @typeclass trait RewritableTree[T] { def children(node: T): List[T] def replaceChildren(node: T, newChildren: List[T]): T }
object RewritableTree { def rewrite[T: RewritableTree](f: PartialFunction[T, T]): T => T = t => { rewrite0(f)(t).getOrElse(t) } private def rewrite0[T: RewritableTree](f: PartialFunction[T, T])(t: T): Option[T] = { import RewritableTree.ops._ // "", simulacrum' val rt = implicitly[RewritableTree[T]] - "" val children = t.children // rt.children(t) var changed = false // , , , val updatedChildren = children.map{child => val res = rewrite0(f)(child) changed = changed || res.isDefined res.getOrElse(child) } // //def rewriteList(lst: List[T], result: mutable.ListBuffer[T], changed: Boolean): (List[T], Boolean) = lst match { // case Nil => (result.toList, changed) // case head :: tail => // val res = rewrite0(f)(head) // rewriteList(tail, result.append(res.getOrElse(head)), changed || res.isDefined) //} //val (updatedChildren, changed) = rewriteList(t.children, mutable.ListBuffer(), false) val updatedTree = if(changed) t.replaceChildren(updatedChildren) else t var changed2 = true val updatedTree2 = f.applyOrElse(t1, (_:T) =>{changed2 = false; updatedTree}) if(changed || changed2) Some(updatedTree2) else None } }
同じ型クラスを使用して、ツリーがトラバースされるときに値を収集collect
メソッドを実装できます。
派生型の型クラスの帰納的定義
型Ordering[T]
の型Ordering[T]
クラスを既に実装しているとしますT
そして、 Option[T]
リストをソートしたいと思います。 既に実装されている型クラスを利用して、欠落している機能を単に補完することは可能ですか?
これは、既存の型クラスから実装を構築することにより、その場で型クラスの実装を提供する場合に実行できます。
implicit def optionOrdering[T:Ordering]: Ordering[Option[T]] = new Ordering[Option[T]] { def compare(a: Option[T], b: Option[T]): Int = (a, b) match { case (Some(c), Some(d)) => implicitly[Ordering[T]].compare(c,d) case (None, None) => 0 case (_, None) => 1 case (None, _) => -1 } }
そのような実装は、 Ordering[T]
型のクラスのインスタンスが存在する型のソートアルゴリズムに自動的に挿入されます。
同様に、 List[T]
、 Tuple2[A,B]
、...などのジェネリック型に対して型クラスを構築できます。
標準型クラス(猫)
型クラス内で宣言される操作は任意です。 このアルゴリズムでは、抽象境界線を任意に描画できます。たとえば、アルゴリズム全体を型クラスに入れたり、逆にデータアクセスメソッドを直接型クラスに入れたりできます。 これらの境界オプションは両方とも、再利用に関して最適ではありません。 したがって、最小数の操作を他の型に簡単に実装できる型クラスに入れる価値があり、同時にこれらの操作を通じてアルゴリズムを表現できます。
この観点からアルゴリズムとデータアクセスを検討し始めるとすぐに、一般的に使用されるいくつかの型クラスに到達する可能性があります。
Scala標準ライブラリにはいくつかの型クラスがあります: Ordering[T]
、 Equiv[T]
、 Numeric[T]
、 Integral[T]
、...
typelevel / catsライブラリ(およびscalazライブラリ)で、頻繁に使用される操作を持つ単純型のいくつかの追加クラスが宣言されています( http://typelevel.org/cats/typeclasses.html ):
- セミグループ -単一の
combine
操作。 - モノイドは、空(「ゼロ」)要素-emptyのセミグループです。
たとえば、数値の場合、 combine
操作を数値の合計として定義できます。この場合、ゼロ要素は通常のゼロになります。 加法モノイドを取得します。 乗算をcombine
演算として使用し、1つを単位として使用する場合、乗法モノイドを使用することもできます。 数字のリストは、モノイドとみなすこともできます。 combine
操作はリストの接着であり、null要素は空のリストです。
例:
モノイドを使用して累積を実装できます。 モノイドからempty
等しい初期値を持つ状態を作成します。 さらに、入力データの場合、すでに状態にあるものとcombine
ことができます。 たとえば、操作「sum」でタイプInt
を取得できます。 この場合、着信データの合計が1つの値に累積されます。 または、 List[T]
モノイドを取ります。 この場合、すべてのデータがこのリストで利用可能になります(入力時にリストが存在するか、各番号がリストにラップされる必要があります)。 両方の場合の累積アルゴリズムは同一です-既存のデータと新しいデータに対してcombine
メソッドを呼び出します。 また、アルゴリズムは、動作する特定のタイプに依存しません。
また、あるタイプについてそれがモノイドであることがわかっている場合(つまり、このタイプのモノイドクラスのインスタンスがある場合)、 foldLeft
を使用できます-これらの要素のコレクションの畳み込み(再実装する必要はありません)。
高次タイプ
単純な基本型に加えて、型クラスを使用して、それ自体がパラメーターを持つ型を操作できます。 (したがって、型クラスには、言語の高次型のサポートが必要です。)高次型は、種類-「型型」によって特徴付けられます。
- 単純型の種類は
*
(たとえば、Int
、String
)です。 - 1つの引数をとるタイプ-
* -> *
(たとえば、List[T]
、Option[T]
、Future[T]
); - 2つの引数をとるタイプ-
* -> * -> *
(たとえば、関数Function1[A,B]
)。 (値レベルの関数自体には1つの矢印A => B
が含まれ、型レベルではA => B => (A=>B)
-2つの矢印(3番目の矢印は既に型自体の中にあります)。
catsライブラリには、基本型で動作する型クラスに加えて、型コンストラクタで動作するときに使用される型クラスがあります。 特に、タイプ* -> *
:
ファンクターは、1つの操作
map
を含む型クラスです。 この操作は、たとえばList[Int]
型のオブジェクトを受け取り、指定された関数を各要素に適用します。List
とOption
場合、この操作は、一般的に言えば、データ型自体に既に実装されており、その型のクラスを作成しないことも可能です。 ただし、map
操作を使用してユニバーサルアルゴリズムを実装する場合は、そのようなタイプのクラスが必要です。
- Monadは、操作
flatMap
、bind
または>>=
(およびflatten
、map
、pure
)を含むファンクターです。 この型クラスは、明らかに最も有名です。 その有用性は、flatMap
(bind
)が逐次計算を接着するかなり普遍的な方法であるという事実によるものです。 Scalaの理解度は、flatMap
操作にも基づいています。
例:
- リスト処理。 オブジェクトのコレクションのすべての子を収集します
val allChildren = objects.flatMap(_.children)
- 欠損値の処理:
val street = personOpt.flatMap(_.addressOpt).flatMap(_.streetOpt)
- リクエストの実行の遅延。 データベースからのクエリの結果が
DataTable[T]
で表されるようにしDataTable[T]
。flatMap
を使用して、このクエリの結果からデータを抽出するサブクエリを定義できます。 このようなクエリは、最初のクエリを実行したり、結果のコレクションを処理したりすることなく 、元のクエリに接着できます。 接着されたクエリをSQLでコンパイルし、DBMS側で実行するためにデータベースに送信できます。 このアプローチは、たとえばSlickライブラリに実装されています。
catsライブラリのタイプ* -> * -> *
には、タイプのクラスもあります。
- カテゴリ -操作
compose
+ "null"要素identity
。 タイプ「カテゴリ」のクラスが定義されているタイプは、「矢印」(矢印)と呼ばれます。 矢印は機能に似ています。 特に、通常の関数の場合、compose
操作はandThen
メソッドに対応し、identity
操作はidentity
関数に対応します。
カテゴリの例:
- 通常の機能。
- モデル関数(モデル言語)。
- レンズ(クラスから分離されたオブジェクトのプロパティ)( モノクルライブラリを参照)。
- 機能的反応型プログラミングの有向グラフ(例: SynapseGrid )。
例:
カテゴリの場合、 compose
は重要な機能です。 つまり アルゴリズムを構成に関して表現できる場合、このアルゴリズムを任意のカテゴリに適用できます。
独自のDSLを使用して、一連のデータ変換をモデル化します。 各変換は、あるタイプのTransform[A,B]
で表現できると仮定します。
A
とB
は、必ずしもデータモデルの型ではありません。 これらは、いわゆるファントムタイプにすることができます 。 ファントムタイプを使用すると、コンパイラによってチェックされる変換の許可された組み合わせに対して独自のルールを定義できます。 つまり 互換性のない変換にはcompose
メソッドを使用できません。
ユーザーがこのDSLを使用して自分のタスクを説明した後、条件付き変換を、実際の型の通常の関数で既に表されているデータを使用して実際のアクションに変換できます。 1つのカテゴリ(モデル関数)を別のカテゴリ(実際の関数)に変換することを「自然変換」と呼びます。
型クラスの法律
catsライブラリに実装されている型クラスは、カテゴリ理論の概念をモデル化します。 したがって、これらの型クラスのメソッドは、特定のプロパティ(理論で説明されている)を満たす必要があります。 たとえば、モノイドの場合:
-
a combine empty = a = empty combine a
空の要素の定義 -
(a combine b) combine c = a combine (b combine c)
-combine
操作のcombine
型理論のカテゴリー理論に必要なすべてのプロパティは、「法則」の形式で実装されます-ScalaCheckライブラリの「プロパティ」のセット。 また、型クラスのどのインスタンスでも、このインスタンスがこの型クラスの要件を満たしているかどうかを確認できます。 多くのアルゴリズムはこれらのプロパティに依存しているため、データの型クラスを実装するときは、ユニットテストでこれらの法則を確認する必要があります。
型クラスの実装が既存の法則を満たしていることを確認したら、型クラスのこれらのプロパティに基づくライブラリのアルゴリズムを使用するプログラムの正確性をほぼ確信できます。
タイプクラスの利点
子孫に実装する必要がある従来のインターフェイスと比較して、型クラスには次の利点があります。
- アクセスできない型のクラスを実装できます。
- このタイプのゼロ個のインスタンスで動作するオペレーションを宣言できます。 特に、
empty: T
メソッド、またはparse: String => T
メソッドparse: String => T
; - 基本型のインスタンスがある場合、複合型のインスタンスを帰納的に定義できます。 たとえば、
Option[T]
またはA \/ B
これらの利点は、どのプログラムでも独立して使用できます。 コードの構造を見てみるだけで十分です。
catsライブラリー(およびscalazライブラリー)には、多くのアルゴリズムとライブラリーで使用される(カテゴリ理論からの)よく組織されテストされた一連の型クラスがあります。 , , .