Jak przesłonić zastosowanie w towarzyszącej klasie sprawy

84

Oto sytuacja. Chcę zdefiniować taką klasę przypadku:

case class A(val s: String)

i chcę zdefiniować obiekt, aby zapewnić, że podczas tworzenia instancji klasy wartość „s” jest zawsze wielka, na przykład:

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

Jednak to nie działa, ponieważ Scala narzeka, że ​​metoda apply (s: String) jest zdefiniowana dwukrotnie. Rozumiem, że składnia klasy przypadku automatycznie zdefiniuje ją za mnie, ale czy nie ma innego sposobu, w jaki mogę to osiągnąć? Chciałbym trzymać się klasy przypadku, ponieważ chcę jej używać do dopasowywania wzorców.

John S.
źródło
3
Może zmień tytuł na „Jak nadpisać zastosowanie w przypadku towarzyszącej klasy sprawy”
ziggystar
1
Nie używaj cukru, jeśli nie robi tego, co chcesz ...
Raphael
7
@Raphael Co jeśli chcesz brązowy cukier, tj. Chcemy cukru z pewnymi specjalnymi atrybutami. Mam dokładnie to samo zapytanie co OP: klasy przypadków są v przydatne, ale jest to dość powszechny przypadek użycia, aby ozdobić obiekt towarzyszący dodatkowe zastosowanie.
StephenBoesch
FYI Naprawiono to w wersji 2.12+. Zdefiniowanie w inny sposób łączącej się metody stosowania w towarzyszącej funkcji zapobiega generowaniu domyślnej metody stosowania.
stewSquared

Odpowiedzi:

90

Przyczyną konfliktu jest to, że klasa case zapewnia dokładnie tę samą metodę apply () (ten sam podpis).

Przede wszystkim chciałbym zasugerować użycie wymagają:

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

Spowoduje to zgłoszenie wyjątku, jeśli użytkownik spróbuje utworzyć wystąpienie, w którym s zawiera małe litery. Jest to dobre zastosowanie klas przypadków, ponieważ to, co umieszczasz w konstruktorze, jest również tym, co otrzymujesz, gdy używasz dopasowywania wzorców ( match).

Jeśli tego nie chcesz, zrobiłbym konstruktora privatei zmusiłbym użytkowników do używania tylko metody zastosuj:

class A private (val s: String) {
}

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

Jak widzisz, A nie jest już case class. Nie jestem pewien, czy klasy przypadków z niezmiennymi polami są przeznaczone do modyfikacji przychodzących wartości, ponieważ nazwa „klasa przypadku” sugeruje, że powinno być możliwe wyodrębnienie (niezmodyfikowanych) argumentów konstruktora przy użyciu match.

olle kullberg
źródło
5
Telefon toCharArraynie jest konieczny, możesz też napisać s.exists(_.isLower).
Frank S. Thomas
4
Swoją drogą myślę, że s.forall(_.isUpper)łatwiej to zrozumieć niż !s.exists(_.isLower).
Frank S. Thomas
dzięki! To z pewnością działa na moje potrzeby. @Frank, zgadzam się, że s.forall(_isupper)jest to łatwiejsze do odczytania. Użyję tego w połączeniu z sugestią @ olle.
John S
4
+1 dla "nazwa" klasa przypadku "oznacza, że ​​powinno być możliwe wyodrębnienie (niezmodyfikowanych) argumentów konstruktora przy użyciu match."
Eugen Labun,
2
@ollekullberg Nie musisz odchodzić od używania klasy skrzynek (i tracić wszystkich dodatkowych korzyści, które domyślnie zapewnia klasa skrzynek), aby osiągnąć pożądany efekt PO. Jeśli wprowadzisz dwie modyfikacje, możesz mieć swoją klasę sprawy i też ją zjeść! A) oznacz klasę przypadku jako abstrakcyjną i B) oznacz konstruktor klasy sprawy jako prywatny [A] (w przeciwieństwie do tylko prywatnego). Istnieje kilka innych, bardziej subtelnych problemów związanych z rozszerzaniem klas przypadków przy użyciu tej techniki. Zapoznaj się z odpowiedzią, którą opublikowałem, aby uzyskać bardziej szczegółowe informacje: stackoverflow.com/a/25538287/501113
chaotic3quilibrium
28

UPDATE 2016/02/25:
Chociaż odpowiedź, którą napisałem poniżej, pozostaje wystarczająca, warto również odwołać się do innej powiązanej odpowiedzi na to, dotyczącej obiektu towarzyszącego klasy przypadku. Mianowicie, w jaki sposób dokładnie odtworzyć niejawny obiekt towarzyszący wygenerowany przez kompilator, który pojawia się, gdy definiuje się tylko samą klasę przypadku. Dla mnie okazało się to sprzeczne z intuicją.


Podsumowanie:
Możesz zmienić wartość parametru klasy przypadku, zanim zostanie on zapisany w klasie przypadku, w prosty sposób, podczas gdy nadal pozostanie prawidłowym (ated) ADT (Abstract Data Type). Chociaż rozwiązanie było stosunkowo proste, odkrycie szczegółów było nieco trudniejsze.

Szczegóły:
Jeśli chcesz mieć pewność, że tylko poprawne instancje klasy przypadku będą mogły zostać kiedykolwiek utworzone, co jest podstawowym założeniem ADT (abstrakcyjny typ danych), musisz zrobić kilka rzeczy.

Na przykład copymetoda wygenerowana przez kompilator jest domyślnie udostępniana w klasie sprawy. Tak więc, nawet gdybyś był bardzo ostrożny, aby upewnić się, że tylko instancje zostały utworzone za pomocą metody jawnego obiektu towarzyszącego, applyktóra gwarantuje, że mogą one zawsze zawierać tylko wielkie litery, poniższy kod utworzy instancję klasy z małymi literami:

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

Dodatkowo klasy przypadków implementują java.io.Serializable. Oznacza to, że twoją ostrożną strategię, aby mieć tylko wielkie litery, można obalić za pomocą prostego edytora tekstu i deserializacji.

Tak więc, dla wszystkich różnych sposobów wykorzystania klasy sprawy (życzliwie i / lub wrogo), oto działania, które musisz wykonać:

  1. W przypadku obiektu towarzyszącego:
    1. Utwórz go, używając dokładnie tej samej nazwy, co klasa sprawy
      • Ma to dostęp do części intymnych klasy sprawy
    2. Utwórz applymetodę z dokładnie tym samym podpisem, co główny konstruktor dla Twojej klasy case
      • Kompilacja zakończy się powodzeniem po zakończeniu kroku 2.1
    3. Podaj implementację uzyskując wystąpienie klasy case przy użyciu newoperatora i podając pustą implementację{}
      • Spowoduje to teraz utworzenie instancji klasy przypadku ściśle według Twoich warunków
      • {}Należy podać pustą implementację, ponieważ została zadeklarowana klasa przypadku abstract(patrz krok 2.1)
  2. Dla Twojej klasy sprawy:
    1. Zadeklaruj to abstract
      • Zapobiega generowaniu przez kompilator Scali applymetody w obiekcie towarzyszącym, co powoduje błąd kompilacji „metoda jest zdefiniowana dwukrotnie ...” (krok 1.2 powyżej)
    2. Oznacz głównego konstruktora jako private[A]
      • Główny konstruktor jest teraz dostępny tylko dla samej klasy przypadku i jej obiektu towarzyszącego (tego, który zdefiniowaliśmy powyżej w kroku 1.1)
    3. Utwórz readResolvemetodę
      1. Zapewnij implementację przy użyciu metody Apply (krok 1.2 powyżej)
    4. Utwórz copymetodę
      1. Zdefiniuj go tak, aby miał dokładnie taką samą sygnaturę jak główny konstruktor klasy przypadku
      2. Dla każdego parametru, należy dodać wartość domyślną przy użyciu tej samej nazwy parametru (np s: String = s)
      3. Zapewnij implementację za pomocą metody zastosuj (krok 1.2 poniżej)

Oto twój kod zmodyfikowany za pomocą powyższych działań:

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

A oto twój kod po zaimplementowaniu wymagania (sugerowanego w odpowiedzi @ollekullberg), a także wskazaniu idealnego miejsca do umieszczenia dowolnego rodzaju buforowania:

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

Ta wersja jest bezpieczniejsza / bardziej niezawodna, jeśli ten kod zostanie użyty za pośrednictwem środowiska Java (ukrywa klasę przypadku jako implementację i tworzy ostateczną klasę, która zapobiega wyprowadzaniu):

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

Chociaż to bezpośrednio odpowiada na twoje pytanie, istnieje jeszcze więcej sposobów na rozszerzenie tej ścieżki o klasy przypadków poza buforowanie instancji. Na potrzeby własnego projektu stworzyłem jeszcze bardziej rozbudowane rozwiązanie, które udokumentowałem w witrynie CodeReview (siostrzanej witrynie StackOverflow). Jeśli w końcu przejrzysz moje rozwiązanie, użyjesz lub wykorzystasz moje rozwiązanie, rozważ zostawienie mi opinii, sugestii lub pytań. Postaram się odpowiedzieć w ciągu jednego dnia.

chaotic3quilibrium
źródło
Właśnie opublikowałem nowsze, ekspansywne rozwiązanie, które ma być bardziej idiomatyczne dla Scala i zawiera użycie ScalaCache do łatwego buforowania instancji klas przypadków (nie wolno było edytować istniejącej odpowiedzi zgodnie z regułami meta): codereview.stackexchange.com/a/98367/4758
chaotic3quilibrium
dzięki za to szczegółowe wyjaśnienie. Ale staram się zrozumieć, dlaczego wdrożenie readResolve jest wymagane. Ponieważ kompilacja działa również bez implementacji readResolve.
mogli
wysłał osobne pytanie: stackoverflow.com/questions/32236594/…
mogli
12

Nie wiem, jak przesłonić applymetodę w obiekcie towarzyszącym (jeśli to w ogóle możliwe), ale możesz również użyć specjalnego typu dla ciągów wielkich liter:

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

Powyższy kod wyprowadza:

A(HELLO)

Powinieneś także spojrzeć na to pytanie i odpowiedzi: Scala: czy można przesłonić domyślny konstruktor klasy przypadku?

Frank S. Thomas
źródło
Dzięki za to - myślałem w tym samym kierunku, ale nie wiedziałem Proxy! Może lepiej s.toUpperCase kiedyś .
Ben Jackson
@Ben Nie widzę, gdzie toUpperCasejest wywoływany więcej niż raz.
Frank S. Thomas
masz rację val self, nie def self. Właśnie mam C ++ w mózgu.
Ben Jackson
6

Dla osób czytających to po kwietniu 2017 r .: Od wersji Scala 2.12.2+, Scala domyślnie zezwala na nadpisywanie stosowania i anulowania . Możesz uzyskać to zachowanie, dając -Xsource:2.12opcję kompilatorowi również w wersji 2.11.11+.

Mehmet Emre
źródło
1
Co to znaczy? Jak mogę zastosować tę wiedzę do rozwiązania? Czy możesz podać przykład?
k0pernikus
Zauważ, że wycofywanie nie jest wykorzystywany do zajęć case dopasowywanie do wzorca, co czyni go dość bezużyteczny nadrzędnych (Jeśli oświadczenie widać, że nie jest używany). -Xprintmatch
J Cracknell
5

Działa ze zmiennymi var:

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

Ta praktyka jest najwyraźniej zalecana w klasach przypadków zamiast definiowania innego konstruktora. Spójrz tutaj. . Podczas kopiowania obiektu zachowujesz te same modyfikacje.

Mikaël Mayer
źródło
4

Innym pomysłem, zachowując klasę przypadku i nie mając ukrytych braków lub innego konstruktora, jest uczynienie podpisu applynieco innym, ale z perspektywy użytkownika takim samym. Gdzieś widziałem ukrytą sztuczkę, ale nie mogę sobie przypomnieć / znaleźć, który to niejawny argument, więc wybrałem Booleantutaj. Jeśli ktoś może mi pomóc i dokończyć sztuczkę ...

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)
Peter Schmitz
źródło
W witrynach wywołań spowoduje to błąd kompilacji (niejednoznaczne odniesienie do przeciążonej definicji). Działa tylko wtedy, gdy typy skal są różne, ale takie same po usunięciu, np. Aby mieć dwie różne funkcje dla List [Int] i List [String].
Mikaël Mayer
Nie mogłem sprawić, by ta ścieżka rozwiązania działała (z 2.11). W końcu zrozumiałem, dlaczego nie mógł zapewnić własnej metody stosowania na jawnym obiekcie towarzyszącym.
Wyszczególniłem
3

Napotkałem ten sam problem i to rozwiązanie jest dla mnie w porządku:

sealed trait A {
  def s:String
}

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

A jeśli potrzebna jest jakakolwiek metoda, po prostu zdefiniuj ją w trait i nadpisz w klasie case.

Pere Ramirez
źródło
0

Jeśli utkniesz ze starszą skalą, w której domyślnie nie możesz nadpisać lub nie chcesz dodawać flagi kompilatora, jak pokazano @ mehmet-emre, i potrzebujesz klasy przypadku, możesz wykonać następujące czynności:

case class A(private val _s: String) {
  val s = _s.toUpperCase
}
krytyka
źródło
0

Począwszy od 2020 w Scali 2.13, powyższy scenariusz zastępowania metody stosowania klasy przypadków z tym samym podpisem działa całkowicie dobrze.

case class A(val s: String)

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

powyższy fragment kodu kompiluje się i działa dobrze w Scali 2.13, zarówno w trybie REPL, jak i nie-REPL.

Gana
źródło
-2

Myślę, że to już działa dokładnie tak, jak chcesz. Oto moja sesja REPL:

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)

Używa Scala 2.8.1.final

mattrjacobs
źródło
3
Nie działa tutaj, jeśli umieszczę kod w pliku i spróbuję go skompilować.
Frank S. Thomas
Wydaje mi się, że zasugerowałem coś podobnego we wcześniejszej odpowiedzi i ktoś powiedział, że działa to tylko w repl ze względu na sposób, w jaki działa repl.
Ben Jackson
5
REPL zasadniczo tworzy nowy zakres z każdym wierszem, wewnątrz poprzedniego. Dlatego niektóre rzeczy nie działają zgodnie z oczekiwaniami po wklejeniu z REPL do kodu. Dlatego zawsze sprawdzaj oba.
gregturn
1
Właściwym sposobem przetestowania powyższego kodu (który nie działa) jest użycie: wklej w REPL, aby upewnić się, że zarówno przypadek, jak i obiekt są zdefiniowane razem.
StephenBoesch