web-dev-qa-db-ja.com

Scalaの型クラスは何に役立ちますか?

このブログ投稿 から理解できるように、== "[type classes" in Scalaは、特性と暗黙的アダプターで実装された単なる「パターン」です。

ブログにあるように、特性AとアダプターB -> Aその後、このアダプターを明示的に呼び出すことなく、A型の引数を必要とするB型の引数を必要とする関数を呼び出すことができます。

ナイスですが、特に便利ではありません。この機能が何に役立つのかを示すユースケース/例を教えてください。

66
Michael

要求された1つのユースケース...

整数、浮動小数点数、行列、文字列、波形などの可能性のあるもののリストがあると想像してください。このリストが与えられたら、コンテンツを追加します。

これを行う1つの方法は、一緒に追加できるすべての単一の型によって継承される必要があるいくつかのAddable特性、または3番目からのオブジェクトを処理する場合のAddableへの暗黙的な変換インターフェイスを後付けできないパーティライブラリ。

このアプローチは、オブジェクトのリストに実行できる他の操作を追加したい場合にも、すぐに圧倒されます。また、代替手段が必要な場合にもうまく機能しません(たとえば、2つの波形を追加してそれらを連結しますか、それともオーバーレイしますか?)解決策はアドホックポリモーフィズムであり、can既存のタイプに後付けされます。

元の問題については、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メソッドはTraversableLikeではなくListで定義されており、型クラスはNumericです(また、_zeroおよびappend

85
Kevin Wright

そこで最初のコメントを読み直してください。

型クラスとインターフェイスの重要な違いは、クラス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)

  ...
}
32
Alexey Romanov

タイプクラスは、タイプセーフメタデータをクラスに追加する機能と考えています。

したがって、まず問題のドメインをモデル化するクラスを定義し、次に追加するメタデータを考えます。 Equals、Hashable、Viewableなどのようなものです。これにより、クラスがよりリーンであるため、問題ドメインとクラスを使用するメカニズムが分離され、サブクラス化が可能になります。

それ以外は、クラスが定義されている場所だけでなく、スコープのどこにでも型クラスを追加でき、実装を変更できます。たとえば、Point#hashCodeを使用してPointクラスのハッシュコードを計算する場合、特定のポイントセットの値の適切な分布を作成できない特定の実装に制限されます。ただし、Hashable [Point]を使用する場合は、独自の実装を提供できます。

[例で更新]例として、先週のユースケースを紹介します。当社の製品には、値としてコンテナを含むマップのいくつかのケースがあります。例:_Map[Int, List[String]]_または_Map[String, Set[Int]]_。これらのコレクションに追加すると、冗長になる場合があります。

_map += key -> (value :: map.getOrElse(key, List()))
_

だから私はこれをラップする関数を持ちたいと思った

_map +++= key -> value
_

主な問題は、コレクションにすべての要素を追加するための同じメソッドがないことです。 「+」が付いているものと、「:+」が付いているものがあります。また、リストに要素を追加する効率を維持したかったため、新しいコレクションを作成するfold/mapを使用したくありませんでした。

解決策は、型クラスを使用することです。

_  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
    }
  }
_

ここで、コレクションCに要素Cを追加できる型クラスAddableを定義しました。デフォルトの実装は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。コレクションタイプごとにメソッドを記述する。例:addElementToSubListaddElementToSetなど。これにより、実装に多くの定型文が作成され、名前空間が汚染されます2.リフレクションを使用して、サブコレクションがリスト/セットであるかどうかを判断します。最初はマップが空であるため、これはトリッキーです(もちろんscalaはマニフェストでも役立ちます)3.ユーザーに加算器の提供を要求することにより、貧乏人のタイプクラスを作成します。 addToMap(map, key, value, adder)のように、見苦しいです

9
IttayD

このブログ投稿が役立つと思うもう1つの方法は、タイプクラスの説明です。 Monads Are Not Metaphors

Typeclassの記事を検索してください。最初に一致する必要があります。この記事では、著者はMonadタイプクラスの例を提供します。

6
Bradford

フォーラムのスレッド「 特性よりも型クラスを改善するものは何ですか? 」は、興味深い点をいくつか示しています。

  • タイプクラスは、equalityorderingなど、サブタイピングの存在下では表現が非常に難しい概念を非常に簡単に表現できます。
    演習:小さなクラス/特性階層を作成し、各クラス/特性に_.equals_を実装して、階層の任意のインスタンスに対する操作が適切に再帰的、対称的、推移的になるようにします。
  • タイプクラスを使用すると、「コントロール」の外部のタイプが何らかの動作に適合するという証拠を提供できます。
    他の誰かの型が型クラスのメンバーになることができます。
  • 「このメソッドはメソッドレシーバーと同じ型の値を取得/返す」というサブタイプを表現することはできませんが、この(非常に便利な)制約は型クラスを使用すると簡単です。これは、f-bounded types problem(F-bounded typeが独自のサブタイプでパラメータ化されている)です。
  • 特性に定義されたすべての操作にはインスタンスが必要です;常にthis引数があります。したがって、たとえばFooのインスタンスなしで呼び出すことができるように、_trait Foo_でfromString(s:String): Fooメソッドを定義することはできません。
    In Scala=これは、人々が必死にコンパニオンオブジェクトを抽象化しようとしていることを示しています。
    しかし、 このモノイドの例 のゼロ要素で示されているように、タイプクラスを使用すると簡単です。
  • タイプクラスは帰納的に定義することができます;たとえば、_JsonCodec[Woozle]_がある場合は、_JsonCodec[List[Woozle]]_を無料で取得できます。
    上記の例は、「一緒に追加できるもの」についてこれを示しています。
5
VonC

型クラスを見る1つの方法は、 retroactive extension または retroactive polymorphism を有効にすることです。 Casual MiraclesDaniel Westheide による素晴らしい投稿がいくつかあります。これらはScalaでタイプクラスを使用してこれを達成する例を示しています。

私のブログへの投稿 scala of retroactive supertyping )のさまざまな方法を探索します。タイプクラスの例。

4
Jonathan Warden

In scala= typeクラス

  • アドホックポリモーフィズムを有効にします
  • 静的に型付けされた(つまり、タイプセーフ)
  • ハスケルから借りた
  • 式の問題を解決します

動作は、コンパイル時-事後-既存のコードを変更/再コンパイルせずに拡張できます

Scala Implicits

メソッドの最後のパラメーターリストは暗黙的にマークできます。

  • 暗黙のパラメーターはコンパイラーによって入力されます

  • 実際には、コンパイラの証拠が必要です

  • …スコープ内の型クラスの存在など

  • 必要に応じて、パラメーターを明示的に指定することもできます

型クラス実装を使用した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
}
1
Nihat Hosgur

アドホックポリモーフィズム以外のユースケースは知りません ここ 可能な限り最良の方法です。

1
lisak

Type-conversion には、implicitstypeclassesの両方が使用されます。両方の主な使用例は、アドホックポリモーフィズム(i.e)を変更することはできませんが、継承のようなポリモーフィズムを期待するクラスに提供することです。暗黙の場合、暗黙のdefまたは暗黙のクラス(これはラッパークラスですが、クライアントからは見えません)の両方を使用できます。タイプクラスは、既存の継承チェーンに機能を追加できるため、より強力です(例:scalaのsort関数のOrdering [T])。詳細については、以下を参照してください https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/

1

これ は重要な違いです(関数型プログラミングに必要):

enter image description here

inc:Num a=> a -> a

受け取ったaは返されるものと同じです。これはサブタイピングではできません

0
jhegedus

型クラスを、依存関係注入の軽量なScala慣用的な形式として使用するのが好きです。これは、循環依存関係で動作しますが、コードの複雑さをあまり追加しません。最近、 ScalaプロジェクトをCakeパターンを使用してDI のクラスを入力することから書き直し、コードサイズを59%削減しました。

0
Glenn