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 dtipo 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

tensorbiblioteca está logrando esto de manera elegante usando unadatadefinició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
LANGUAGEdirectivas, 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
datadeclaración introduce un tipo llamadoNaty dos constructores de tipo llamadosSyZ, en otras palabras, tenemos números naturales de nivel de tipo . Tenga en cuenta que los tiposSyZno 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:
Vecrequiere un tipo de tipoNat(es decir, unZo unStipo) 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 unaVConscelda 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
Vecs 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, esVecaplicativo<*>; No lo puse en unaApplicativeinstancia porque se vuelve desordenado . Tenga en cuenta también que estoy usando lafoldrinstancia generada por el compilador deFoldable.Probémoslo:
¡Excelente! Obtiene un error en tiempo de compilación cuando intenta
dotvectores 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
Nats. Para esto usamos una función de nivel de tipo :Esta
type familydeclaració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 queZero, 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 dosNats.) 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
nargumento 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 queDataKindssolo nos da constructores de tipos promocionados. Para decirlo de otra manera, los constructores de tiposSyZno 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 unNattynos enseña sobre élny 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
singletonsbiblioteca contiene algunos ayudantes de Template Haskell para generar valores únicos comoNattypara 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 VNilpara 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.AVeces un tipo existencial : la variable de tiponno aparece en el tipo de retorno delAVecconstructor de datos. Lo estamos utilizando para simular un par dependiente :fromListno 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 nel 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á
filterpara los vectores:A
dotdosAVecs, tenemos que demostrarle a GHC que sus longitudes son iguales.Data.Type.Equalitydefine 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: usaremosgcastWithpara convertir entre tipos equivalentes yTestEqualitypara determinar si dosNattys son iguales.Para probar la igualdad de dos
Nattys, 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
Reflel 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 deReflinmediato.Ahora podemos escribir una instancia de
TestEqualitypor 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
dotun par deAVecs de longitud desconocida.Primero, coincidencia de patrón en el
AVecconstructor para extraer una representación en tiempo de ejecución de las longitudes de los vectores. Ahora usetestEqualitypara determinar si esas longitudes son iguales. Si lo son, lo haremosJust Refl;gcastWithutilizará esa prueba de igualdad para garantizar quedot u vesté bien tipada al descargar sun ~ msuposició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 obtenerifdeclaraciones 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
Nothingno 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 apara 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,
Natel tipo,Natel tipo,Natty nel 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 deprintfinterfaces 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). Bque es una función deAaB xdondexestá 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