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

Entonces esta es la situación. Quiero definir una clase de caso así:

case class A(val s: String) 

y quiero definir un objeto para garantizar que cuando creo instancias de la clase, el valor para ‘s’ siempre sea mayúscula, 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) está definido dos veces. Entiendo que la syntax de la clase de caso lo definirá automáticamente para mí, pero ¿no hay otra forma en que pueda lograrlo? Me gustaría seguir con la clase de caso, ya que quiero usarlo para la coincidencia de patrones.

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

En primer lugar, me gustaría sugerir que el uso requiera:

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

Esto lanzará una Excepción si el usuario intenta crear una instancia donde s incluya caracteres en minúsculas. Este es un buen uso de las clases de casos, ya que lo que pone en el constructor también es lo que obtiene cuando usa la coincidencia de patrones ( match ).

Si esto no es lo que quieres, haré que el constructor sea private y forzaré a los usuarios a usar solo el método apply:

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

Como ve, A ya no es una case class . No estoy seguro de si las clases de caso con campos inmutables están destinadas a la modificación de los valores entrantes, ya que el nombre “clase de caso” implica que debería ser posible extraer los argumentos del constructor (no modificado) usando match .

ACTUALIZACIÓN 25/02/2016:
Si bien la respuesta que escribí a continuación sigue siendo suficiente, también vale la pena hacer referencia a otra respuesta relacionada a esto en relación con el objeto complementario de la clase de caso. Concretamente, ¿cómo se reproduce exactamente el objeto complementario implícito generado por el comstackdor que se produce cuando uno solo define la clase de caso en sí misma? 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 siga siendo un ADT válido (ated) (Tipo de datos abstractos). 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 varias cosas que debe hacer.

Por ejemplo, un método de copy generada por el comstackdor se proporciona por defecto en una clase de caso. Por lo tanto, incluso si fue muy cuidadoso para garantizar que solo se crearan instancias mediante el método de apply explícito del objeto complementario, que garantizaba que solo podrían contener valores en mayúscula, 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 implementan java.io.Serializable . Esto significa que su estrategia cuidadosa para solo tener instancias en mayúsculas se puede subvertir con un editor de texto simple y deserialización.

Entonces, para todas las formas en que se puede usar su clase de caso (benévola y / o malevolentemente), aquí están 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 método de apply con exactamente la misma firma que el constructor principal para su clase de caso
      • Esto se comstackrá exitosamente una vez que se complete el paso 2.1
    3. Proporcione una implementación que obtenga una instancia de la clase de caso utilizando el new operador y proporcionando una implementación vacía {}
      • Esto ahora instanciará la clase de caso estrictamente en sus términos
      • Debe proporcionarse la implementación vacía {} porque la clase de caso se declara abstract (consulte el paso 2.1)
  2. Para su clase de caso:
    1. Declararlo abstract
      • Impide que el comstackdor de Scala genere un método de apply en el objeto complementario, que es lo que estaba causando el error de comstackción “el método está definido dos veces …” (paso 1.2 anterior)
    2. Marcar el constructor principal como private[A]
      • El constructor primario ahora solo está disponible para la clase de caso y para su objeto complementario (el que definimos anteriormente en el paso 1.1)
    3. Crear un método readResolve
      1. Proporcione una implementación utilizando el método apply (paso 1.2 anterior)
    4. Crear un método de copy
      1. Defina que tenga exactamente la misma firma que el constructor principal de la clase de caso
      2. Para cada parámetro, agregue un valor predeterminado con el mismo nombre de parámetro (por ejemplo: s: String = s )
      3. Proporcione una implementación usando el método de aplicar (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 requerimiento (sugerido en la respuesta @ollekullberg) y también identificando 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 las 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, existen aún más formas de expandir esta vía alrededor de las clases de casos más allá del almacenamiento en caché de instancias. Para mis propias necesidades de proyecto, he creado una solución aún más expansiva que he documentado en CodeReview (un sitio hermano de StackOverflow). Si termina revisándolo, utilizando 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.

No sé cómo anular el método de apply en el objeto complementario (si eso es posible) pero también podría 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")) 

Las salidas de código anteriores:

 A(HELLO) 

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

Para las personas que lean esto después de abril de 2017: a partir de Scala 2.12.2+, Scala permite anular la aplicación y la desactivación por defecto . Puede obtener este comportamiento dando -Xsource:2.12 opción -Xsource:2.12 al comstackdor en Scala 2.11.11+.

Otra idea al mantener la clase de caso y no tener defs implícitos u otro constructor es hacer que la firma de la apply ligeramente diferente, pero desde la perspectiva del usuario es 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í Boolean aquí. Si alguien puede ayudarme y terminar el truco …

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

Funciona con var variables:

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

Esta práctica aparentemente se fomenta en las clases de casos en lugar de definir otro constructor. Mira aquí. . Al copiar un objeto, también conservas las mismas modificaciones.

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ótalo en la clase de caso.

Si está atascado con una scala más antigua en la que no puede sobrescribir de forma predeterminada o si no desea agregar la bandera del comstackdor como @mehmet-emre mostró, y necesita una clase de caso, puede hacer lo siguiente:

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

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