66 Stimmen

Wozu sind Typklassen in Scala nützlich?

Wie ich aus dieser Blogbeitrag "Typklassen" sind in Scala nur ein "Muster", das mit Traits und impliziten Adaptern implementiert wird.

Wie der Blog sagt, wenn ich eine Eigenschaft habe A und einen Adapter B -> A dann kann ich eine Funktion aufrufen, die ein Argument vom Typ A mit einem Argument des Typs B ohne diesen Adapter explizit aufzurufen.

Ich fand es nett, aber nicht besonders nützlich. Könnten Sie einen Anwendungsfall/ein Beispiel nennen, der/das zeigt, wozu diese Funktion nützlich ist?

86voto

Kevin Wright Punkte 49145

Ein Anwendungsfall, wie gewünscht...

Stellen Sie sich vor, Sie haben eine Liste von Dingen, z. B. Ganzzahlen, Fließkommazahlen, Matrizen, Strings, Wellenformen usw. Zu dieser Liste wollen Sie den Inhalt hinzufügen.

Eine Möglichkeit, dies zu tun, wäre es, einige Addable Eigenschaft, die von jedem einzelnen Typ, der addiert werden kann, geerbt werden muss, oder eine implizite Umwandlung in eine Addable wenn Sie mit Objekten aus einer Bibliothek eines Drittanbieters arbeiten, für die Sie keine Schnittstellen nachrüsten können.

Dieser Ansatz wird schnell überwältigend, wenn Sie auch noch andere Operationen hinzufügen wollen, die mit einer Liste von Objekten durchgeführt werden können. Es funktioniert auch nicht gut, wenn man Alternativen braucht (z. B. ob das Hinzufügen von zwei Wellenformen diese verkettet oder überlagert). Die Lösung ist Ad-hoc-Polymorphismus, bei dem man peut Auswahl von Verhaltensweisen, die in bestehende Typen nachgerüstet werden können.

Für das ursprüngliche Problem könnte man also eine Addable Typklasse:

trait Addable[T] {
  def zero: T
  def append(a: T, b: T): T
}
//yup, it's our friend the monoid, with a different name!

Sie können dann implizite Instanzen dieser Unterklasse erstellen, die jedem Typ entsprechen, den Sie hinzufügen möchten:

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...

Die Methode zur Summierung einer Liste ist dann trivial zu schreiben...

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

Das Schöne an diesem Ansatz ist, dass Sie eine alternative Definition einer Typklasse bereitstellen können, indem Sie entweder das implizite Argument, das Sie im Geltungsbereich haben möchten, über Importe steuern oder indem Sie das ansonsten implizite Argument explizit bereitstellen. So ist es möglich, verschiedene Arten der Addition von Wellenformen bereitzustellen oder Modulo-Arithmetik für die Addition ganzer Zahlen zu spezifizieren. Es ist auch ziemlich einfach, einen Typ aus einer Bibliothek eines Drittanbieters zu Ihrer Typklasse hinzuzufügen.

Dies ist übrigens genau der Ansatz, den die 2.8er API für Sammlungen verfolgt. Obwohl die sum Methode ist definiert für TraversableLike statt auf List , und die Typklasse ist Numeric (er enthält auch ein paar mehr Operationen als nur zero y append )

32voto

Alexey Romanov Punkte 160158

Lesen Sie noch einmal den ersten Kommentar dort:

Ein entscheidender Unterschied zwischen Typklassen und Schnittstellen besteht darin, dass Klasse A nur dann "Mitglied" einer Schnittstelle sein kann, wenn sie dies an der Stelle ihrer eigenen Definition deklariert. Im Gegensatz dazu kann einer Typklasse jederzeit ein beliebiger Typ hinzugefügt werden, vorausgesetzt, man kann die erforderlichen Definitionen bereitstellen, so dass die Mitglieder einer Typklasse zu einem bestimmten Zeitpunkt vom aktuellen Anwendungsbereich abhängig sind. Daher ist es uns egal, ob der Ersteller von A die Typklasse vorweggenommen hat, zu der er gehören soll; wenn nicht, können wir einfach unsere eigene Definition erstellen, die zeigt, dass er tatsächlich dazugehört, und sie dann entsprechend verwenden. Dies ist also nicht nur eine bessere Lösung als Adapter, sondern macht in gewisser Weise das ganze Problem, das mit Adaptern gelöst werden sollte, überflüssig.

Dies ist meiner Meinung nach der wichtigste Vorteil von Typklassen.

Außerdem werden die Fälle korrekt behandelt, in denen die Operationen nicht das Argument des Typs haben, auf den wir dispatchen, oder mehr als eines haben. Betrachten Sie z. B. diese Typklasse:

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)

  ...
}

9voto

IttayD Punkte 26802

Ich betrachte Typklassen als die Möglichkeit, einer Klasse typsichere Metadaten hinzuzufügen.

Sie definieren also zunächst eine Klasse, um den Problembereich zu modellieren, und denken dann über Metadaten nach, die Sie ihr hinzufügen können. Dinge wie Equals, Hashable, Viewable, etc. Auf diese Weise wird eine Trennung zwischen dem Problembereich und den Mechanismen zur Verwendung der Klasse geschaffen und die Möglichkeit der Unterklassifizierung eröffnet, da die Klasse schlanker ist.

Abgesehen davon können Sie Typklassen überall im Anwendungsbereich hinzufügen, nicht nur dort, wo die Klasse definiert ist, und Sie können Implementierungen ändern. Wenn ich beispielsweise mit Point#hashCode einen Hash-Code für eine Point-Klasse berechne, bin ich auf diese spezifische Implementierung beschränkt, die möglicherweise keine gute Verteilung der Werte für die spezifische Menge von Points, die ich habe, erzeugt. Wenn ich jedoch Hashable[Point] verwende, kann ich meine eigene Implementierung bereitstellen.

[Aktualisiert mit Beispiel] Hier ist ein Beispiel für einen Anwendungsfall, den ich letzte Woche hatte. In unserem Produkt gibt es mehrere Fälle von Maps, die Container als Werte enthalten. Z.B., Map[Int, List[String]] o Map[String, Set[Int]] . Das Hinzufügen zu diesen Sammlungen kann langwierig sein:

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

Ich wollte also eine Funktion haben, die dies umschließt, damit ich schreiben kann

map +++= key -> value

Das Hauptproblem ist, dass die Sammlungen nicht alle die gleichen Methoden zum Hinzufügen von Elementen haben. Einige haben '+', andere ':+'. Ich wollte auch die Effizienz des Hinzufügens von Elementen zu einer Liste beibehalten, also wollte ich nicht fold/map verwenden, die neue Sammlungen erstellen.

Die Lösung ist die Verwendung von Typklassen:

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

Hier habe ich eine Typklasse definiert Addable die ein Element C zu einer Sammlung CC hinzufügen kann. Ich habe 2 Standardimplementierungen: Für Listen mit :: und für andere Sammlungen mit Hilfe des Builder-Frameworks.

Dann ist die Verwendung dieser Typklasse:

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

Der besondere Clou ist die Verwendung von adder.add um die Elemente hinzuzufügen und adder.empty um neue Sammlungen für neue Schlüssel zu erstellen.

Zum Vergleich: Ohne Typklassen hätte ich 3 Möglichkeiten gehabt: 1. eine Methode pro Sammlungstyp zu schreiben. Z.B., addElementToSubList y addElementToSet usw. Dies führt zu einer Menge an Boilerplate in der Implementierung und verschmutzt den Namespace 2. Reflection zu verwenden, um festzustellen, ob die Untersammlung eine Liste / Menge ist. Das ist knifflig, da die Map zu Beginn leer ist (natürlich hilft Scala hier auch mit Manifesten) 3. eine "poor-man's type class" zu haben, indem man den Benutzer auffordert, den Addierer zu liefern. Also etwas wie addToMap(map, key, value, adder) was einfach hässlich ist

6voto

Bradford Punkte 4035

Eine weitere Möglichkeit, wie ich diesen Blogbeitrag hilfreich finde, ist die Beschreibung von Typklassen: Monaden sind keine Metaphern

Suchen Sie im Artikel nach typeclass. Es sollte der erste Treffer sein. In diesem Artikel liefert der Autor ein Beispiel für eine Monad-Typklasse.

5voto

VonC Punkte 1117238

Der Forumsbeitrag " Was macht Typklassen besser als Traits? " enthält einige interessante Punkte:

  • Typklassen können sehr einfach Begriffe darstellen, die in Gegenwart von Subtypen recht schwierig darzustellen sind, wie z. B. Gleichstellung y Bestellung .
    Übung: Erstellen Sie eine kleine Klassen-/Eigenschaftshierarchie und versuchen Sie, diese zu implementieren. .equals auf jede Klasse/Eigenschaft in der Weise, dass die Operation über beliebige Instanzen der Hierarchie ordnungsgemäß reflexiv, symmetrisch und transitiv ist.
  • Mit Typklassen können Sie nachweisen, dass ein Typ außerhalb Ihrer "Kontrolle" mit einem bestimmten Verhalten übereinstimmt.
    Der Typ einer anderen Person kann ein Mitglied Ihrer Typklasse sein.
  • Sie können nicht ausdrücken "diese Methode nimmt/liefert einen Wert desselben Typs wie der Methodenempfänger" in Form von Subtyping, aber diese (sehr nützliche) Einschränkung ist mit Typklassen einfach zu erreichen. Dies ist die f-gebundene Typen Problem (wobei ein F-begrenzter Typ über seine eigenen Subtypen parametrisiert ist).
  • Alle Operationen, die für einen Trait definiert werden, erfordern eine Instanz ; es gibt immer eine this Argument. Sie können also zum Beispiel nicht ein fromString(s:String): Foo Methode auf trait Foo so zu gestalten, dass Sie es ohne eine Instanz von Foo .
    In Scala manifestiert sich dies darin, dass die Leute verzweifelt versuchen, über Begleitobjekte zu abstrahieren.
    Bei einer Typklasse ist dies jedoch problemlos möglich, wie das Null-Element in dieses monoide Beispiel .
  • Typklassen können induktiv definiert werden Wenn Sie zum Beispiel eine JsonCodec[Woozle] können Sie eine JsonCodec[List[Woozle]] umsonst.
    Das obige Beispiel veranschaulicht dies für "Dinge, die man zusammenzählen kann".

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X