Según esta pregunta , el sistema de tipos de Scala es Turing completo . ¿Qué recursos están disponibles que permiten a un recién llegado aprovechar el poder de la programación a nivel de tipo?
Estos son los recursos que he encontrado hasta ahora:
- La alta hechicería de Daniel Spiewak en la tierra de Scala
- Programación a nivel de tipo de Apocalisp en Scala
- Jesper's HList
Estos recursos son geniales, pero siento que me faltan los conceptos básicos y, por lo tanto, no tengo una base sólida sobre la cual construir. Por ejemplo, ¿dónde hay una introducción a las definiciones de tipos? ¿Qué operaciones puedo realizar en tipos?
¿Existen buenos recursos introductorios?
Respuestas:
Visión general
La programación a nivel de tipo tiene muchas similitudes con la programación tradicional a nivel de valor. Sin embargo, a diferencia de la programación a nivel de valor, donde el cálculo ocurre en tiempo de ejecución, en la programación a nivel de tipo, el cálculo ocurre en tiempo de compilación. Trataré de establecer paralelismos entre la programación a nivel de valor y la programación a nivel de tipo.
Paradigmas
Hay dos paradigmas principales en la programación a nivel de tipo: "orientado a objetos" y "funcional". La mayoría de los ejemplos vinculados desde aquí siguen el paradigma orientado a objetos.
Un buen ejemplo bastante simple de programación a nivel de tipo en el paradigma orientado a objetos se puede encontrar en la implementación de apocalisp del cálculo lambda , replicado aquí:
Como puede verse en el ejemplo, el paradigma orientado a objetos para la programación a nivel de tipo procede de la siguiente manera:
trait Lambda
que los siguientes tipos que existen garantías:subst
,apply
, yeval
.trait App extends Lambda
que están parametrizados con dos tipos (S
yT
ambos deben ser subtipos deLambda
), los que estántrait Lam extends Lambda
parametrizados con un tipo (T
) ytrait X extends Lambda
(que no están parametrizados).#
(que es muy similar al operador punto:.
para valores). En rasgoApp
del ejemplo lambda cálculo, el tipoeval
se implementa como sigue:type eval = S#eval#apply[T]
. Básicamente, se trata de llamar aleval
tipo de parámetro del rasgoS
y llamarapply
con parámetroT
en el resultado. Tenga en cuenta queS
se garantiza que tiene uneval
tipo porque el parámetro especifica que es un subtipo deLambda
. De manera similar, el resultado deeval
debe tener unapply
tipo, ya que se especifica que es un subtipo deLambda
, como se especifica en el rasgo abstractoLambda
.El paradigma funcional consiste en definir lotes de constructores de tipos parametrizados que no se agrupan en rasgos.
Comparación entre programación a nivel de valor y programación a nivel de tipo
abstract class C { val x }
trait C { type X }
C.x
(haciendo referencia al valor del campo / función x en el objeto C)C#x
(campo de referencia tipo x en el rasgo C)def f(x:X) : Y
type f[x <: X] <: Y
(esto se llama un "constructor de tipos" y generalmente ocurre en el rasgo abstracto)def f(x:X) : Y = x
type f[x <: X] = x
a:A == b:B
implicitly[A =:= B]
assert(a == b)
implicitly[A =:= B]
A <:< B
, compila solo siA
es un subtipo deB
A =:= B
, compila solo siA
es un subtipo deB
yB
es un subtipo deA
A <%< B
, ("visible como") se compila solo siA
se puede ver comoB
(es decir, hay una conversión implícita deA
a un subtipo deB
)Conversión entre tipos y valores
En muchos de los ejemplos, los tipos definidos a través de rasgos suelen ser tanto abstractos como sellados y, por lo tanto, no se pueden instanciar directamente ni a través de una subclase anónima. Por lo tanto, es común usarlo
null
como un valor de marcador de posición cuando se realiza un cálculo de nivel de valor usando algún tipo de interés:val x:A = null
, ¿dóndeA
está el tipo que le interesa?Debido al borrado de tipo, todos los tipos parametrizados tienen el mismo aspecto. Además, (como se mencionó anteriormente) los valores con los que está trabajando tienden a ser todos
null
, por lo que condicionar el tipo de objeto (por ejemplo, a través de una declaración de coincidencia) es ineficaz.El truco consiste en utilizar funciones y valores implícitos. El caso base suele ser un valor implícito y el caso recursivo suele ser una función implícita. De hecho, la programación a nivel de tipo hace un uso intensivo de los implícitos.
Considere este ejemplo ( tomado de metascala y apocalisp ):
Aquí tienes una codificación peano de los números naturales. Es decir, tiene un tipo para cada entero no negativo: un tipo especial para 0, a saber
_0
; y cada entero mayor que cero tiene un tipo de la formaSucc[A]
, dondeA
es el tipo que representa un entero más pequeño. Por ejemplo, el tipo que representa 2 sería:Succ[Succ[_0]]
(sucesor aplicado dos veces al tipo que representa cero).Podemos alias varios números naturales para una referencia más conveniente. Ejemplo:
(Esto es muy parecido a definir
val
a como el resultado de una función).Ahora, suponga que queremos definir una función de nivel de valor
def toInt[T <: Nat](v : T)
que toma un valor de argumentov
, que se ajustaNat
y devuelve un número entero que representa el número natural codificado env
el tipo de. Por ejemplo, si tenemos el valorval x:_3 = null
(null
de tipoSucc[Succ[Succ[_0]]]
), querríamostoInt(x)
volver3
.Para implementar
toInt
, vamos a hacer uso de la siguiente clase:Como veremos a continuación, habrá un objeto construido a partir de la clase
TypeToValue
para cada unoNat
de_0
hasta (por ejemplo)_3
, y cada uno almacenará la representación del valor del tipo correspondiente (es decirTypeToValue[_0, Int]
, almacenará el valor0
,TypeToValue[Succ[_0], Int]
almacenará el valor1
, etc.). Tenga en cuenta,TypeToValue
está parametrizado por dos tipos:T
yVT
.T
corresponde al tipo al que estamos tratando de asignar valores (en nuestro ejemplo,Nat
) yVT
corresponde al tipo de valor que le estamos asignando (en nuestro ejemplo,Int
).Ahora hacemos las siguientes dos definiciones implícitas:
Y lo implementamos de la
toInt
siguiente manera:Para entender cómo
toInt
funciona, consideremos qué hace en un par de entradas:Cuando llamamos
toInt(z)
, el compilador busca un argumento implícitottv
de tipoTypeToValue[_0, Int]
(ya quez
es de tipo_0
). Encuentra el objeto_0ToInt
, llama algetValue
método de este objeto y vuelve0
. El punto importante a tener en cuenta es que no especificamos al programa qué objeto usar, el compilador lo encontró implícitamente.Ahora consideremos
toInt(y)
. Esta vez, el compilador busca un argumento implícitottv
de tipoTypeToValue[Succ[_0], Int]
(ya quey
es de tipoSucc[_0]
). Encuentra la funciónsuccToInt
, que puede devolver un objeto del tipo apropiado (TypeToValue[Succ[_0], Int]
) y lo evalúa. Esta función en sí misma toma un argumento implícito (v
) de tipoTypeToValue[_0, Int]
(es decir, aTypeToValue
donde el primer parámetro de tipo tiene uno menosSucc[_]
). El compilador suministra_0ToInt
(como se hizo en la evaluacióntoInt(z)
anterior) ysuccToInt
construye un nuevoTypeToValue
objeto con valor1
. Nuevamente, es importante tener en cuenta que el compilador proporciona todos estos valores implícitamente, ya que no tenemos acceso a ellos explícitamente.Comprobando tu trabajo
Hay varias formas de verificar que sus cálculos a nivel de tipo estén haciendo lo que espera. Aquí hay algunos enfoques. Haz dos tipos
A
yB
, que quieras verificar, son iguales. Luego verifique que se compile lo siguiente:Equal[A, B]
Equal[T1 >: T2 <: T2, T2]
( tomado de apocolisp )implicitly[A =:= B]
Alternativamente, puede convertir el tipo en un valor (como se muestra arriba) y hacer una verificación en tiempo de ejecución de los valores. Por ejemplo
assert(toInt(a) == toInt(b))
, dondea
es de tipoA
yb
es de tipoB
.Recursos adicionales
El conjunto completo de construcciones disponibles se puede encontrar en la sección de tipos del manual de referencia de scala (pdf) .
Adriaan Moors tiene varios artículos académicos sobre constructores de tipos y temas relacionados con ejemplos de scala:
Apocalisp es un blog con muchos ejemplos de programación a nivel de tipo en scala.
ScalaZ es un proyecto muy activo que proporciona una funcionalidad que extiende la API de Scala utilizando varias funciones de programación a nivel de tipo. Es un proyecto muy interesante que tiene muchos seguidores.
MetaScala es una biblioteca a nivel de tipo para Scala, que incluye metatipos para números naturales, booleanos, unidades, HList, etc. Es un proyecto de Jesper Nordenberg (su blog) .
El Michid (blog) tiene algunos ejemplos asombrosos de programación a nivel de tipo en Scala (de otra respuesta):
Debasish Ghosh (blog) también tiene algunas publicaciones relevantes:
(He estado investigando un poco sobre este tema y esto es lo que he aprendido. Todavía soy nuevo en él, así que indique cualquier inexactitud en esta respuesta).
fuente
Además de los otros enlaces aquí, también están las publicaciones de mi blog sobre metaprogramación de nivel de tipo en Scala:
fuente
Como se sugiere en Twitter: Shapeless: una exploración de la programación genérica / politípica en Scala por Miles Sabin.
fuente
fuente
Scalaz tiene código fuente, wiki y ejemplos.
fuente