lunes, 11 de diciembre de 2017

Un resumen de Scala for the Impatient, parte 33

Self type


Cuando un trait extiende de una clase, hay una garantía de que la superclase está presente en cualquier clase que se mezcle con el trait. Scala tiene un mecanismo alternativo para garantizar esto,  se denomina self type.

Cuando un trait comienza con :

this: Type =>

entonces solo se puede mezclar en una subclase del tipo dado.

trait LoggedException extends ConsoleLogger {
    this: Exception =>
    def log() { log(getMessage()) }
}

Note que el trait no extiende de Exception, en cambio, tiene un tipo propio Excepción. Eso significa que solo se puede mezclar en subclases de Excepción.

En los métodos del rasgo, podemos llamar a cualquier método del self type. Por ejemplo, la llamada a getMessage() en el método de registro es válida, ya que sabemos que debe ser un trait extendido por una excepción.

Como se puede suponer si una clase que no sea una Excepción quiere utilizar LoggedException eso no va a compilar.

También se puede utilizar type self con un método determinado, sin especificar una clase.

trait LoggedException extends ConsoleLogger {
    this: { def getMessage() : String } =>
        def log() { log(getMessage()) }
}

Por lo tanto este trait podrá mezclarse con cualquier clase que implemente getMessage() .

Que pasa en la jvm?

Scala necesita pasar un trait a una clase o interfaz java para que esto pueda ser comprendido por la JVM. Y es muy útil entender como trabajan los traits.

Un trait con todos los métodos abstractos es convertido a una interfaz. Un trait con un método es como una interfaz con un método por defecto:

trait ConsoleLogger {
    def log(msg: String) { println(msg) }
}

Se convierte:

public interface ConsoleLogger {
    default void log(String msg) { ... }
}

Si un trait tiene campos, es transformado a una interfaz con los métodos getters y setters:

trait ShortLogger extends Logger {
    val maxLength = 15 // A concrete field
    ...
}

es transformado a:

public interface ShortLogger extends Logger {
    int maxLength();
    void weird_prefix$maxLength_$eq(int);
    default void log(String msg) { ... } // Calls maxLength()
    default void $init$() { weird_prefix$maxLength_$eq(15); }
}

Por supuesto las interfaces en java no tienen campos, por lo que llama a los metodos getters o setters cuando quiera acceder o cambiar este valor. El setter es necesario tambien para inicializar el campo. Esto sucede en el método $init$.

Cuando el rasgo se mezcla en una clase, la clase obtiene un campo maxLength, y el getter y el setter se definen para obtener y establecer el campo. Los constructores de esa clase invocan el método $init$ del trait. Por ejemplo:

class SavingsAccount extends Account with ConsoleLogger with ShortLogger

seria en java:

public class SavingsAccount extends Account
 implements ConsoleLogger, ShortLogger {
   private int maxLength;
   public int maxLength() { return maxLength; }
   public void weird_prefix$maxLength_$eq(int arg) { maxLength = arg; }
 
   public SavingsAccount() {
      super();
      ConsoleLogger.$init$();
      ShortLogger.$init$();
   }
...
}