Tipos en la especificación de Scala 2.11.x

Tipos

  Tipo             ::=  ArgumentosFun '=>' Tipo
                     |  TipoInfixo [CláusulaExistencial]
  ArgumentosFun    ::=  TipoInfixo
                     |  '(' [ TipoParam {',' TipoParam } ] ')'
  CláusulaExistencial ::=  'forSome' '{' DeclaraciónExistencial
                               {puntoYComa DeclaraciónExistencial} '}'
  DeclaraciónExistencial    ::=  'type' DeclaraciónTipo
                              |  'val' DeclaraciónValor
  TipoInfixo       ::=  TipoCompuesto {id [nl] TipoCompuesto}
  TipoCompuesto    ::=  TipoAnotado {'with' TipoAnotado} [Refinamiento]
                     |  Refinamiento
  TipoAnotado      ::=  TipoSimple {Anotación}
  TipoSimple       ::=  TipoSimple ArgumentosTipo
                     |  TipoSimple '#' id
                     |  IdEstable
                     |  Ruta '.' 'type'
                     |  '(' Tipos ')'
  ArgumentosTipo   ::=  '[' Tipos ']'
  Tipos            ::=  Tipo {',' Tipo}

Distinguimos entre tipos de primer orden y constructores de tipo, que reciben parámetros de tipo y producen tipos. Un subconjunto de tipos de primer orden llamado tipos de valor representa conjuntos de valores de primera clase. Los tipos de valor pueden ser concretos o abstractos.

Cada tipo de valor concreto puede representarse como un tipo de clase, es decir, un designador de tipo que apunta a una clase o trait[1], o como un tipo compuesto que representa una intersección de tipos, posiblemente con un refinamiento que restrinja aún más los tipos de sus miembros.

Los tipos de valor abstractos se introducen mediante parámetros de tipo y enlaces de tipo abstracto. Los paréntesis en los tipos se pueden usar para agrupación.

Los tipos que no son de valor capturan propiedades de identificadores que no son valores. Por ejemplo, un constructor de tipo no especifica directamente un tipo de valores. Sin embargo, cuando un constructor de tipo se aplica a los argumentos de tipo correctos, produce un tipo de primer orden, que puede ser un tipo de valor.

Los tipos que no son de valor se expresan indirectamente en Scala. Por ejemplo, un tipo de método se describe escribiendo una firma de método, que en sí misma no es un tipo real, aunque da lugar a un tipo de método correspondiente. Los constructores de tipo son otro ejemplo, ya que uno puede escribir type Intercambio[m[_, _], a,b] = m[b, a], pero no hay sintaxis para escribir la función de tipo anónima correspondiente directamente.

Rutas

Ruta            ::=  IdEstable
                  |  [id '.'] this
IdEstable       ::=  id
                  |  Ruta '.' id
                  |  [id '.'] 'super' [CalificadorClase] '.' id
CalificadorClase  ::= '[' id ']'

Las rutas no son tipos en sí mismas, pero pueden formar parte de tipos con nombre y en esa función desempeñan un papel central en el sistema de tipos de Scala.

Una ruta es una de las siguientes:

  • La ruta vacía ε (que no se puede escribir explícitamente en programas de usuario).
  • C.this, donde C hace referencia a una clase. La ruta this se toma como abreviatura de C.this donde C es el nombre de la clase que encierra directamente la referencia.
  • p.x donde p es una ruta y x es un miembro estable de p. Miembros estables son paquetes o miembros introducidos por definiciones de objeto o por definiciones de valor de tipos no volátiles.
  • C.super.x o C.super[M].x donde C hace referencia a una clase y x hace referencia a un miembro estable de la superclase o la clase padre designada M de C. El prefijo super se toma como abreviatura de C.super donde C es el nombre de la clase que encierra directamente la referencia.

Un identificador estable es una ruta que termina en un identificador.

Tipos de valor

Cada valor en Scala tiene un tipo que es de una de las siguientes formas.

Tipos singleton

TipoSimple  ::=  Ruta '.' type

Un tipo singleton tiene la forma p.type, donde p es una ruta que apunta a un valor que se espera cumpla con scala.AnyRef. El tipo denota el conjunto de valores que consiste en null y el valor denotado por p.

Un tipo estable es un tipo singleton o un tipo que se declara como subtipo del trait scala.Singleton.

Proyección de tipo

TipoSimple  ::=  TipoSimple '#' id

Una proyección de tipo T#x hace referencia al miembro de tipo llamado x del tipo T.

Designadores de tipo

TipoSimple  ::=  IdEstable

Un designador de tipo hace referencia a un tipo de valor con nombre. Puede ser simple o calificado. Todos estos designadores de tipo son abreviaturas de proyecciones de tipo.

Específicamente, el nombre de tipo no calificado t donde t está enlazado en alguna clase, objeto o paquete C se toma como abreviatura de C.this.type#t. Si t no está enlazado en una clase, objeto o paquete, entonces t se toma como abreviatura de ε.type#t.

Un designador de tipo calificado tiene la forma p.t donde p es una ruta y t es un nombre de tipo. Tal designador de tipo es equivalente a la proyección de tipo p.type#t.

Ejemplo

Algunos designadores de tipo y sus expansiones se listan a continuación. Asumimos un parámetro de tipo local t, un valor tablaPrincipal con un miembro de tipo Nodo y la clase estándar scala.Int.

Designador Expansión
t ε.type#t
Int scala.type#Int
scala.Int scala.type#Int
datos.tablaPrincipal.Nodo datos.tablaPrincipal.type#Nodo

Tipos parametrizados

TipoSimple      ::=  TipoSimple ArgumentosTipo
ArgumentosTipo  ::=  '[' Tipos ']'

Un tipo parametrizado T[T₁, ..., Tₙ] consiste en un designador de tipo T y parámetros de tipo T₁, ..., Tₙ donde n ≥ 1. T debe hacer referencia a un constructor de tipo que tome n parámetros de tipo a₁, ..., aₙ.

Supongamos que los parámetros de tipo tienen límites inferiores L₁, ..., Lₙ y límites superiores U₁, ..., Uₙ. El tipo parametrizado está bien formado si cada parámetro de tipo actual se ajusta a sus límites, es decir, σLᵢ <: Tᵢ <: σUᵢ donde σ es la sustitución [a₁ := T₁, ..., aₙ := Tₙ].

Ejemplo de tipos parametrizados

Dadas las definiciones de tipo parciales:

class ArbolMapa[A <: Comparable[A], B] { ... }
class Lista[A] { ... }
class I extends Comparable[I] { ... }

class F[M[_], X] { ... }
class S[K <: String] { ... }
class G[M[Z <: I], I] { ... }

Los siguientes tipos parametrizados están bien formados:

ArbolMapa[I, String]
Lista[I]
Lista[List[Boolean]]

F[List, Int]
G[S, String]

Ejemplo

Dadas las definiciones de tipo anteriores, los siguientes tipos están mal formados:

ArbolMapa[I]            // ilegal: número incorrecto de parámetros
ArbolMapa[List[I], Int] // ilegal: el parámetro de tipo no está dentro del límite

F[Int, Boolean]       // ilegal: Int no es un constructor de tipo
F[ArbolMapa, Int]     // ilegal: ArbolMapa toma dos parámetros,
                      //   F espera un constructor que tome uno
G[S, Int]             // ilegal: S restringe su parámetro para
                      //   que se ajuste a String,
                      //   G espera un constructor de tipo con un parámetro
                      //   que se ajuste a Int

Tipos de tupla

TipoSimple    ::=   '(' Tipos ')'

Un tipo de tupla (T₁, ..., Tₙ) es un alias de la clase scala.Tuple$n$[T₁, ..., Tₙ], donde n ≥ 2.

Las clases de tupla son clases case cuyos campos se pueden acceder usando selectores _1, ..., _n. Su funcionalidad se abstrae en un trait Product correspondiente. La clase de tupla n-aria y el trait de producto se definen al menos de la siguiente manera en la biblioteca estándar de Scala (también podrían añadir otros métodos e implementar otros traits).

case class Tuple$n$[+$T₁$, ..., +$Tₙ$](_1: $T₁$, ..., _n: $Tₙ$)
extends Product_n[$T₁$, ..., $Tₙ$]

trait Product_n[+$T₁$, ..., +$Tₙ$] {
  override def productArity = $n$
  def _1: $T₁$
  ...
  def _n: $Tₙ$
}

Tipos anotados

TipoAnotado  ::=  TipoSimple {Anotación}

Un tipo anotado T a₁, ..., aₙ adjunta anotaciones a₁, ..., aₙ al tipo T.

Ejemplo

El siguiente tipo añade la anotación @suspendable al tipo String:

String @suspendable

Tipos compuestos

TipoCompuesto    ::=  TipoAnotado {'with' TipoAnotado} [Refinamiento]
                  |  Refinamiento
Refinamiento     ::=  [nl] '{' EstadRefinamiento {puntoYComa EstadRefinamiento} '}'
EstadRefinamiento ::=  Declaración
                   |  'type' DefTipo
                   |

Un tipo compuesto T₁ with ... with Tₙ { R } representa objetos con miembros como se indica en los tipos de componente T₁, ..., Tₙ y el refinamiento { R }. Un refinamiento { R } contiene declaraciones y definiciones de tipo. Si una declaración o definición anula una declaración o definición en uno de los tipos de componente T₁, ..., Tₙ, se aplican las reglas habituales para la anulación; de lo contrario, la declaración o definición se denomina "estructural"[2].

Dentro de una declaración de método en un refinamiento estructural, el tipo de cualquier parámetro de valor solo puede hacer referencia a parámetros de tipo o tipos abstractos que estén contenidos dentro del refinamiento. Es decir, debe hacer referencia a un parámetro de tipo del propio método o a una definición de tipo dentro del refinamiento. Esta restricción no se aplica al tipo de resultado del método.

Si no se proporciona refinamiento, se añade implícitamente un refinamiento vacío, es decir, T₁ with ... with Tₙ es una abreviatura de T₁ with ... with Tₙ {}.

Un tipo compuesto también puede consistir solo en un refinamiento { R } sin tipos de componente precedentes. Tal tipo es equivalente a AnyRef { R }.

Ejemplo

El siguiente ejemplo muestra cómo declarar y usar un método que tiene un tipo de parámetro que contiene un refinamiento con declaraciones estructurales.

case class Pajaro(val nombre: String) extends Object {
        def volar(altura: Int) = ...
...
}
case class Avion(matricula: String) extends Object {
        def volar(altura: Int) = ...
...
}
def despegar(
            pista: Int,
      r: { val matricula: String; def volar(altura: Int) }) = {
  torre.imprimir(r.matricula + " solicita despegar en pista " + pista)
  torre.leer(r.matricula + " tiene autorización para despegar")
  r.volar(1000)
}
val pajaro = new Pajaro("Polly el loro"){ val matricula = nombre }
val a380 = new Avion("TZ-987")
despegar(42, pajaro)
despegar(89, a380)

Aunque Pajaro y Avion no comparten ninguna clase padre distinta de Object, el parámetro r del método despegar se define usando un refinamiento con declaraciones estructurales para aceptar cualquier objeto que declare un valor matricula y un método volar.

Tipos infijos

TipoInfixo     ::=  TipoCompuesto {id [nl] TipoCompuesto}

Un tipo infijo T₁ op T₂ consiste en un operador infijo op que se aplica a dos operandos de tipo T₁ y T₂. El tipo es equivalente a la aplicación de tipo op[T₁, T₂]. El operador infijo op puede ser un identificador arbitrario.

Todos los operadores infijos de tipo tienen la misma precedencia; se deben usar paréntesis para agrupar. La asociatividad de un operador de tipo se determina como para los operadores de término: los operadores de tipo que terminan en dos puntos ':' son asociativos por la derecha; todos los demás operadores son asociativos por la izquierda.

En una secuencia de operaciones infijas de tipo consecutivas t₀ op₁ t₁ op₂ ... opₙ tₙ, todos los operadores op₁, ..., opₙ deben tener la misma asociatividad. Si todos son asociativos por la izquierda, la secuencia se interpreta como ((t₀ op₁ t₁) op₂ ...) opₙ tₙ, de lo contrario se interpreta como t₀ op₁ (t₁ op₂ (... opₙ tₙ) ...).

Tipos de función

Tipo              ::=  ArgumentosFun '=>' Tipo
ArgumentosFun     ::=  TipoInfixo
                    |  '(' [ TipoParam {',' TipoParam } ] ')'

El tipo (T₁, ..., Tₙ) => U representa el conjunto de valores de función que toman argumentos de tipos T₁, ..., Tₙ y producen resultados de tipo U. En el caso de exactamente un tipo de argumento, T => U es una abreviatura de (T) => U. Un tipo de argumento de la forma => T representa un parámetro por nombre de tipo T.

Los tipos de función asocian a la derecha, por ejemplo, S => T => U es lo mismo que S => (T => U).

Los tipos de función son abreviaturas de tipos de clase que definen funciones apply. Específicamente, el tipo de función n-ario (T₁, ..., Tₙ) => U es una abreviatura del tipo de clase Function$n$[T₁, ..., Tₙ, U]. Tales tipos de clase se definen en la biblioteca de Scala para n entre 0 y 9 de la siguiente manera.

package scala
trait Function_n[-T₁, ..., -Tₙ, +R] {
  def apply(x₁: T₁, ..., xₙ: Tₙ): R
  override def toString = "<función>"
}

Por lo tanto, los tipos de función son covariantes en su tipo de resultado y contravariantes en sus tipos de argumento.

Tipos existenciales

Tipo               ::= TipoInfixo CláusulasExistenciales
CláusulasExistenciales ::= 'forSome' '{' DeclaraciónExistencial
                       {puntoYComa DeclaraciónExistencial} '}'
DeclaraciónExistencial     ::= 'type' DeclaraciónTipo
                           |  'val' DeclaraciónValor

Un tipo existencial tiene la forma $T$ forSome { $Q$ } donde Q es una secuencia de declaraciones de tipo.

Sea t₁[tps₁] >: L₁ <: U₁, ..., tₙ[tpsₙ] >: Lₙ <: Uₙ los tipos declarados en Q (cualquiera de las secciones de parámetros de tipo [tpsᵢ] podría faltar). El alcance de cada tipo tᵢ incluye el tipo T y la cláusula existencial Q. Las variables de tipo tᵢ se dicen que están ligadas en el tipo $T$ forSome { $Q$ }. Las variables de tipo que aparecen en un tipo T pero que no están ligadas en T se dice que están libres en T.

Una instancia de tipo de $T$ forSome { $Q$ } es un tipo σT donde σ es una sustitución sobre t₁, ..., tₙ tal que, para cada i, σLᵢ <: σtᵢ <: σUᵢ. El conjunto de valores denotados por el tipo existencial $T$ forSome { $Q$ } es la unión del conjunto de valores de todas sus instancias de tipo.

Una skolemización de $T$ forSome { $Q$ } es una instancia de tipo σT, donde σ es la sustitución [t₁'/t₁, ..., tₙ'/tₙ] y cada tᵢ' es un tipo abstracto fresco con límite inferior σLᵢ y límite superior σUᵢ.

Reglas de simplificación

Los tipos existenciales obedecen las siguientes cuatro equivalencias:

  1. Múltiples cláusulas for en un tipo existencial se pueden fusionar. Por ejemplo, $T$ forSome { $Q$ } forSome { $Q'$ } es equivalente a $T$ forSome { $Q$ ; $Q'$ }.
  2. Las cuantificaciones no usadas se pueden eliminar. Por ejemplo, $T$ forSome { $Q$ ; $Q'$ } donde ninguno de los tipos definidos en Q' es referenciado por T o Q, es equivalente a $T$ forSome { $Q$ }.
  3. Una cuantificación vacía se puede eliminar. Por ejemplo, $T$ forSome { } es equivalente a T.
  4. Un tipo existencial $T$ forSome { $Q$ } donde Q contiene una cláusula type $t[tps] >: L <: U$ es equivalente al tipo $T'$ forSome { $Q$ } donde T' resulta de T reemplazando cada aparición covariante de t en T por U y reemplazando cada aparición contravariante de t en T por L.
Cuantificación existencial sobre valores

Como conveniencia sintáctica, la cláusula de enlaces en un tipo existencial también puede contener declaraciones de valor val $x$: $T$. Un tipo existencial $T$ forSome { $Q$; val $x$: $S$; $Q'$ } se trata como una abreviatura del tipo $T'$ forSome { $Q$; type $t$ <: $S$ with Singleton; $Q'$ }, donde t es un nombre de tipo fresco y T' resulta de T reemplazando cada aparición de $x$.type por t.

Sintaxis de comodín para tipos existenciales
TipoComodín   ::=  '_' LímitesTipo

Scala admite una sintaxis de comodín para tipos existenciales. Un tipo comodín tiene la forma _ >: L <: U. Ambas cláusulas de límite pueden omitirse. Si falta una cláusula de límite inferior >: L, se asume >: scala.Nothing. Si falta una cláusula de límite superior <: U, se asume <: scala.Any. Un tipo comodín es una abreviatura de una variable de tipo cuantificada existencialmente, donde la cuantificación existencial es implícita.

Un tipo comodín debe aparecer como argumento de tipo de un tipo parametrizado. Sea T = p.c[targs, T, targs'] un tipo parametrizado donde targs, targs' pueden estar vacíos y T es un tipo comodín _ >: L <: U. Entonces T es equivalente al tipo existencial $p.c[targs, t, targs']$ forSome { type $t$ >: $L$ <: $U$ } donde t es alguna variable de tipo fresca. Los tipos comodín también pueden aparecer como partes de tipos infijos, tipos de función o tipos de tupla. Su expansión es entonces la expansión en el tipo parametrizado equivalente.

Ejemplo

Supongamos las definiciones de clase

class Ref[T]
abstract class Exterior { type T } .

Aquí hay algunos ejemplos de tipos existenciales:

Ref[T] forSome { type T <: java.lang.Number }
Ref[x.T] forSome { val x: Exterior }
Ref[x_type # T] forSome { type x_type <: Exterior with Singleton }

Los dos últimos tipos de esta lista son equivalentes. Una formulación alternativa del primer tipo anterior usando sintaxis de comodín es:

Ref[_ <: java.lang.Number]

Ejemplo

El tipo List[List[_]] es equivalente al tipo existencial

List[List[t] forSome { type t }] .

Ejemplo

Supongamos un tipo covariante

class List[+T]

El tipo

List[T] forSome { type T <: java.lang.Number }

es equivalente (por la regla de simplificación 4 anterior) a

List[java.lang.Number] forSome { type T <: java.lang.Number }

que a su vez es equivalente (por las reglas de simplificación 2 y 3 anteriores) a List[java.lang.Number].

Tipos que no son de valor

Los tipos que se explican a continuación no denotan conjuntos de valores, ni aparecen explícitamente en programas. Se introducen en este informe como los tipos internos de identificadores definidos.

Tipos de método

Un tipo de método se denota internamente como (Ps)U, donde (Ps) es una secuencia de nombres de parámetros y tipos (p₁:T₁, ..., pₙ:Tₙ) para algún n ≥ 0 y U es un tipo (de valor o de método). Este tipo representa métodos con nombre que toman argumentos llamados p₁, ..., pₙ de tipos T₁, ..., Tₙ y que devuelven un resultado de tipo U.

Los tipos de método asocian a la derecha: (Ps₁)(Ps₂)U se trata como (Ps₁)((Ps₂)U).

Un caso especial son los tipos de métodos sin ningún parámetro. Se escriben aquí como => T. Los métodos sin parámetros nombran expresiones que se reevalúan cada vez que se hace referencia al nombre del método sin parámetros.

Los tipos de método no existen como tipos de valores. Si se usa un nombre de método como valor, su tipo se convierte implícitamente en un tipo de función correspondiente.

Ejemplo

Las declaraciones

def a: Int
def b(x: Int): Boolean
def c(x: Int)(y: String, z: String): String

producen las siguientes asignaciones de tipo:

a: => Int
b: (Int) Boolean
c: (Int) (String, String) String

Tipos de método polimórfico

Un tipo de método polimórfico se denota internamente como [tps]T donde [tps] es una sección de parámetros de tipo [a₁ >: L₁ <: U₁, ..., aₙ >: Lₙ <: Uₙ] para algún n ≥ 0 y T es un tipo (de valor o de método). Este tipo representa métodos con nombre que toman argumentos de tipo S₁, ..., Sₙ que se ajustan a los límites inferiores L₁, ..., Lₙ y a los límites superiores U₁, ..., Uₙ y que producen resultados de tipo T.

Ejemplo

Las declaraciones

def vacio[A]: List[A]
def union[A <: Comparable[A]](x: Set[A], xs: Set[A]): Set[A]

producen las siguientes asignaciones de tipo:

vacio : [A >: Nothing <: Any] List[A]
union : [A >: Nothing <: Comparable[A]] (x: Set[A], xs: Set[A]) Set[A]

Constructores de tipo

Un constructor de tipo se representa internamente de manera similar a un tipo de método polimórfico. [±a₁ >: L₁ <: U₁, ..., ±aₙ >: Lₙ <: Uₙ] T representa un tipo que se espera en un parámetro de constructor de tipo o en un enlace de constructor de tipo abstracto con la cláusula de parámetros de tipo correspondiente.

Ejemplo

Considere este fragmento de la clase Iterable[+X]:

trait Iterable[+X] {
  def flatMap[nuevoTipo[+X] <: Iterable[X], S](f: X => nuevoTipo[S]): nuevoTipo[S]
}

Conceptualmente, el constructor de tipo Iterable es un nombre para el tipo anónimo [+X] Iterable[X], que puede pasarse al parámetro de constructor de tipo nuevoTipo en flatMap.

Tipos base y definiciones de miembros

Los tipos de los miembros de una clase dependen de la manera en que se hace referencia a los miembros. Centrales aquí son tres nociones, a saber:

  1. la noción del conjunto de tipos base de un tipo T,
  2. la noción de un tipo T en alguna clase C visto desde algún tipo de prefijo S,
  3. la noción del conjunto de enlaces de miembros de algún tipo T.

Estas nociones se definen de manera mutuamente recursiva de la siguiente manera.

  1. El conjunto de tipos base de un tipo es un conjunto de tipos de clase, dado de la siguiente manera:

    • Los tipos base de un tipo de clase C con padres T₁, ..., Tₙ son el propio C, así como los tipos base del tipo compuesto $T₁$ with ... with $Tₙ$ { $R$ }.
    • Los tipos base de un tipo con alias son los tipos base de su alias.
    • Los tipos base de un tipo abstracto son los tipos base de su límite superior.
    • Los tipos base de un tipo parametrizado $C$[$T₁, ..., Tₙ$] son los tipos base del tipo C, donde cada aparición de un parámetro de tipo aᵢ de C se ha reemplazado por el tipo de parámetro correspondiente Tᵢ.
    • Los tipos base de un tipo singleton $p$.type son los tipos base del tipo de p.
    • Los tipos base de un tipo compuesto $T₁$ with ... with $Tₙ$ { $R$ } son la unión reducida de las clases base de todos los Tᵢ's. Esto significa: Sea el multiconjunto 𝓢 la unión de multiconjuntos de los tipos base de todos los Tᵢ's. Si 𝓢 contiene varias instancias de tipo de la misma clase, digamos $S^i$#$C$[$T^i₁, ..., T^iₙ$] (i ∈ I), entonces todas esas instancias se reemplazan por una de ellas que se ajuste a todas las demás. Es un error si no existe tal instancia. Se deduce que la unión reducida, si existe, produce un conjunto de tipos de clase, donde diferentes tipos son instancias de diferentes clases.
    • Los tipos base de una selección de tipo $S$#$T$ se determinan de la siguiente manera. Si T es un alias o un tipo abstracto, se aplican las cláusulas anteriores. De lo contrario, T debe ser un tipo de clase (posiblemente parametrizado), que se define en alguna clase B. Entonces los tipos base de $S$#$T$ son los tipos base de T en B vistos desde el tipo de prefijo S.
    • Los tipos base de un tipo existencial $T$ forSome { $Q$ } son todos los tipos $S$ forSome { $Q$ } donde S es un tipo base de T.
  2. La noción de un tipo T en la clase C visto desde algún tipo de prefijo S tiene sentido solo si el tipo de prefijo S tiene una instancia de tipo de la clase C como un tipo base, digamos $S'$#$C$[$T₁, ..., Tₙ$]. Entonces se define de la siguiente manera:

    • Si $S$ = ε.type, entonces T en C visto desde S es el propio T.
    • De lo contrario, si S es un tipo existencial $S'$ forSome { $Q$ }, y T en C visto desde S' es T', entonces T en C visto desde S es $T'$ forSome { $Q$ }.
    • De lo contrario, si T es el i-ésimo parámetro de tipo de alguna clase D, entonces:
      • Si S tiene un tipo base $D$[$U₁, ..., Uₙ$], para algunos parámetros de tipo [$U₁, ..., Uₙ$], entonces T en C visto desde S es Uᵢ.
      • De lo contrario, si C se define en una clase C', entonces T en C visto desde S es lo mismo que T en C' visto desde S'.
      • De lo contrario, si C no se define en otra clase, entonces T en C visto desde S es el propio T.
    • De lo contrario, si T es el tipo singleton $D$.this.type para alguna clase D, entonces:
      • Si D es una subclase de C y S tiene una instancia de tipo de la clase D entre sus tipos base, entonces T en C visto desde S es S.
      • De lo contrario, si C se define en una clase C', entonces T en C visto desde S es lo mismo que T en C' visto desde S'.
      • De lo contrario, si C no se define en otra clase, entonces T en C visto desde S es el propio T.
    • Si T es algún otro tipo, entonces el mapeo descrito se realiza en todos sus componentes de tipo.

    Si T es un tipo de clase (posiblemente parametrizado), donde la clase de T se define en alguna otra clase D, y S es algún tipo de prefijo, entonces usamos "T visto desde S" como abreviatura de "T en D visto desde S".

  3. Los enlaces de miembros de un tipo T son:

    1. todos los enlaces d tales que existe una instancia de tipo de alguna clase C entre los tipos base de T y existe una definición o declaración d' en C tal que d resulta de d' reemplazando cada tipo T' en d' por T' en C visto desde T, y
    2. todos los enlaces del refinamiento del tipo, si tiene uno.

    La definición de una proyección de tipo S#T es el enlace de miembro dT del tipo T en S. En ese caso, también decimos que S#T está definido por dT.

Relaciones entre tipos

Definimos dos relaciones entre tipos.

Nombre Simbólicamente Interpretación
Equivalencia T ≡ U T y U son intercambiables en todos los contextos.
Conformidad T <: U El tipo T se ajusta al tipo U.

Equivalencia

La equivalencia (≡) entre tipos es la congruencia más pequeña[3] tal que se cumple lo siguiente:

  • Si t se define por un alias de tipo type $t$ = $T$, entonces t es equivalente a T.
  • Si una ruta p tiene un tipo singleton $q$.type, entonces $p$.type ≡ $q$.type.
  • Si O se define por una definición de objeto, y p es una ruta que consiste solo en selectores de paquete u objeto y termina en O, entonces $O$.this.type ≡ $p$.type.
  • Dos tipos compuestos son equivalentes si las secuencias de sus componentes son equivalentes por pares, y ocurren en el mismo orden, y sus refinamientos son equivalentes. Dos refinamientos son equivalentes si enlazan los mismos nombres y los modificadores, tipos y límites de cada entidad declarada son equivalentes en ambos refinamientos.
  • Dos tipos de método son equivalentes si:
    • ninguno es implícito, o ambos lo son[4];
    • tienen tipos de resultado equivalentes;
    • tienen el mismo número de parámetros; y
    • los parámetros correspondientes tienen tipos equivalentes. Nótese que los nombres de los parámetros no importan para la equivalencia de tipos de método.
  • Dos tipos de método polimórfico son equivalentes si tienen el mismo número de parámetros de tipo y, después de renombrar un conjunto de parámetros de tipo por otro, los tipos de resultado así como los límites inferiores y superiores de los parámetros de tipo correspondientes son equivalentes.
  • Dos tipos existenciales son equivalentes si tienen el mismo número de cuantificadores y, después de renombrar una lista de cuantificadores de tipo por otra, los tipos cuantificados así como los límites inferiores y superiores de los cuantificadores correspondientes son equivalentes.
  • Dos constructores de tipo son equivalentes si tienen el mismo número de parámetros de tipo y, después de renombrar una lista de parámetros de tipo por otra, los tipos de resultado así como las varianzas, límites inferiores y superiores de los parámetros de tipo correspondientes son equivalentes.

Conformidad

La relación de conformidad (<:) es la relación transitiva más pequeña que satisface las siguientes condiciones.

  • La conformidad incluye la equivalencia. Si T ≡ U entonces T <: U.
  • Para cada tipo de valor T, scala.Nothing <: T <: scala.Any.
  • Para cada constructor de tipo T (con cualquier número de parámetros de tipo), scala.Nothing <: T <: scala.Any.
  • Para cada tipo de clase T tal que T <: scala.AnyRef se tiene scala.Null <: T.
  • Una variable de tipo o un tipo abstracto t se ajusta a su límite superior y su límite inferior se ajusta a t.
  • Un tipo de clase o un tipo parametrizado se ajusta a cualquiera de sus tipos base.
  • Un tipo singleton $p$.type se ajusta al tipo de la ruta p.
  • Un tipo singleton $p$.type se ajusta al tipo scala.Singleton.
  • Una proyección de tipo $T$#$t$ se ajusta a $U$#$t$ si T se ajusta a U.
  • Un tipo parametrizado $T$[$T₁, ..., Tₙ$] se ajusta a $T$[$U₁, ..., Uₙ$] si se cumplen las siguientes tres condiciones para i ∈ {1, ..., n}:
    1. Si el i-ésimo parámetro de tipo de T se declara covariante, entonces Tᵢ <: Uᵢ.
    2. Si el i-ésimo parámetro de tipo de T se declara contravariante, entonces Uᵢ <: Tᵢ.
    3. Si el i-ésimo parámetro de tipo de T no se declara ni covariante ni contravariante, entonces Uᵢ ≡ Tᵢ.
  • Un tipo compuesto $T₁$ with ... with $Tₙ$ { $R$ } se ajusta a cada uno de sus tipos de componente Tᵢ.
  • Si T <: Uᵢ para i ∈ {1, ..., n} y para cada enlace d de un tipo o valor x en R existe un enlace de miembro de x en T que subsume d, entonces T se ajusta al tipo compuesto $U₁$ with ... with $Uₙ$ { $R$ }.
  • El tipo existencial $T$ forSome { $Q$ } se ajusta a U si su skolemización se ajusta a U.
  • El tipo T se ajusta al tipo existencial $U$ forSome { $Q$ } si T se ajusta a una de las instancias de tipo de $U$ forSome { $Q$ }.
  • Si Tᵢ ≡ Tᵢ' para i ∈ {1, ..., n} y U se ajusta a U', entonces el tipo de método (p₁:T₁, ..., pₙ:Tₙ) U se ajusta a (p₁':T₁', ..., pₙ':Tₙ') U'.
  • El tipo polimórfico [a₁ >: L₁ <: U₁, ..., aₙ >: Lₙ <: Uₙ] T se ajusta al tipo polimórfico [a₁ >: L₁' <: U₁', ..., aₙ >: Lₙ' <: Uₙ'] T' si, asumiendo L₁' <: a₁ <: U₁', ..., Lₙ' <: aₙ <: Uₙ' se tiene T <: T' y Lᵢ <: Lᵢ' y Uᵢ' <: Uᵢ para i ∈ {1, ..., n}.
  • Los constructores de tipo T y T' siguen una disciplina similar. Caracterizamos T y T' por sus cláusulas de parámetros de tipo [a₁, ..., aₙ] y [a₁', ..., aₙ'], donde un aᵢ o aᵢ' puede incluir una anotación de varianza, una cláusula de parámetros de tipo de orden superior y límites. Entonces, T se ajusta a T' si cualquier lista [t₁, ..., tₙ] -- con varianzas, límites y cláusulas de parámetros de tipo de orden superior declarados -- de argumentos de tipo válidos para T' es también una lista válida de argumentos de tipo para T y T[t₁, ..., tₙ] <: T'[t₁, ..., tₙ]. Nótese que esto implica que:
    • Los límites en aᵢ deben ser más débiles que los límites correspondientes declarados para aᵢ'.
    • La varianza de aᵢ debe coincidir con la varianza de aᵢ', donde la covarianza coincide con la covarianza, la contravarianza coincide con la contravarianza y cualquier varianza coincide con la invarianza.
    • Recursivamente, estas restricciones se aplican a las cláusulas de parámetros de tipo de orden superior correspondientes de aᵢ y aᵢ'.

Una declaración o definición en algún tipo compuesto o tipo de clase C subsume otra declaración del mismo nombre en algún tipo compuesto o tipo de clase C', si se cumple una de las siguientes condiciones.

  • Una declaración o definición de valor que define un nombre x con tipo T subsume una declaración de valor o método que define x con tipo T', siempre que T <: T'.
  • Una declaración o definición de método que define un nombre x con tipo T subsume una declaración de método que define x con tipo T', siempre que T <: T'.
  • Un alias de tipo type $t$[$T₁, ..., Tₙ$] = $T$ subsume un alias de tipo type $t$[$T₁, ..., Tₙ$] = $T'$ si T ≡ T'.
  • Una declaración de tipo type $t$[$T₁, ..., Tₙ$] >: $L$ <: $U$ subsume una declaración de tipo type $t$[$T₁, ..., Tₙ$] >: $L'$ <: $U'$ si L' <: L y U <: U'.
  • Una definición de tipo o clase que enlaza un nombre de tipo t subsume una declaración de tipo abstracto type t[$T₁, ..., Tₙ$] >: L <: U si L <: t <: U.

La relación (<:) forma un pre-orden entre tipos, es decir, es transitiva y reflexiva. Los límites superiores más pequeños y los límites inferiores más grandes de un conjunto de tipos se entienden relativos a ese orden.

Nota

El límite superior más pequeño o el límite inferior más grande de un conjunto de tipos no siempre existe. Por ejemplo, considere las definiciones de clase

class A[+T] {}
class B extends A[B]
class C extends A[C]

Entonces los tipos A[Any], A[A[Any]], A[A[A[Any]]], ... forman una secuencia descendente de límites superiores para B y C. El límite superior más pequeño sería el límite infinito de esa secuencia, que no existe como un tipo de Scala. Dado que casos como este son en general imposibles de detectar, un compilador de Scala es libre de rechazar un término que tiene un tipo especificado como un límite superior o inferior más pequeño, y ese límite sería más complejo que algún límite establecido por el compilador[5].

El límite superior más pequeño o el límite inferior más grande tampoco pueden ser únicos. Por ejemplo, A with B y B with A son ambos límites inferiores más grandes de A y B. Si hay varios límites superiores más pequeños o límites inferiores más grandes, el compilador de Scala es libre de elegir cualquiera de ellos.

Conformidad débil

En algunas situaciones, Scala utiliza una relación de conformidad más general. Un tipo S se ajusta débilmente a un tipo T, escrito S <:_w T, si S <: T o tanto S como T son tipos numéricos primitivos y S precede a T en el siguiente orden.

Byte  <:_w Short
Short <:_w Int
Char  <:_w Int
Int   <:_w Long
Long  <:_w Float
Float <:_w Double

Un límite superior más pequeño débil es un límite superior más pequeño con respecto a la conformidad débil.

Tipos volátiles

La volatilidad de un tipo aproxima la posibilidad de que un parámetro de tipo o una instancia de tipo abstracto de un tipo no tenga ningún valor no nulo. Un miembro de valor de un tipo volátil no puede aparecer en una ruta.

Un tipo es volátil si cae en una de cuatro categorías:

Un tipo compuesto $T₁$ with ... with $Tₙ$ { $R$ } es volátil si se cumple una de las siguientes dos condiciones.

  1. Uno de T₂, ..., Tₙ es un parámetro de tipo o un tipo abstracto, o
  2. T₁ es un tipo abstracto y tanto el refinamiento R como un tipo Tⱼ para j > 1 contribuyen un miembro abstracto al tipo compuesto, o
  3. uno de T₁, ..., Tₙ es un tipo singleton.

Aquí, un tipo S contribuye un miembro abstracto a un tipo T si S contiene un miembro abstracto que también es miembro de T. Un refinamiento R contribuye un miembro abstracto a un tipo T si R contiene una declaración abstracta que también es miembro de T.

Un designador de tipo es volátil si es un alias de un tipo volátil, o si designa un parámetro de tipo o un tipo abstracto que tiene un tipo volátil como su límite superior.

Un tipo singleton $p$.type es volátil si el tipo subyacente de la ruta p es volátil.

Un tipo existencial $T$ forSome { $Q$ } es volátil si T es volátil.

Borrado de tipos

Un tipo se llama genérico si contiene argumentos de tipo o variables de tipo. Borrado de tipos es un mapeo de tipos (posiblemente genéricos) a tipos no genéricos. Escribimos |T| para el borrado del tipo T. El mapeo de borrado se define de la siguiente manera.

  • El borrado de un tipo con alias es el borrado de su lado derecho.
  • El borrado de un tipo abstracto es el borrado de su límite superior.
  • El borrado del tipo parametrizado scala.Array$[T₁]$ es scala.Array$[|T₁|]$.
  • El borrado de todos los demás tipos parametrizados T[T₁, ..., Tₙ] es |T|.
  • El borrado de un tipo singleton $p$.type es el borrado del tipo de p.
  • El borrado de una proyección de tipo $T$#$x$ es |$T$|#$x$.
  • El borrado de un tipo compuesto $T₁$ with ... with $Tₙ$ { $R$ } es el borrado del dominador de intersección de T₁, ..., Tₙ.
  • El borrado de un tipo existencial $T$ forSome { $Q$ } es |T|.

El dominador de intersección de una lista de tipos T₁, ..., Tₙ se calcula de la siguiente manera. Sea Tᵢ₁, ..., Tᵢₘ la subsecuencia de tipos Tᵢ que no son supertipos de algún otro tipo Tⱼ. Si esta subsecuencia contiene un designador de tipo Tc que apunta a una clase que no es un trait, el dominador de intersección es Tc. De lo contrario, el dominador de intersección es el primer elemento de la subsecuencia, Tᵢ₁.

2 Una referencia a un miembro definido estructuralmente (llamada a método o acceso a una valor o variable) puede generar código binario significativamente más lento que un código equivalente a un miembro no estructural.

3 Una congruencia es una relación de equivalencia que está cerrada bajo formación de contextos.

4 Un tipo de método es implícito si la sección de parámetros que lo define comienza con la palabra clave implicit.

5 El compilador de Scala actual limita el nivel de anidamiento de la parametrización en tales límites para que sea como máximo dos niveles más profundo que el nivel máximo de anidamiento de los tipos de operando.

Etiquetas: scala tipos especificación sistema de tipos tipos parametrizados

Publicado el 7-4 17:09