spark - Recursos de programación tipo Scala
scala functions (5)
Según esta pregunta , el sistema de tipos de Scala está completo . ¿Qué recursos están disponibles que permiten a un recién llegado aprovechar la potencia de la programación de tipo de nivel?
Estos son los recursos que he encontrado hasta ahora:
- La Alta Hechicería de Daniel Spiewak en la Tierra de Scala
- Programación de nivel de tipo Apocalisp en Scala
- Jesper''s HList
Estos recursos son geniales, pero siento que me falta lo básico, por lo que no tengo una base sólida sobre la cual construir. Por ejemplo, ¿dónde hay una introducción a las definiciones de tipo? ¿Qué operaciones puedo realizar en tipos?
¿Hay algún buen recurso introductorio?
Además de los otros enlaces aquí, también hay publicaciones de mi blog sobre meta programación de tipo en Scala:
- Meta-Programación con Scala Parte I: Adición
- Meta-Programación con Scala Parte II: Multiplicación
- Meta-programación con Scala Parte III: aplicación de función parcial
- Metaprogramación con Scala: compilación condicional y desenrollamiento en bucle
- Codificación de nivel de tipo Scala del cálculo de SKI
Como se sugiere en Twitter: Shapeless: una exploración de programación genérica / politípica en Scala por Miles Sabin.
ScalaZ tiene un código fuente, una wiki y ejemplos.
Visión de conjunto
La programación a nivel de tipo tiene muchas similitudes con la programación tradicional de nivel de valor. Sin embargo, a diferencia de la programación de nivel de valor, donde el cálculo se produce en tiempo de ejecución, en la programación de nivel de tipo, el cálculo se produce en tiempo de compilación. Trataré de establecer paralelismos entre la programación en el nivel de valor y la programación en el 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 a partir de aquí siguen el paradigma orientado a objetos.
Un buen y bastante simple ejemplo de programación de nivel de tipo en el paradigma orientado a objetos se puede encontrar en la implementación de apocalisp del cálculo lambda , que se reproduce aquí:
// Abstract trait
trait Lambda {
type subst[U <: Lambda] <: Lambda
type apply[U <: Lambda] <: Lambda
type eval <: Lambda
}
// Implementations
trait App[S <: Lambda, T <: Lambda] extends Lambda {
type subst[U <: Lambda] = App[S#subst[U], T#subst[U]]
type apply[U] = Nothing
type eval = S#eval#apply[T]
}
trait Lam[T <: Lambda] extends Lambda {
type subst[U <: Lambda] = Lam[T]
type apply[U <: Lambda] = T#subst[U]#eval
type eval = Lam[T]
}
trait X extends Lambda {
type subst[U <: Lambda] = U
type apply[U] = Lambda
type eval = X
}
Como se puede ver en el ejemplo, el paradigma orientado a objetos para la programación a nivel de tipo procede de la siguiente manera:
- Primero: defina un rasgo abstracto con varios campos de tipo abstracto (vea debajo lo que es un campo abstracto). Esta es una plantilla para garantizar que ciertos campos de tipos existen en todas las implementaciones sin forzar una implementación. En el ejemplo de cálculo lambda, esto corresponde al
trait Lambda
que garantiza que existen los siguientes tipos:subst
,apply
yeval
. - Siguiente: defina los subtratos que extienden el rasgo abstracto e implemente los diversos campos de tipo abstracto
- A menudo, estos subtratos se parametrizarán con argumentos. En el ejemplo de cálculo lambda, los subtipos son
trait App extends Lambda
que se parametriza con dos tipos (S
yT
, ambos deben ser subtipos deLambda
), eltrait Lam extends Lambda
parametrizado con un tipo (T
) y eltrait X extends Lambda
( que no está parametrizado). - los campos de tipo a menudo se implementan haciendo referencia a los parámetros de tipo del sustrat ya veces haciendo referencia a sus campos de tipo a través del operador hash:
#
(que es muy similar al operador de punto:. para los valores). En laApp
de rasgo del ejemplo del cálculo lambda, el tipoeval
se implementa de la siguiente manera:type eval = S#eval#apply[T]
. Básicamente se trata de llamar al tipoeval
del parámetroS
del rasgo, y llamarapply
con el parámetroT
al resultado. Tenga en cuenta que se garantiza queS
tiene un tipo deeval
porque el parámetro lo especifica como un subtipo deLambda
. De forma similar, el resultado deeval
debe tener un tipo deapply
, ya que se especifica que es un subtipo deLambda
, como se especifica en el rasgo abstractoLambda
.
- A menudo, estos subtratos se parametrizarán con argumentos. En el ejemplo de cálculo lambda, los subtipos son
El paradigma funcional consiste en definir muchos constructores de tipos parametrizados que no están agrupados en rasgos.
Comparación entre la programación de nivel de valor y la programación de tipo de nivel
- clase abstracta
- valor-nivel:
abstract class C { val x }
- tipo-nivel:
trait C { type X }
- valor-nivel:
- tipos dependientes de ruta
-
Cx
(haciendo referencia al valor de campo / función x en el objeto C) -
C#x
(campo de referencia tipo x en el rasgo C)
-
- firma de función (sin implementación)
- valor-nivel:
def f(x:X) : Y
- tipo-nivel:
type f[x <: X] <: Y
(esto se llama "constructor de tipo" y generalmente aparece en el rasgo abstracto)
- valor-nivel:
- implementación de la función
- valor-nivel:
def f(x:X) : Y = x
- tipo-nivel:
type f[x <: X] = x
- valor-nivel:
- condicionales
- comprobación de igualdad
- nivel de valor:
a:A == b:B
- tipo-nivel:
implicitly[A =:= B]
- nivel de valor: sucede en la JVM mediante una prueba unitaria en tiempo de ejecución (es decir, sin errores de tiempo de ejecución):
- en essense es un assert:
assert(a == b)
- en essense es un assert:
- tipo-nivel: sucede en el compilador a través de un control de tipo (es decir, sin errores del compilador):
- en esencia es una comparación de tipo: por ejemplo,
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") compila solo siA
es visible comoB
(es decir, hay una conversión implícita deA
a un subtipo deB
) - un ejemplo
- más operadores de comparación
- en esencia es una comparación de tipo: por ejemplo,
- nivel de valor:
Conversión entre tipos y valores
En muchos de los ejemplos, los tipos definidos a través de rasgos a menudo son abstractos y sellados, y por lo tanto no se pueden crear instancias directamente ni a través de una subclase anónima. Por lo tanto, es común utilizar
null
como valor de marcador de posición cuando se realiza un cálculo del nivel de valor utilizando algún tipo de interés:- ej.
val x:A = null
, dondeA
es del tipo que te importa
- ej.
Debido al borrado de tipos, todos los tipos parametrizados tienen el mismo aspecto. Además, (como se mencionó anteriormente) los valores con los que está trabajando tienden a ser
null
, por lo que condicionar el tipo de objeto (por ejemplo, a través de una declaración de coincidencia) es ineficaz.
El truco es usar 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 implícitos.
Considere este ejemplo ( tomado de metascala y apocalisp ):
sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat
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 número entero mayor que cero tiene un tipo de la forma Succ[A]
, donde A
es el tipo que representa un número 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 aliar varios números naturales para una referencia más conveniente. Ejemplo:
type _3 = Succ[Succ[Succ[_0]]]
(Esto es muy parecido a definir un val
como el resultado de una función).
Supongamos ahora que queremos definir una función de nivel de def toInt[T <: Nat](v : T)
que toma un valor de argumento, v
, que se ajusta a Nat
y devuelve un entero que representa el número natural codificado en v
'' tipo de s Por ejemplo, si tenemos el valor val x:_3 = null
( null
de tipo Succ[Succ[Succ[_0]]]
), queremos que toInt(x)
devuelva 3
.
Para implementar toInt
, vamos a hacer uso de la siguiente clase:
class TypeToValue[T, VT](value : VT) { def getValue() = value }
Como veremos a continuación, habrá un objeto construido a partir de la clase TypeToValue
para cada Nat
desde _0
hasta (por ejemplo) _3
, y cada uno almacenará la representación del valor del tipo correspondiente (es decir, TypeToValue[_0, Int]
almacenará el valor 0
, TypeToValue[Succ[_0], Int]
almacenará el valor 1
, etc.). Tenga en cuenta que TypeToValue
está parametrizado en dos tipos: T
y VT
. T
corresponde al tipo al que intentamos asignar valores (en nuestro ejemplo, Nat
) y VT
corresponde al tipo de valor que le asignamos (en nuestro ejemplo, Int
).
Ahora hacemos las siguientes dos definiciones implícitas:
implicit val _0ToInt = new TypeToValue[_0, Int](0)
implicit def succToInt[P <: Nat](implicit v : TypeToValue[P, Int]) =
new TypeToValue[Succ[P], Int](1 + v.getValue())
Y implementamos toInt
siguiente manera:
def toInt[T <: Nat](v : T)(implicit ttv : TypeToValue[T, Int]) : Int = ttv.getValue()
Para entender cómo funciona, considere lo que hace en un par de entradas:
val z:_0 = null
val y:Succ[_0] = null
Cuando llamamos a toInt(z)
, el compilador busca un argumento implícito TypeToValue[_0, Int]
de tipo TypeToValue[_0, Int]
(ya que z
es de tipo _0
). Encuentra el objeto _0ToInt
, llama al método getValue
de este objeto y recupera 0
. 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ícito TypeToValue[Succ[_0], Int]
de tipo TypeToValue[Succ[_0], Int]
(ya que y
es de tipo Succ[_0]
). Encuentra la función succToInt
, que puede devolver un objeto del tipo apropiado ( TypeToValue[Succ[_0], Int]
) y lo evalúa. Esta función toma un argumento implícito ( v
) de tipo TypeToValue[_0, Int]
(es decir, un TypeToValue
donde el primer parámetro de tipo tiene uno menos Succ[_]
). El compilador proporciona _0ToInt
(como se hizo en la evaluación de toInt(z)
anterior) y succToInt
construye un nuevo objeto TypeToValue
con valor 1
. De nuevo, es importante tener en cuenta que el compilador proporciona todos estos valores implícitamente, ya que no tenemos acceso a ellos explícitamente.
Verificando tu trabajo
Hay varias formas de verificar que sus cálculos de nivel de tipo estén haciendo lo que espera. Aquí hay algunos enfoques. Haga dos tipos A
y B
, que desea verificar son iguales. Luego verifique que la siguiente compilación:
-
Equal[A, B]
- con: rasgo
Equal[T1 >: T2 <: T2, T2]
( tomado de apocolisp )
- con: rasgo
-
implicitly[A =:= B]
Alternativamente, puede convertir el tipo a un valor (como se muestra arriba) y hacer una verificación en tiempo de ejecución de los valores. Ej assert(toInt(a) == toInt(b))
, donde a
es de tipo A
y b
es de tipo B
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:
- Genéricos de un tipo superior (pdf)
- Type Constructor Polymorphism for Scala: Theory and Practice (pdf) (Tesis de doctorado, que incluye el trabajo anterior de Moors)
- Tipo Constructor Inferencia de polimorfismo
Apocalisp es un blog con muchos ejemplos de programación a nivel de tipo en scala.
- La Programación de Tipo de Nivel en Scala es una fantástica visita guiada de algunos tipos de programación que incluye booleanos, números naturales (como arriba), números binarios, listas heterogéneas, y más.
- Más Scala Typehackery es la implementación de cálculo lambda anterior.
ScalaZ es un proyecto muy activo que proporciona funcionalidades que amplían la API de Scala utilizando varias características de programación de tipo. Es un proyecto muy interesante que tiene muchos seguidores.
MetaScala es una biblioteca de nivel de tipo para Scala, incluidos los metadatos para números naturales, booleanos, unidades, HList, etc. Es un proyecto de Jesper Nordenberg (su blog) .
El Michid (blog) tiene algunos impresionantes ejemplos de programación de tipo de nivel en Scala (de otra respuesta):
- Meta-Programación con Scala Parte I: Adición
- Meta-Programación con Scala Parte II: Multiplicación
- Meta-programación con Scala Parte III: aplicación de función parcial
- Metaprogramación con Scala: compilación condicional y desenrollamiento en bucle
- Codificación de nivel de tipo Scala del cálculo de SKI
Debasish Ghosh (blog) también tiene algunos mensajes relevantes:
- Abstracciones de orden superior en scala
- La tipificación estática le da una ventaja
- Scala implica clases de tipo, aquí vengo
- Refactorización en clases de tipo scala
- Usar restricciones de tipo generalizadas
- Cómo scalas teclea las palabras del sistema para ti
- Elegir entre los miembros de tipo abstracto
(He estado haciendo una investigación sobre este tema y esto es lo que he aprendido. Todavía soy nuevo en esto, así que por favor señale cualquier inexactitud en esta respuesta).
- Sing , una biblioteca de metaprogramación de nivel de tipo en Scala.
- El comienzo de la programación de nivel de tipo en Scala