Come sostituire la domanda in un compagno di classe caso


84

Quindi ecco la situazione. Voglio definire una classe case in questo modo:

case class A(val s: String)

e voglio definire un oggetto per assicurarmi che quando creo istanze della classe, il valore per 's' sia sempre maiuscolo, in questo modo:

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

Tuttavia, questo non funziona poiché Scala si lamenta del fatto che il metodo apply (s: String) è definito due volte. Capisco che la sintassi della classe case lo definirà automaticamente per me, ma non c'è un altro modo per ottenere questo risultato? Mi piacerebbe attenermi alla classe case poiché voglio usarla per la corrispondenza dei modelli.


3
Forse cambia il titolo in "Come ignorare l'applicazione in un compagno di classe del caso"
ziggystar

1
Non usare lo zucchero se non fa quello che vuoi ...
Raphael

7
@Raphael E se vuoi lo zucchero di canna, cioè vogliamo lo zucchero con alcuni attributi speciali .. Ho la stessa precisa richiesta dell'OP: le classi case sono v utili ma è un caso d'uso abbastanza comune da voler decorare l'oggetto associato con una domanda aggiuntiva.
StephenBoesch

Cordiali saluti Questo è risolto in scala 2.12+. La definizione di un metodo di applicazione altrimenti confuso nel companion impedisce di generare il metodo di applicazione predefinito.
stewSquared

Risposte:


90

La ragione del conflitto è che la classe case fornisce lo stesso identico metodo apply () (stessa firma).

Prima di tutto vorrei suggerirti di utilizzare require:

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

Questo genererà un'eccezione se l'utente tenta di creare un'istanza in cui s include caratteri minuscoli. Questo è un buon uso delle classi case, dal momento che ciò che metti nel costruttore è anche ciò che ottieni quando usi il pattern matching ( match).

Se questo non è quello che vuoi, allora creerei il costruttore privatee costringerei gli utenti a usare solo il metodo apply:

class A private (val s: String) {
}

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

Come vedi, A non è più a case class. Non sono sicuro che le classi case con campi immutabili siano destinate alla modifica dei valori in ingresso, poiché il nome "classe case" implica che dovrebbe essere possibile estrarre gli argomenti del costruttore (non modificati) utilizzando match.


5
La toCharArraychiamata non è necessaria, potresti anche scrivere s.exists(_.isLower).
Frank S. Thomas,

4
BTW penso che s.forall(_.isUpper)sia più facile da capire di !s.exists(_.isLower).
Frank S. Thomas,

Grazie! Questo sicuramente funziona per le mie esigenze. @ Frank, sono d'accordo che s.forall(_isupper)è più facile da leggere. Lo userò insieme al suggerimento di @olle.
John S

4
+1 per "il nome" classe case "implica che dovrebbe essere possibile estrarre gli argomenti del costruttore (non modificati) utilizzando match."
Eugen Labun

2
@ollekullberg Non è necessario abbandonare l'utilizzo di una classe case (e perdere tutti i vantaggi extra forniti di default da una classe case) per ottenere l'effetto desiderato dall'OP. Se apporti due modifiche, puoi avere la classe del tuo caso e mangiarla anche tu! A) contrassegnare la classe case come astratta e B) contrassegnare il costruttore della classe case come private [A] (in opposizione a private). Ci sono altri problemi più sottili riguardo all'estensione delle classi di casi usando questa tecnica. Si prega di vedere la risposta che ho postato per i dettagli più approfonditi: stackoverflow.com/a/25538287/501113
chaotic3quilibrium

28

AGGIORNAMENTO 25/02/2016:
Sebbene la risposta che ho scritto di seguito rimanga sufficiente, vale anche la pena fare riferimento a un'altra risposta correlata a questo riguardo all'oggetto associato della classe case. Vale a dire, come si riproduce esattamente l'oggetto associato implicito generato dal compilatore che si verifica quando si definisce solo la classe case stessa. Per me si è rivelato controintuitivo.


Riepilogo:
è possibile modificare il valore di un parametro della classe case prima che venga memorizzato nella classe case in modo abbastanza semplice pur rimanendo un ADT (Abstract Data Type) valido (ated). Sebbene la soluzione fosse relativamente semplice, scoprire i dettagli è stato un po 'più impegnativo.

Dettagli:
se vuoi assicurarti che solo istanze valide della tua classe case possano essere istanziate, che è un presupposto essenziale dietro un ADT (Abstract Data Type), ci sono una serie di cose che devi fare.

Ad esempio, un copymetodo generato dal compilatore viene fornito per impostazione predefinita su una classe case. Quindi, anche se fossi molto attento a assicurarti che solo le istanze siano state create tramite il applymetodo dell'oggetto associato esplicito che garantiva che potevano contenere solo valori maiuscoli, il codice seguente produrrebbe un'istanza di classe case con un valore minuscolo:

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

Inoltre, implementano le classi case java.io.Serializable. Ciò significa che la tua attenta strategia di avere solo le maiuscole può essere sovvertita con un semplice editor di testo e deserializzazione.

Quindi, per tutti i vari modi in cui la classe del tuo caso può essere utilizzata (in modo benevolo e / o malevolo), ecco le azioni che devi intraprendere:

  1. Per il tuo oggetto compagno esplicito:
    1. Crealo usando esattamente lo stesso nome della classe del tuo caso
      • Questo ha accesso alle parti private della classe del caso
    2. Crea un applymetodo con esattamente la stessa firma del costruttore principale per la tua classe case
      • Questo verrà compilato correttamente una volta completato il passaggio 2.1
    3. Fornire un'implementazione ottenendo un'istanza della classe case utilizzando l' newoperatore e fornendo un'implementazione vuota{}
      • Questo ora istanzerà la classe del caso rigorosamente secondo le tue condizioni
      • L'implementazione vuota {}deve essere fornita perché la classe case è dichiarata abstract(vedere il passaggio 2.1)
  2. Per la tua classe di casi:
    1. Dichiaralo abstract
      • Impedisce al compilatore Scala di generare un applymetodo nell'oggetto associato che è la causa dell'errore di compilazione "il metodo è definito due volte ..." (passaggio 1.2 sopra)
    2. Contrassegna il costruttore principale come private[A]
      • Il costruttore primario è ora disponibile solo per la classe case stessa e per il suo oggetto associato (quello che abbiamo definito sopra nel passaggio 1.1)
    3. Crea un readResolvemetodo
      1. Fornire un'implementazione utilizzando il metodo apply (passaggio 1.2 sopra)
    4. Crea un copymetodo
      1. Definitelo in modo che abbia esattamente la stessa firma del costruttore principale della classe case
      2. Per ogni parametro, aggiungere un valore predefinito utilizzando lo stesso nome del parametro (es: s: String = s)
      3. Fornire un'implementazione utilizzando il metodo apply (passaggio 1.2 di seguito)

Ecco il tuo codice modificato con le azioni precedenti:

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

Ed ecco il tuo codice dopo aver implementato il require (suggerito nella risposta @ollekullberg) e aver anche identificato il posto ideale dove mettere qualsiasi tipo di caching:

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

E questa versione è più sicura / robusta se questo codice verrà utilizzato tramite l'interoperabilità Java (nasconde la classe case come implementazione e crea una classe finale che impedisce le derivazioni):

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

Sebbene questo risponda direttamente alla tua domanda, ci sono ancora più modi per espandere questo percorso attorno alle classi di casi oltre alla memorizzazione nella cache delle istanze. Per le mie esigenze di progetto, ho creato una soluzione ancora più ampia che ho documentato su CodeReview (un sito gemello StackOverflow). Se finisci per esaminarlo, utilizzare o sfruttare la mia soluzione, ti preghiamo di lasciarmi feedback, suggerimenti o domande e, entro limiti ragionevoli, farò del mio meglio per rispondere entro un giorno.


Ho appena pubblicato una nuova soluzione espansiva per essere più idiomatico di Scala e per includere l'uso di ScalaCache per memorizzare facilmente nella cache istanze di classi di casi (non era consentito modificare la risposta esistente secondo le meta regole): codereview.stackexchange.com/a/98367/4758
caotic3quilibrium

grazie per questa spiegazione dettagliata. Ma sto lottando per capire perché è richiesta l'implementazione di readResolve. Perché la compilazione funziona anche senza l'implementazione di readResolve.
mogli

postato una domanda separata: stackoverflow.com/questions/32236594/...
Mogli

12

Non so come sovrascrivere il applymetodo nell'oggetto associato (se possibile) ma potresti anche usare un tipo speciale per le stringhe in maiuscolo:

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

Il codice precedente restituisce:

A(HELLO)

Dovresti anche dare un'occhiata a questa domanda e alle sue risposte: Scala: è possibile sovrascrivere il costruttore della classe case predefinito?


Grazie per questo - stavo pensando sulla stessa linea ma non lo sapevo Proxy! Potrebbe essere meglio s.toUpperCase una volta però.
Ben Jackson

@ Ben non vedo dove toUpperCaseviene chiamato più di una volta.
Frank S. Thomas,

hai perfettamente ragione val self, no def self. Ho appena avuto C ++ nel cervello.
Ben Jackson,

6

Per le persone che leggono questo articolo dopo aprile 2017: a partire da Scala 2.12.2+, Scala consente di ignorare applica e non applica per impostazione predefinita . Puoi ottenere questo comportamento dando l' -Xsource:2.12opzione al compilatore anche su Scala 2.11.11+.


1
Cosa significa questo? Come posso applicare questa conoscenza a una soluzione? Puoi fornire un esempio?
k0pernikus

Si noti che unapply non viene utilizzata per classi case pattern matching, che rende imperativi è abbastanza inutile (se -Xprintuna matchdichiarazione vedrete che non è utilizzato).
J Cracknell

5

Funziona con variabili var:

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

Questa pratica è apparentemente incoraggiata nelle classi case invece di definire un altro costruttore. Vedere qui. . Quando si copia un oggetto, si mantengono anche le stesse modifiche.


4

Un'altra idea pur mantenendo la classe case e non avendo def impliciti o un altro costruttore è quella di rendere la firma di applyleggermente diversa ma dal punto di vista dell'utente la stessa. Da qualche parte ho visto il trucco implicito, ma non riesco a ricordare / trovare quale argomento implicito fosse, quindi ho scelto Booleanqui. Se qualcuno può darmi una mano e finire il trucco ...

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)

Nei siti di chiamata ti darà un errore di compilazione (riferimento ambiguo alla definizione sovraccarica). Funziona solo se i tipi di scala sono diversi ma sono gli stessi dopo la cancellazione, ad esempio per avere due funzioni differenti per List [Int] e List [String].
Mikaël Mayer

Non sono riuscito a far funzionare questo percorso di soluzione (con 2.11). Alla fine ho capito perché non poteva fornire il proprio metodo di applicazione sull'oggetto compagno esplicito. Ho dettagliato nella risposta che ho appena postato: stackoverflow.com/a/25538287/501113
chaotic3quilibrium

3

Ho affrontato lo stesso problema e questa soluzione è giusta per me:

sealed trait A {
  def s:String
}

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

E, se è necessario un metodo, basta definirlo nel tratto e sovrascriverlo nella classe case.


0

Se sei bloccato con la scala più vecchia in cui non puoi sovrascrivere per impostazione predefinita o non vuoi aggiungere il flag del compilatore come mostrato da @ mehmet-emre e hai bisogno di una classe case, puoi fare quanto segue:

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

0

A partire dal 2020 su Scala 2.13, lo scenario precedente di sovrascrivere un metodo di applicazione della classe case con la stessa firma funziona perfettamente.

case class A(val s: String)

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

lo snippet di cui sopra si compila e funziona perfettamente in Scala 2.13 sia in modalità REPL che non-REPL.


-2

Penso che funzioni esattamente come lo vuoi già. Ecco la mia sessione 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)

Questo sta usando Scala 2.8.1.final


3
Non funziona qui se inserisco il codice in un file e provo a compilarlo.
Frank S. Thomas,

Credo di aver suggerito qualcosa di simile in una risposta precedente e qualcuno ha detto che funziona solo nella sostituzione a causa del modo in cui funziona la sostituzione.
Ben Jackson

5
Il REPL crea essenzialmente un nuovo ambito con ogni riga, all'interno del precedente. Ecco perché alcune cose non funzionano come previsto quando vengono incollate da REPL nel codice. Quindi, controlla sempre entrambi.
gregturn

1
Il modo corretto per testare il codice sopra (che non funziona) è usare: incolla nel REPL per assicurarti che sia il caso che l'oggetto siano definiti insieme.
StephenBoesch
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.