So überschreiben Sie die Anwendung in einem Fallklassenbegleiter

83

Hier ist also die Situation. Ich möchte eine Fallklasse wie folgt definieren:

case class A(val s: String)

und ich möchte ein Objekt definieren, um sicherzustellen, dass beim Erstellen von Instanzen der Klasse der Wert für 's' immer in Großbuchstaben geschrieben wird, wie folgt:

object A {
  def apply(s: String) = new A(s.toUpperCase)
}

Dies funktioniert jedoch nicht, da Scala sich darüber beschwert, dass die Methode apply (s: String) zweimal definiert ist. Ich verstehe, dass die Syntax der Fallklasse sie automatisch für mich definiert, aber gibt es keine andere Möglichkeit, dies zu erreichen? Ich möchte bei der Fallklasse bleiben, da ich sie für den Mustervergleich verwenden möchte.

John S.
quelle
3
Vielleicht ändern Sie den Titel in "So überschreiben Sie die Anwendung in einem
Fallklassenbegleiter
1
Verwenden Sie keinen Zucker, wenn er nicht das tut, was Sie wollen ...
Raphael
7
@Raphael Was ist, wenn Sie braunen Zucker wollen, dh wir wollen Zucker mit einigen speziellen Attributen. Ich habe genau die gleiche Anfrage wie das OP: Fallklassen sind sehr nützlich, aber es ist ein häufig genug verwendeter Anwendungsfall, mit dem das Begleitobjekt dekoriert werden soll eine zusätzliche gelten.
Javadba
Zu Ihrer Information Dies ist in Scala 2.12+ behoben. Das Definieren einer ansonsten konfluierenden Apply-Methode im Begleiter verhindert das Generieren der Standard-Apply-Methode.
Eintopf Quadrat

Antworten:

89

Der Grund für den Konflikt ist, dass die Fallklasse genau dieselbe Methode apply () (dieselbe Signatur) bereitstellt.

Zunächst möchte ich vorschlagen, dass Sie Folgendes benötigen:

case class A(s: String) {
  require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s)
}

Dies löst eine Ausnahme aus, wenn der Benutzer versucht, eine Instanz zu erstellen, in der s Kleinbuchstaben enthält. Dies ist eine gute Verwendung von Fallklassen, da Sie in den Konstruktor auch das eingeben, was Sie bei Verwendung von Pattern Matching ( match) erhalten.

Wenn dies nicht das ist, was Sie wollen, würde ich den Konstruktor privateerstellen und die Benutzer zwingen, nur die Methode apply zu verwenden:

class A private (val s: String) {
}

object A {
  def apply(s: String): A = new A(s.toUpperCase)
}

Wie Sie sehen, ist A nicht länger a case class. Ich bin nicht sicher, ob Fallklassen mit unveränderlichen Feldern zur Änderung der eingehenden Werte gedacht sind, da der Name "Fallklasse" impliziert, dass es möglich sein sollte, die (nicht modifizierten) Konstruktorargumente mit zu extrahieren match.

olle kullberg
quelle
5
Der toCharArrayAnruf ist nicht notwendig, Sie könnten auch schreiben s.exists(_.isLower).
Frank S. Thomas
4
Übrigens denke ich s.forall(_.isUpper)ist leichter zu verstehen als !s.exists(_.isLower).
Frank S. Thomas
Vielen Dank! Dies funktioniert sicherlich für meine Bedürfnisse. @ Frank, ich stimme zu, dass s.forall(_isupper)das leichter zu lesen ist. Ich werde das in Verbindung mit dem Vorschlag von @ olle verwenden.
John S
4
+1 für "the name" case class "impliziert, dass es möglich sein sollte, die (unveränderten) Konstruktorargumente mit zu extrahieren match."
Eugen Labun
2
@ollekullberg Sie müssen nicht von der Verwendung einer Fallklasse abweichen (und alle zusätzlichen Extras verlieren, die eine Fallklasse standardmäßig bietet), um den gewünschten Effekt des OP zu erzielen. Wenn Sie zwei Änderungen vornehmen, können Sie Ihre Fallklasse haben und sie auch essen! A) Markieren Sie die Fallklasse als abstrakt und B) Markieren Sie den Fallklassenkonstruktor als privat [A] (im Gegensatz zu nur privat). Es gibt einige andere subtilere Probleme beim Erweitern von Fallklassen mit dieser Technik. Weitere Informationen finden Sie in der Antwort, die ich gepostet habe: stackoverflow.com/a/25538287/501113
chaotic3quilibrium
28

UPDATE 25.02.2016:
Obwohl die Antwort, die ich unten geschrieben habe, weiterhin ausreichend ist, lohnt es sich, auch auf eine andere verwandte Antwort in Bezug auf das Begleitobjekt der Fallklasse zu verweisen. Das heißt, wie kann man genau den Compiler erzeugte implizite Begleitobjekt reproduzieren , das auftritt , wenn man nur den Fall der Klasse selbst definiert. Für mich erwies es sich als kontraintuitiv.


Zusammenfassung:
Sie können den Wert eines Fallklassenparameters ändern, bevor er in der Fallklasse gespeichert wird, ganz einfach, während er noch ein gültiger (ated) ADT (Abstract Data Type) bleibt. Während die Lösung relativ einfach war, war es etwas schwieriger, die Details zu entdecken.

Details:
Wenn Sie sicherstellen möchten, dass nur gültige Instanzen Ihrer Fallklasse instanziiert werden können, was eine wesentliche Voraussetzung für einen ADT (Abstract Data Type) ist, müssen Sie eine Reihe von Maßnahmen ergreifen.

Beispielsweise copywird standardmäßig eine vom Compiler generierte Methode für eine Fallklasse bereitgestellt. Selbst wenn Sie sehr sorgfältig darauf achten würden, dass nur Instanzen über die applyMethode des expliziten Begleitobjekts erstellt werden, die garantiert, dass sie immer nur Großbuchstaben enthalten können, würde der folgende Code eine Fallklasseninstanz mit einem Kleinbuchstabenwert erzeugen:

val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"

Zusätzlich werden Fallklassen implementiert java.io.Serializable. Dies bedeutet, dass Ihre sorgfältige Strategie, nur Großbuchstaben zu verwenden, mit einem einfachen Texteditor und einer Deserialisierung unterlaufen werden kann.

Für all die verschiedenen Möglichkeiten, wie Ihre Fallklasse verwendet werden kann (wohlwollend und / oder böswillig), müssen Sie folgende Maßnahmen ergreifen:

  1. Für Ihr explizites Begleitobjekt:
    1. Erstellen Sie es mit genau demselben Namen wie Ihre Fallklasse
      • Dies hat Zugriff auf die privaten Teile der Fallklasse
    2. Erstellen Sie eine applyMethode mit genau derselben Signatur wie der primäre Konstruktor für Ihre Fallklasse
      • Dies wird erfolgreich kompiliert, sobald Schritt 2.1 abgeschlossen ist
    3. Stellen Sie eine Implementierung bereit, die mithilfe des newOperators eine Instanz der Fallklasse abruft und eine leere Implementierung bereitstellt{}
      • Dadurch wird die Fallklasse nun streng zu Ihren Bedingungen instanziiert
      • Die leere Implementierung {}muss bereitgestellt werden, da die Fallklasse deklariert ist abstract(siehe Schritt 2.1).
  2. Für Ihre Fallklasse:
    1. Erkläre es abstract
      • Verhindert, dass der Scala-Compiler eine applyMethode im Begleitobjekt generiert, die den Kompilierungsfehler "Methode wird zweimal definiert ..." verursacht hat (Schritt 1.2 oben).
    2. Markieren Sie den primären Konstruktor als private[A]
      • Der primäre Konstruktor ist jetzt nur für die Fallklasse selbst und für ihr Begleitobjekt verfügbar (das oben in Schritt 1.1 definierte).
    3. Erstellen Sie eine readResolveMethode
      1. Stellen Sie eine Implementierung mit der Methode apply bereit (Schritt 1.2 oben).
    4. Erstellen Sie eine copyMethode
      1. Definieren Sie es so, dass es genau dieselbe Signatur wie der primäre Konstruktor der Fallklasse hat
      2. Für jeden Parameter, fügen Sie einen Standardwert unter Verwendung der gleichen Parameternamen (zB: s: String = s)
      3. Stellen Sie eine Implementierung mit der Apply-Methode bereit (Schritt 1.2 unten).

Hier ist Ihr Code, der mit den oben genannten Aktionen geändert wurde:

object A {
  def apply(s: String, i: Int): A =
    new A(s.toUpperCase, i) {} //abstract class implementation intentionally empty
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

Und hier ist Ihr Code, nachdem Sie die Anforderung implementiert haben (vorgeschlagen in der @ kollekullberg-Antwort) und den idealen Ort für jede Art von Caching identifiziert haben:

object A {
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i) {} //abstract class implementation intentionally empty
  }
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

Und diese Version ist sicherer / robuster, wenn dieser Code über Java Interop verwendet wird (versteckt die case-Klasse als Implementierung und erstellt eine endgültige Klasse, die Ableitungen verhindert):

object A {
  private[A] abstract case class AImpl private[A] (s: String, i: Int)
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i)
  }
}
final class A private[A] (s: String, i: Int) extends A.AImpl(s, i) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

Während dies Ihre Frage direkt beantwortet, gibt es noch mehr Möglichkeiten, diesen Pfad um Fallklassen über das Instanz-Caching hinaus zu erweitern. Für meine eigenen Projektanforderungen habe ich eine noch umfassendere Lösung erstellt, die ich auf CodeReview (einer StackOverflow-Schwestersite) dokumentiert habe . Wenn Sie am Ende darüber nachdenken, meine Lösung nutzen oder nutzen, sollten Sie mir Feedback, Vorschläge oder Fragen hinterlassen. Ich werde mein Bestes tun, um innerhalb eines Tages zu antworten.

chaotisches Gleichgewicht
quelle
Ich habe gerade eine neuere, expansive Lösung veröffentlicht, um Scala-idiomatischer zu gestalten und die Verwendung von ScalaCache zum einfachen Zwischenspeichern von Fallklasseninstanzen einzuschließen (die vorhandene Antwort durfte gemäß den Metaregeln nicht bearbeitet werden): codereview.stackexchange.com/a/98367/4758
chaotic3quilibrium
Vielen Dank für diese ausführliche Erklärung. Ich habe jedoch Schwierigkeiten zu verstehen, warum die Implementierung von readResolve erforderlich ist. Weil die Kompilierung auch ohne readResolve-Implementierung funktioniert.
Mogli
hat eine separate Frage gestellt: stackoverflow.com/questions/32236594/…
mogli
12

Ich weiß nicht, wie ich die applyMethode im Begleitobjekt überschreiben soll (falls dies überhaupt möglich ist), aber Sie können auch einen speziellen Typ für Zeichenfolgen in Großbuchstaben verwenden:

class UpperCaseString(s: String) extends Proxy {
  val self: String = s.toUpperCase
}

implicit def stringToUpperCaseString(s: String) = new UpperCaseString(s)
implicit def upperCaseStringToString(s: UpperCaseString) = s.self

case class A(val s: UpperCaseString)

println(A("hello"))

Der obige Code gibt aus:

A(HELLO)

Sie sollten sich auch diese Frage und ihre Antworten ansehen: Scala: Ist es möglich, den Standardkonstruktor für Fallklassen zu überschreiben?

Frank S. Thomas
quelle
Danke dafür - ich dachte in die gleiche Richtung, wusste aber nichts davon Proxy! Könnte aber besser sein als s.toUpperCase einmal .
Ben Jackson
@ Ben Ich sehe nicht, wo toUpperCasemehr als einmal angerufen wird.
Frank S. Thomas
Du hast ganz recht val self, nicht def self. Ich habe gerade C ++ im Gehirn.
Ben Jackson
6

Für die Personen, die dies nach April 2017 lesen: Ab Scala 2.12.2+ erlaubt Scala standardmäßig das Überschreiben von Anwenden und Nicht-Anwenden . Sie können dieses Verhalten erhalten, indem Sie -Xsource:2.12dem Compiler auch in Scala 2.11.11+ eine Option geben .

Mehmet Emre
quelle
1
Was bedeutet das? Wie kann ich dieses Wissen auf eine Lösung anwenden? Können Sie ein Beispiel geben?
k0pernikus
Beachten Sie, dass unapply nicht für Fallklassen mit Mustervergleich verwendet wird, was das Überschreiben ziemlich nutzlos macht (wenn Sie -Xprinteine matchAnweisung eingeben, werden Sie feststellen, dass es nicht verwendet wird).
J Cracknell
5

Es funktioniert mit var Variablen:

case class A(var s: String) {
   // Conversion
   s = s.toUpperCase
}

Diese Praxis wird anscheinend in Fallklassen empfohlen, anstatt einen anderen Konstruktor zu definieren. Siehe hier. . Beim Kopieren eines Objekts behalten Sie auch die gleichen Änderungen bei.

Mikaël Mayer
quelle
4

Eine andere Idee, die Fallklasse beizubehalten und keine impliziten Defs oder einen anderen Konstruktor zu haben, besteht darin, die Signatur von applyetwas anders, aber aus Benutzersicht gleich zu machen. Irgendwo habe ich den impliziten Trick gesehen, kann mich aber nicht erinnern / finden, welches implizite Argument es war, also habe ich Booleanhier gewählt. Wenn mir jemand helfen und den Trick beenden kann ...

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)
Peter Schmitz
quelle
An Aufrufstandorten wird ein Kompilierungsfehler angezeigt (mehrdeutiger Verweis auf überladene Definition). Dies funktioniert nur, wenn die Scala- Typen nach dem Löschen unterschiedlich, aber gleich sind, z. B. um zwei verschiedene Funktionen für eine Liste [Int] und eine Liste [Zeichenfolge] zu haben.
Mikaël Mayer
Ich konnte diesen Lösungsweg nicht zum Laufen bringen (mit 2.11). Ich habe schließlich herausgefunden, warum er keine eigene Apply-Methode für das explizite Begleitobjekt bereitstellen konnte. Ich habe es in der Antwort, die ich gerade gepostet habe, detailliert beschrieben: stackoverflow.com/a/25538287/501113
chaotic3quilibrium
3

Ich hatte das gleiche Problem und diese Lösung ist für mich in Ordnung:

sealed trait A {
  def s:String
}

object A {
  private case class AImpl(s:String)
  def apply(s:String):A = AImpl(s.toUpperCase)
}

Und wenn eine Methode benötigt wird, definieren Sie sie einfach im Merkmal und überschreiben Sie sie in der Fallklasse.

Pere Ramirez
quelle
0

Wenn Sie mit einer älteren Scala nicht weiterkommen, bei der Sie standardmäßig nicht überschreiben können, oder wenn Sie das Compiler-Flag nicht hinzufügen möchten, wie @ mehmet-emre gezeigt hat, und Sie eine Fallklasse benötigen, können Sie Folgendes tun:

case class A(private val _s: String) {
  val s = _s.toUpperCase
}
kritium
quelle
-2

Ich denke das funktioniert genau so wie du es schon willst. Hier ist meine REPL-Sitzung:

scala> case class A(val s: String)
defined class A

scala> object A {
     | def apply(s: String) = new A(s.toUpperCase)
     | }
defined module A

scala> A("hello")
res0: A = A(HELLO)

Dies verwendet Scala 2.8.1.final

mattrjacobs
quelle
3
Hier funktioniert es nicht, wenn ich den Code in eine Datei einfüge und versuche, ihn zu kompilieren.
Frank S. Thomas
Ich glaube, ich habe in einer früheren Antwort etwas Ähnliches vorgeschlagen, und jemand sagte, dass es aufgrund der Funktionsweise von repl nur in der Repl funktioniert.
Ben Jackson
5
Die REPL erstellt im Wesentlichen mit jeder Zeile innerhalb der vorherigen einen neuen Bereich. Aus diesem Grund funktionieren einige Dinge nicht wie erwartet, wenn sie aus der REPL in Ihren Code eingefügt werden. Überprüfen Sie also immer beide.
Gregturn
1
Der richtige Weg, um den obigen Code zu testen (was nicht funktioniert), ist folgende: Einfügen in die REPL, um sicherzustellen, dass sowohl Groß- als auch Kleinschreibung zusammen definiert sind.
Javadba