Cómo anular la solicitud en un compañero de clase de caso

84

Así que aquí está la situación. Quiero definir una clase de caso así:

case class A(val s: String)

y quiero definir un objeto para asegurarme de que cuando creo instancias de la clase, el valor de 's' siempre esté en mayúsculas, así:

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

Sin embargo, esto no funciona ya que Scala se queja de que el método apply (s: String) se define dos veces. Entiendo que la sintaxis de la clase de caso la definirá automáticamente para mí, pero ¿no hay otra forma de lograr esto? Me gustaría seguir con la clase de caso ya que quiero usarla para la coincidencia de patrones.

John S
fuente
3
Tal vez cambie el título a "Cómo anular la solicitud en un compañero de clase de caso"
ziggystar
1
No uses azúcar si no hace lo que quieres ...
Raphael
7
@Raphael ¿Qué pasa si quieres azúcar morena, es decir, queremos azúcar con algunos atributos especiales? Tengo la misma consulta precisa que el OP: las clases de casos son muy útiles pero es un caso de uso lo suficientemente común como para querer decorar el objeto complementario con una aplicación adicional.
StephenBoesch
FYI Esto está arreglado en scala 2.12+. La definición de un método de aplicación que de otro modo sería conflictivo en el complemento evita que se genere el método de aplicación predeterminado.
stewSquared

Respuestas:

90

El motivo del conflicto es que la clase de caso proporciona exactamente el mismo método apply () (misma firma).

En primer lugar, me gustaría sugerirle que use require:

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

Esto generará una excepción si el usuario intenta crear una instancia donde s incluye caracteres en minúscula. Este es un buen uso de las clases de casos, ya que lo que pones en el constructor también es lo que obtienes cuando usas la coincidencia de patrones ( match).

Si esto no es lo que desea, entonces crearía el constructor privatey obligaría a los usuarios a usar solo el método de aplicación:

class A private (val s: String) {
}

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

Como puede ver, A ya no es un case class. No estoy seguro si las clases de casos con campos inmutables están destinadas a la modificación de los valores entrantes, ya que el nombre "clase de casos" implica que debería ser posible extraer los argumentos del constructor (sin modificar) usando match.

olle kullberg
fuente
5
La toCharArrayllamada no es necesaria, también podrías escribir s.exists(_.isLower).
Frank S. Thomas
4
Por cierto, creo que s.forall(_.isUpper)es más fácil de entender que !s.exists(_.isLower).
Frank S. Thomas
¡Gracias! Esto ciertamente funciona para mis necesidades. @Frank, estoy de acuerdo en que s.forall(_isupper)es más fácil de leer. Lo usaré junto con la sugerencia de @ olle.
John S
4
+1 para "el nombre" clase de caso "implica que debería ser posible extraer los argumentos del constructor (sin modificar) usando match."
Eugen Labun
2
@ollekullberg No tiene que dejar de usar una clase de caso (y perder todas las ventajas adicionales que una clase de caso proporciona de forma predeterminada) para lograr el efecto deseado del OP. Si hace dos modificaciones, ¡puede tener su clase de caso y comérsela también! A) marque la clase de caso como abstracto y B) marque el constructor de la clase de caso como privado [A] (en lugar de solo privado). Hay algunos otros problemas más sutiles en torno a la extensión de clases de casos utilizando esta técnica. Consulte la respuesta que
publiqué
28

ACTUALIZACIÓN 2016/02/25:
Si bien la respuesta que escribí a continuación sigue siendo suficiente, también vale la pena hacer referencia a otra respuesta relacionada con esto con respecto al objeto complementario de la clase de caso. Es decir, cómo se reproduce exactamente el objeto acompañante implícito generado por el compilador que ocurre cuando solo se define la clase de caso en sí. Para mí, resultó ser contrario a la intuición.


Resumen:
Puede alterar el valor de un parámetro de clase de caso antes de que se almacene en la clase de caso de manera bastante simple mientras sigue siendo un ADT (tipo de datos abstracto) válido (ated). Si bien la solución fue relativamente simple, descubrir los detalles fue un poco más desafiante.

Detalles:
si desea asegurarse de que solo se puedan instanciar instancias válidas de su clase de caso, lo cual es una suposición esencial detrás de un ADT (tipo de datos abstracto), hay una serie de cosas que debe hacer.

Por ejemplo, un copymétodo generado por el compilador se proporciona de forma predeterminada en una clase de caso. Por lo tanto, incluso si tuvo mucho cuidado de asegurarse de que solo se crearan instancias a través del applymétodo del objeto complementario explícito que garantizaba que solo podrían contener valores en mayúsculas, el siguiente código produciría una instancia de clase de caso con un valor en minúscula:

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

Además, las clases de casos se implementan java.io.Serializable. Esto significa que su cuidadosa estrategia de tener solo instancias en mayúsculas se puede alterar con un simple editor de texto y deserialización.

Por lo tanto, para todas las diversas formas en que se puede usar su clase de caso (con benevolencia y / o maldad), estas son las acciones que debe tomar:

  1. Para su objeto complementario explícito:
    1. Créelo usando exactamente el mismo nombre que su clase de caso
      • Esto tiene acceso a las partes privadas de la clase de caso.
    2. Cree un applymétodo con exactamente la misma firma que el constructor principal para su clase de caso
      • Esto se compilará con éxito una vez que se complete el paso 2.1
    3. Proporcionar una implementación obteniendo una instancia de la clase de caso usando el newoperador y proporcionando una implementación vacía{}
      • Esto ahora instanciará la clase de caso estrictamente en sus términos
      • Se {}debe proporcionar la implementación vacía porque se declara la clase de caso abstract(consulte el paso 2.1)
  2. Para su clase de caso:
    1. Declararlo abstract
      • Evita que el compilador de Scala genere un applymétodo en el objeto complementario que es lo que estaba causando el error de compilación "el método se define dos veces ..." (paso 1.2 anterior)
    2. Marque el constructor principal como private[A]
      • El constructor principal ahora solo está disponible para la clase de caso en sí y para su objeto complementario (el que definimos anteriormente en el paso 1.1)
    3. Crea un readResolvemétodo
      1. Proporcione una implementación utilizando el método de aplicación (paso 1.2 anterior)
    4. Crea un copymétodo
      1. Defínalo para que tenga exactamente la misma firma que el constructor principal de la clase de caso
      2. Para cada parámetro, añadir un valor predeterminado utilizando el mismo nombre de parámetro (ex: s: String = s)
      3. Proporcione una implementación utilizando el método de aplicación (paso 1.2 a continuación)

Aquí está su código modificado con las acciones anteriores:

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

Y aquí está su código después de implementar el requisito (sugerido en la respuesta de @ollekullberg) y también identificar el lugar ideal para colocar cualquier tipo de almacenamiento en caché:

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

Y esta versión es más segura / robusta si este código se usará a través de la interoperabilidad de Java (oculta la clase de caso como una implementación y crea una clase final que evita derivaciones):

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

Si bien esto responde directamente a su pregunta, hay incluso más formas de expandir esta vía en torno a las clases de casos más allá del almacenamiento en caché de instancias. Para las necesidades de mi propio proyecto, he creado una solución aún más amplia que he documentado en CodeReview (un sitio hermano de StackOverflow). Si termina revisándolo, usando o aprovechando mi solución, considere dejarme comentarios, sugerencias o preguntas y, dentro de lo razonable, haré todo lo posible para responder dentro de un día.

equilibrio caótico
fuente
Acabo de publicar una solución expansiva más nueva para ser más idiomático de Scala e incluir el uso de ScalaCache para almacenar en caché fácilmente instancias de clases de casos (no se me permitió editar la respuesta existente según las meta reglas): codereview.stackexchange.com/a/98367/4758
chaotic3quilibrium
gracias por esta explicación detallada. Pero estoy luchando por entender por qué se requiere la implementación de readResolve. Porque la compilación también funciona sin la implementación de readResolve.
mogli
publicó una pregunta separada: stackoverflow.com/questions/32236594/…
mogli
12

No sé cómo anular el applymétodo en el objeto complementario (si eso es posible), pero también puede usar un tipo especial para cadenas en mayúsculas:

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

Los resultados del código anterior:

A(HELLO)

También debería echar un vistazo a esta pregunta y sus respuestas: Scala: ¿es posible anular el constructor de clases de caso predeterminado?

Frank S. Thomas
fuente
Gracias por eso, estaba pensando en lo mismo pero no sabía nada Proxy. Aunque podría ser mejor hacerlo s.toUpperCase una vez .
Ben Jackson
@Ben No veo dónde toUpperCasese llama más de una vez.
Frank S. Thomas
tienes toda la razón val self, no def self. Acabo de tener C ++ en el cerebro.
Ben Jackson
6

Para las personas que lean esto después de abril de 2017: A partir de Scala 2.12.2+, Scala permite anular aplicar y cancelar de forma predeterminada . Puede obtener este comportamiento dando la -Xsource:2.12opción al compilador en Scala 2.11.11+ también.

Mehmet Emre
fuente
1
¿Qué significa esto? ¿Cómo puedo aplicar este conocimiento a una solución? ¿Puede dar un ejemplo?
k0pernikus
Tenga en cuenta que unapply no se usa para clases de casos de coincidencia de patrones, lo que hace que anularlo sea bastante inútil (si escribe -Xprintuna matchdeclaración, verá que no se usa).
J Cracknell
5

Funciona con variables var:

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

Aparentemente, esta práctica se recomienda en clases de casos en lugar de definir otro constructor. Mira aquí. . Al copiar un objeto, también conserva las mismas modificaciones.

Mikaël Mayer
fuente
4

Otra idea, manteniendo la clase de caso y sin defs implícitas u otro constructor, es hacer que la firma sea applyligeramente diferente pero desde la perspectiva del usuario sea la misma. En algún lugar he visto el truco implícito, pero no puedo recordar / encontrar qué argumento implícito era, así que elegí Booleanaquí. Si alguien me puede ayudar y terminar el truco ...

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)
Peter Schmitz
fuente
En los sitios de llamada le dará un error de compilación (referencia ambigua a la definición sobrecargada). Sólo funciona si los tipos de scala son diferentes pero iguales después del borrado, por ejemplo, para tener dos funciones diferentes para una Lista [Int] y una Lista [Cadena].
Mikaël Mayer
No pude hacer que esta vía de solución funcione (con 2.11). Finalmente descubrí por qué no podía proporcionar su propio método de aplicación en el objeto compañero explícito. Lo he detallado en la respuesta que acabo de publicar: stackoverflow.com/a/25538287/501113
chaotic3quilibrium
3

Enfrenté el mismo problema y esta solución está bien para mí:

sealed trait A {
  def s:String
}

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

Y, si se necesita algún método, simplemente defínalo en el rasgo y anótelo en la clase case.

Pere Ramírez
fuente
0

Si está atascado con una scala anterior donde no puede anular de forma predeterminada o no desea agregar la marca del compilador como se mostró @ mehmet-emre, y necesita una clase de caso, puede hacer lo siguiente:

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

A partir de 2020 en Scala 2.13, el escenario anterior de anular un método de aplicación de clase de caso con la misma firma funciona totalmente bien.

case class A(val s: String)

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

el fragmento de código anterior se compila y se ejecuta bien en Scala 2.13 tanto en modo REPL como en modo no REPL.

Gana
fuente
-2

Creo que esto funciona exactamente como ya lo quieres. Aquí está mi sesión de 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)

Esto está usando Scala 2.8.1.final

mattrjacobs
fuente
3
No funciona aquí si pongo el código en un archivo e intento compilarlo.
Frank S. Thomas
Creo que sugerí algo similar en una respuesta anterior y alguien dijo que solo funciona en la respuesta debido a la forma en que funciona la respuesta.
Ben Jackson
5
El REPL esencialmente crea un nuevo alcance con cada línea, dentro de la anterior. Es por eso que algunas cosas no funcionan como se esperaba cuando se pegan desde REPL en su código. Por lo tanto, siempre revise ambos.
gregturn
1
La forma correcta de probar el código anterior (que no funciona) es usar: pegar el REPL para asegurarse de que tanto el caso como el objeto estén definidos juntos.
StephenBoesch