Supongamos que quiero escribir una biblioteca que se ocupe de vectores y matrices. ¿Es posible hornear las dimensiones en los tipos, de modo que las operaciones de dimensiones incompatibles generen un error en el momento de la compilación?
Por ejemplo, me gustaría que la firma del producto dot sea algo así como
dotprod :: Num a, VecDim d => Vector a d -> Vector a d -> a
donde el d
tipo contiene un único valor entero (que representa la dimensión de estos vectores).
Supongo que esto podría hacerse definiendo (a mano) un tipo separado para cada número entero y agrupándolos en una clase de tipo llamada VecDim
. ¿Existe algún mecanismo para "generar" tales tipos?
¿O quizás alguna forma mejor / más simple de lograr lo mismo?
haskell
type-systems
type-safety
mitchus
fuente
fuente
tensor
biblioteca está logrando esto de manera elegante usando unadata
definición recursiva : noaxiom.org/tensor-documentation#ordinalsRespuestas:
Para ampliar la respuesta de @ KarlBielefeldt, aquí hay un ejemplo completo de cómo implementar Vectores (listas con un número de elementos estáticamente conocido) en Haskell. Agárrate a tu sombrero ...
Como puede ver en la larga lista de
LANGUAGE
directivas, esto solo funcionará con una versión reciente de GHC.Necesitamos una forma de representar longitudes dentro del sistema de tipos. Por definición, un número natural es cero (
Z
) o es el sucesor de algún otro número natural (S n
). Entonces, por ejemplo, se escribiría el número 3S (S (S Z))
.Con la extensión DataKinds , esta
data
declaración introduce un tipo llamadoNat
y dos constructores de tipo llamadosS
yZ
, en otras palabras, tenemos números naturales de nivel de tipo . Tenga en cuenta que los tiposS
yZ
no tienen ningún valor de miembro: solo los tipos de tipo*
están habitados por valores.Ahora presentamos un GADT que representa vectores con una longitud conocida. Tenga en cuenta la firma de tipo:
Vec
requiere un tipo de tipoNat
(es decir, unZ
o unS
tipo) para representar su longitud.La definición de vectores es similar a la de las listas vinculadas, con información adicional a nivel de tipo sobre su longitud. Un vector es
VNil
, en cuyo caso tiene una longitud deZ
(ero), o es unaVCons
celda que agrega un elemento a otro vector, en cuyo caso su longitud es uno más que el otro vector (S n
). Tenga en cuenta que no hay argumento constructor de tipon
. Solo se usa en tiempo de compilación para rastrear longitudes, y se borrará antes de que el compilador genere código de máquina.Hemos definido un tipo de vector que conlleva un conocimiento estático de su longitud. Vamos a consultar el tipo de unos pocos
Vec
s para tener una idea de cómo funcionan:El producto punto continúa como lo haría para una lista:
vap
, que 'zippily' aplica un vector de funciones a un vector de argumentos, esVec
aplicativo<*>
; No lo puse en unaApplicative
instancia porque se vuelve desordenado . Tenga en cuenta también que estoy usando lafoldr
instancia generada por el compilador deFoldable
.Probémoslo:
¡Excelente! Obtiene un error en tiempo de compilación cuando intenta
dot
vectores cuyas longitudes no coinciden.Aquí hay un intento de una función para concatenar vectores juntos:
La longitud del vector de salida sería la suma de las longitudes de los dos vectores de entrada. Necesitamos enseñarle al verificador de tipos cómo sumar
Nat
s. Para esto usamos una función de nivel de tipo :Esta
type family
declaración introduce una función en los tipos llamados:+:
; en otras palabras, es una receta para que el verificador de tipos calcule la suma de dos números naturales. Se define de forma recursiva: cuando el operando izquierdo es mayor queZ
ero, agregamos uno a la salida y lo reducimos en uno en la llamada recursiva. (Es un buen ejercicio escribir una función de tipo que multiplique dosNat
s.) Ahora podemos+++
compilar:Así es como lo usa:
Hasta ahora muy simple. ¿Qué pasa cuando queremos hacer lo opuesto a la concatenación y dividir un vector en dos? Las longitudes de los vectores de salida dependen del valor de tiempo de ejecución de los argumentos. Nos gustaría escribir algo como esto:
pero desafortunadamente Haskell no nos deja hacer eso. Permitir que el valor del
n
argumento aparezca en el tipo de retorno (esto comúnmente se llama una función dependiente o tipo pi ) requeriría tipos dependientes de "espectro completo", mientras queDataKinds
solo nos da constructores de tipos promocionados. Para decirlo de otra manera, los constructores de tiposS
yZ
no aparecen en el nivel de valor. Tendremos que conformarnos con valores únicos para una representación en tiempo de ejecución de un determinadoNat
. *Para un tipo dado
n
(con tipoNat
), hay precisamente un término de tipoNatty n
. Podemos usar el valor singleton como testigo de tiempo de ejecución paran
: aprender sobre unNatty
nos enseña sobre éln
y viceversa.Vamos a darle una vuelta:
En el primer ejemplo, dividimos con éxito un vector de tres elementos en la posición 2; entonces obtuvimos un error de tipo cuando intentamos dividir un vector en una posición más allá del final. Los singletons son la técnica estándar para hacer que un tipo dependa de un valor en Haskell.
* La
singletons
biblioteca contiene algunos ayudantes de Template Haskell para generar valores únicos comoNatty
para usted.Ultimo ejemplo ¿Qué pasa cuando no conoces la dimensionalidad de tu vector estáticamente? Por ejemplo, ¿qué pasa si estamos tratando de construir un vector a partir de datos de tiempo de ejecución en forma de una lista? Necesita que el tipo de vector dependa de la longitud de la lista de entrada. En otras palabras, no podemos usar
foldr VCons VNil
para construir un vector porque el tipo del vector de salida cambia con cada iteración del pliegue. Necesitamos mantener la longitud del vector en secreto del compilador.AVec
es un tipo existencial : la variable de tipon
no aparece en el tipo de retorno delAVec
constructor de datos. Lo estamos utilizando para simular un par dependiente :fromList
no podemos decirle la longitud del vector estáticamente, pero puede devolver algo con lo que puede hacer coincidir un patrón para conocer la longitud del vector,Natty n
el primer elemento de la tupla . Como dice Conor McBride en una respuesta relacionada , "Miras una cosa y, al hacerlo, aprendes sobre otra".Esta es una técnica común para tipos cuantificados existencialmente. Debido a que en realidad no puede hacer nada con datos para los que no conoce el tipo, intente escribir una función de
data Something = forall a. Sth a
, los existenciales a menudo vienen agrupados con evidencia GADT que le permite recuperar el tipo original realizando pruebas de coincidencia de patrones. Otros patrones comunes para los existenciales incluyen funciones de empaquetamiento para procesar su tipo (data AWayToGetTo b = forall a. HeresHow a (a -> b)
), que es una forma ordenada de hacer módulos de primera clase, o incorporar un diccionario de clase de tipo (data AnOrd = forall a. Ord a => AnOrd a
) que puede ayudar a emular el polimorfismo de subtipo.Los pares dependientes son útiles siempre que las propiedades estáticas de los datos dependan de información dinámica no disponible en el momento de la compilación. Aquí está
filter
para los vectores:A
dot
dosAVec
s, tenemos que demostrarle a GHC que sus longitudes son iguales.Data.Type.Equality
define un GADT que solo se puede construir cuando sus argumentos de tipo son los mismos:Cuando el patrón coincide
Refl
, GHC lo sabea ~ b
. También hay algunas funciones para ayudarlo a trabajar con este tipo: usaremosgcastWith
para convertir entre tipos equivalentes yTestEquality
para determinar si dosNatty
s son iguales.Para probar la igualdad de dos
Natty
s, necesitaremos hacer uso del hecho de que si dos números son iguales, entonces sus sucesores también son iguales (:~:
es congruente conS
):La coincidencia de patrones en
Refl
el lado izquierdo le permite a GHC saber eson ~ m
. Con ese conocimiento, es trivial esoS n ~ S m
, por lo que GHC nos permite devolver uno nuevo deRefl
inmediato.Ahora podemos escribir una instancia de
TestEquality
por recursión directa. Si ambos números son cero, son iguales. Si ambos números tienen predecesores, son iguales si los predecesores son iguales. (Si no son iguales, solo regreseNothing
).Ahora podemos juntar las piezas en
dot
un par deAVec
s de longitud desconocida.Primero, coincidencia de patrón en el
AVec
constructor para extraer una representación en tiempo de ejecución de las longitudes de los vectores. Ahora usetestEquality
para determinar si esas longitudes son iguales. Si lo son, lo haremosJust Refl
;gcastWith
utilizará esa prueba de igualdad para garantizar quedot u v
esté bien tipada al descargar sun ~ m
suposición implícita .Tenga en cuenta que, dado que un vector sin conocimiento estático de su longitud es básicamente una lista, hemos implementado efectivamente la versión de la lista
dot :: Num a => [a] -> [a] -> Maybe a
. La diferencia es que esta versión se implementa en términos de los vectores 'dot
. Este es el punto: antes de que el verificador de tipos le permita llamardot
, debe haber probado si las listas de entrada tienen la misma longitudtestEquality
. ¡Soy propenso a obtenerif
declaraciones incorrectas, pero no en un entorno de tipo dependiente!No puede evitar el uso de contenedores existenciales en los bordes de su sistema, cuando se trata de datos de tiempo de ejecución, pero puede usar tipos dependientes en todas partes dentro de su sistema y mantener los contenedores existenciales en los bordes, cuando realiza la validación de entrada.
Como
Nothing
no es muy informativo, puede refinar aún más el tipo dedot'
para devolver una prueba de que las longitudes no son iguales (en forma de evidencia de que su diferencia no es 0) en el caso de falla. Esto es bastante similar a la técnica estándar de Haskell de usarEither String a
para posiblemente devolver un mensaje de error, ¡aunque un término de prueba es mucho más útil computacionalmente que una cadena!Así termina este recorrido de parada por silbido de algunas de las técnicas que son comunes en la programación de Haskell de tipo dependiente. La programación con tipos como este en Haskell es realmente genial, pero muy incómoda al mismo tiempo. Desglosar todos sus datos dependientes en muchas representaciones que significan lo mismo,
Nat
el tipo,Nat
el tipo,Natty n
el singleton, es realmente bastante engorroso, a pesar de la existencia de generadores de código para ayudar con la repetitiva. También hay actualmente limitaciones en lo que se puede promover al nivel de tipo. ¡Sin embargo, es tentador! La mente se aturde ante las posibilidades: en la literatura hay ejemplos en Haskell deprintf
interfaces de bases de datos fuertemente tipadas , motores de diseño de interfaz de usuario ...Si desea leer más, hay un creciente cuerpo de literatura sobre Haskell de tipo dependiente, tanto publicado como en sitios como Stack Overflow. Un buen punto de partida es el documento de Hasochism : el documento pasa por este mismo ejemplo (entre otros), discutiendo las partes dolorosas con cierto detalle. El artículo de Singletons demuestra la técnica de los valores de singleton (como
Natty
). Para obtener más información sobre la escritura dependiente en general, el tutorial de Agda es un buen lugar para comenzar; Además, Idris es un lenguaje en desarrollo que está diseñado (aproximadamente) para ser "Haskell con tipos dependientes".fuente
Eso se llama mecanografía dependiente . Una vez que conozca el nombre, puede encontrar más información sobre el que nunca podría desear. También hay un interesante lenguaje tipo haskell llamado Idris que los usa de forma nativa. Su autor ha hecho algunas presentaciones realmente buenas sobre el tema que puede encontrar en youtube.
fuente
newtype Vec2 a = V2 (a,a)
,newtype Vec3 a = V3 (a,a,a)
etc., pero esa no es la pregunta).Pi (x : A). B
que es una función deA
aB x
dondex
está el argumento de la función. Aquí el tipo de retorno de la función depende de la expresión suministrada como argumento. Sin embargo, todo esto se puede borrar, solo es tiempo de compilación