このブログ投稿からわかるように 、Scala の「型クラス」は、トレイトと暗黙のアダプターで実装された単なる「パターン」です。
ブログにあるように、traitA
とアダプターがあれば、このアダプターを明示的に呼び出さずに type の引数を使用して、B -> A
type の引数を必要とする関数を呼び出すことができます。A
B
いいと思いましたが、特に役に立ちませんでした。この機能が何に役立つかを示すユースケース/例を挙げていただけますか?
リクエストに応じて 1 つの使用例...
整数、浮動小数点数、行列、文字列、波形などのリストがあるとします。このリストが与えられたら、内容を追加します。
これを行う1つの方法はAddable
、一緒に追加できるすべての単一の型に継承する必要があるいくつかの特性を持つかAddable
、インターフェイスを改造できないサードパーティライブラリのオブジェクトを処理する場合に暗黙的に変換することです。
オブジェクトのリストに実行できる他の操作を追加したい場合、このアプローチはすぐに圧倒されます。また、代替が必要な場合にもうまく機能しません (たとえば、2 つの波形を追加するとそれらが連結されたり、オーバーレイされたりしますか?) 解決策はアドホック ポリモーフィズムです。これにより、動作を選択して既存の型に後付けすることができます。
Addable
元の問題については、型クラスを実装できます。
trait Addable[T] {
def zero: T
def append(a: T, b: T): T
}
//yup, it's our friend the monoid, with a different name!
次に、追加可能にしたい各タイプに対応する、これの暗黙的なサブクラス化されたインスタンスを作成できます。
implicit object IntIsAddable extends Addable[Int] {
def zero = 0
def append(a: Int, b: Int) = a + b
}
implicit object StringIsAddable extends Addable[String] {
def zero = ""
def append(a: String, b: String) = a + b
}
//etc...
リストを合計する方法は、書くのが簡単になります...
def sum[T](xs: List[T])(implicit addable: Addable[T]) =
xs.FoldLeft(addable.zero)(addable.append)
//or the same thing, using context bounds:
def sum[T : Addable](xs: List[T]) = {
val addable = implicitly[Addable[T]]
xs.FoldLeft(addable.zero)(addable.append)
}
このアプローチの利点は、インポートを介してスコープ内で必要な暗黙的なものを制御するか、そうでなければ暗黙的な引数を明示的に提供することにより、型クラスの代替定義を提供できることです。したがって、波形を加算するさまざまな方法を提供したり、整数加算にモジュロ演算を指定したりすることが可能になります。また、サードパーティのライブラリから型クラスに型を追加するのも簡単です。
ちなみに、これはまさに 2.8 コレクション API で採用されたアプローチです。sum
メソッドは onTraversableLike
ではなく on で定義されていますがList
、型クラスは( andNumeric
だけでなくいくつかの操作も含まれています)zero
append
そこの最初のコメントを読み直してください:
型クラスとインターフェイスの重要な違いは、クラス A がインターフェイスの「メンバー」になるためには、それ自体の定義のサイトで宣言する必要があることです。対照的に、必要な定義を提供できれば、任意の型をいつでも型クラスに追加できるため、型クラスのメンバーはいつでも現在のスコープに依存します。したがって、 A の作成者が、それを所属させたい型クラスを予期していたかどうかは気にしません。そうでない場合は、それが実際に属していることを示す独自の定義を作成し、それに応じて使用することができます。したがって、これはアダプターよりも優れたソリューションを提供するだけでなく、ある意味では、アダプターが対処することを意図していた問題全体を未然に防ぎます。
これが型クラスの最も重要な利点だと思います。
また、操作がディスパッチする型の引数を持たない場合や、複数の引数を持つ場合も適切に処理します。たとえば、この型クラスを考えてみましょう:
case class Default[T](val default: T)
object Default {
implicit def IntDefault: Default[Int] = Default(0)
implicit def OptionDefault[T]: Default[Option[T]] = Default(None)
...
}
タイプ クラスは、タイプ セーフなメタデータをクラスに追加する機能と考えています。
したがって、最初にクラスを定義して問題のドメインをモデル化し、次にそれに追加するメタデータを考えます。Equals、Hashable、Viewable など。これにより、問題のドメインとクラスを使用するメカニズムが分離され、クラスがよりスリムになるため、サブクラス化が開かれます。
それ以外は、クラスが定義されている場所だけでなく、スコープ内のどこにでも型クラスを追加でき、実装を変更できます。たとえば、Point#hashCode を使用して Point クラスのハッシュ コードを計算すると、その特定の実装に制限され、特定の Point のセットに対して適切な値の分布が作成されない可能性があります。しかし、Hashable[Point] を使用する場合は、独自の実装を提供できます。
[例で更新] 例として、先週のユースケースを次に示します。私たちの製品には、コンテナを値として含むマップのケースがいくつかあります。たとえば、Map[Int, List[String]]
またはMap[String, Set[Int]]
. これらのコレクションへの追加は冗長になる可能性があります。
map += key -> (value :: map.getOrElse(key, List()))
だから私はこれをラップする関数が欲しかったので、私は書くことができました
map +++= key -> value
主な問題は、要素を追加するためのメソッドがすべてのコレクションにあるとは限らないことです。「+」を持つものもあれば、「:+」を持つものもあります。また、リストに要素を追加する効率を維持したかったので、新しいコレクションを作成するフォールド/マップを使用したくありませんでした。
解決策は、型クラスを使用することです。
trait Addable[C, CC] {
def add(c: C, cc: CC) : CC
def empty: CC
}
object Addable {
implicit def listAddable[A] = new Addable[A, List[A]] {
def empty = Nil
def add(c: A, cc: List[A]) = c :: cc
}
implicit def addableAddable[A, Add](implicit cbf: CanBuildFrom[Add, A, Add]) = new Addable[A, Add] {
def empty = cbf().result
def add(c: A, cc: Add) = (cbf(cc) += c).result
}
}
Addable
ここでは、要素 C をコレクション CC に追加できる型クラスを定義しました。::
私は2つのデフォルトの実装を持っています.ビルダーフレームワークを使用するリストと他のコレクションの場合です。
次に、この型クラスを使用すると、次のようになります。
class RichCollectionMap[A, C, B[_], M[X, Y] <: collection.Map[X, Y]](map: M[A, B[C]])(implicit adder: Addable[C, B[C]]) {
def updateSeq[That](a: A, c: C)(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = {
val pair = (a -> adder.add(c, map.getOrElse(a, adder.empty) ))
(map + pair).asInstanceOf[That]
}
def +++[That](t: (A, C))(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = updateSeq(t._1, t._2)(cbf)
}
implicit def toRichCollectionMap[A, C, B[_], M[X, Y] <: col
特別なビットを使用adder.add
して、要素を追加し、adder.empty
新しいキーの新しいコレクションを作成します。
比較すると、型クラスがなければ、次の 3 つの選択肢がありました。 1. コレクション型ごとにメソッドを作成する。などaddElementToSubList
などaddElementToSet
。これにより、実装に多くのボイラープレートが作成され、名前空間が汚染されます。 2. リフレクションを使用して、サブコレクションがリスト/セットであるかどうかを判断します。そもそもマップが空であるため、これは注意が必要です (もちろん、ここではマニフェストでも scala が役立ちます)。のようなものですaddToMap(map, key, value, adder)
。これは単純に醜いです
このブログ投稿が役立つと思うさらに別の方法は、型クラスについて説明している箇所です: Monads Are Not Metaphors
typeclass の記事を検索します。初戦のはず。この記事では、著者は Monad 型クラスの例を提供します。
フォーラムのスレッド「What make type classes better than traits?」 は、いくつかの興味深い点を述べています。
- 型クラスは、等価性や順序付けなど、サブタイプの存在下では表現が非常に難しい概念を非常に簡単に表現できます。
演習: 小さなクラス/特性階層を作成し.equals
、階層からの任意のインスタンスに対する操作が適切に再帰的、対称的、および推移的であるように、各クラス/特性に実装してみてください。- 型クラスを使用すると、「コントロール」の外側にある型が何らかの動作に準拠しているという証拠を提供できます。
他の誰かの型があなたの型クラスのメンバーになることができます。- 「このメソッドはメソッド レシーバーと同じ型の値を受け取る/返す」という表現はサブタイプでは表現できませんが、この (非常に便利な) 制約は型クラスを使用して簡単に表現できます。これは、f-bounded 型の問題です(F-bounded 型がそれ自体のサブタイプでパラメーター化される場合)。
- トレイトで定義されたすべての操作にはインスタンスが必要です。常に
this
議論があります。したがって、たとえば、のインスタンスなしで呼び出せるようなfromString(s:String): Foo
方法で onのメソッドを定義することはできません。 Scala では、コンパニオン オブジェクトを必死に抽象化しようとする人々として現れます。しかし、この monoid example のゼロ要素で示されているように、型クラスを使用すると簡単です。trait Foo
Foo
- 型クラスは帰納的に定義できます。たとえば、 をお持ちの場合は、を無料で
JsonCodec[Woozle]
入手できます。 上記の例は、「一緒に追加できるもの」についてこれを示しています。JsonCodec[List[Woozle]]
型クラスを見る 1 つの方法は、遡及的拡張または遡及的ポリモーフィズムを有効にすることです。これを実現するために Scala で型クラスを使用する例を示す、 Casual MiraclesとDaniel Westheideによる素晴らしい投稿がいくつかあります。
これは、型クラスの例を含む、一種の遡及的拡張であるscala の遡及的スーパータイピング のさまざまな方法を探る私のブログの投稿です。
ここで可能な限り最善の方法で説明されているアドホック ポリモヒズム以外のユース ケースは知りません。
scala 型クラス内
コンパイル時、事後、既存のコードを変更/再コンパイルすることなく、動作を拡張できます。
Scala の暗黙的要素
メソッドの最後のパラメーター リストを暗黙的にマークできる
暗黙的なパラメーターはコンパイラーによって入力されます
実際には、コンパイラの証拠が必要です
…スコープ内の型クラスの存在など
必要に応じて、パラメータを明示的に指定することもできます
以下の型クラス実装を使用した String クラスの拡張の例では、string が final であっても、新しいメソッドでクラスを拡張します :)
/**
* Created by nihat.hosgur on 2/19/17.
*/
case class PrintTwiceString(val original: String) {
def printTwice = original + original
}
object TypeClassString extends App {
implicit def stringToString(s: String) = PrintTwiceString(s)
val name: String = "Nihat"
name.printTwice
}
私は型クラスを軽量な Scala の慣用的な依存性注入の形式として使用するのが好きです。これは、循環依存性でも機能し、コードの複雑さをあまり追加しません。私は最近、Scala プロジェクトを Cake パターンの使用から DI の型クラスへと書き直し、コード サイズを 59% 削減しました。