¿Es posible una variante Lisp completa de tipo estático?

107

¿Es posible una variante Lisp completa de tipo estático? ¿Tiene sentido que exista algo como esto? Creo que una de las virtudes de un lenguaje Lisp es la simplicidad de su definición. ¿La escritura estática comprometería este principio fundamental?

Lambda la penúltima
fuente
10
Me gustan las macros de formato libre de Lisp, pero me gusta la solidez del sistema de tipos de Haskell. Me encantaría ver cómo se ve un Lisp de tipo estático.
mcandre
4
¡Buena pregunta! Creo que shenlanguage.org hace eso. Ojalá se volviera más común.
Hamish Grubijan
¿Cómo se hace la computación simbólica con Haskell? (resuelva 'x' (= (+ xy) (* xy))). Si lo pone en una cadena, no hay verificación (a diferencia de Lisp, que puede usar macros para agregar verificación). Si usa tipos de datos algebraicos o listas ... Será muy detallado: solve (Sym "x") (Eq (Plus (Sym "x") (Sym "y")) (Mult (Sym "x") (Sym "y")))
aoeu256

Respuestas:

57

Sí, es muy posible, aunque un sistema de tipo de estilo HM estándar suele ser la elección incorrecta para la mayoría de los códigos Lisp / Scheme idiomáticos. Vea Typed Racket para un lenguaje reciente que es un "Full Lisp" (más como Scheme, en realidad) con escritura estática.

Eli Barzilay
fuente
1
El problema aquí es, ¿cuál es el tipo de lista que compone el código fuente completo de un programa de raqueta escrito?
Zorf
18
Eso sería normalmente Sexpr.
Eli Barzilay
Pero luego, puedo escribir coerce :: a->ben términos de eval. ¿Dónde está el tipo de seguridad?
miércoles
2
@ssice: cuando está usando una función sin tipo eval, necesita probar el resultado para ver qué sale, lo cual no es nada nuevo en Typed Racked (el mismo trato que una función que toma un tipo de unión de Stringy Number). Una forma implícita de ver que esto se puede hacer es el hecho de que puede escribir y utilizar un lenguaje de tipado dinámico en un lenguaje de tipado estático HM.
Eli Barzilay
37

Si todo lo que quisiera fuera un lenguaje de tipado estático que se pareciera a Lisp, podría hacerlo con bastante facilidad, definiendo un árbol de sintaxis abstracto que represente su lenguaje y luego mapeando ese AST a expresiones S. Sin embargo, no creo que llamaría Lisp al resultado.

Si desea algo que realmente tenga características Lisp-y además de la sintaxis, es posible hacerlo con un lenguaje escrito estáticamente. Sin embargo, hay muchas características de Lisp de las que es difícil obtener una escritura estática muy útil. Para ilustrar, echemos un vistazo a la estructura de la lista en sí, llamada contras , que forma el bloque de construcción principal de Lisp.

Llamar a los contras una lista, aunque (1 2 3)parece una, es un nombre poco apropiado. Por ejemplo, no es en absoluto comparable a una lista escrita estáticamente, como la lista de C ++ std::listo Haskell. Se trata de listas enlazadas unidimensionales en las que todas las celdas son del mismo tipo. Lisp felizmente lo permite (1 "abc" #\d 'foo). Además, incluso si amplía sus listas de tipo estático para cubrir listas de listas, el tipo de estos objetos requiere que cada elemento de la lista sea una sublista. ¿Cómo representarías ((1 2) 3 4)en ellos?

Lisp conses forma un árbol binario, con hojas (átomos) y ramas (conses). Además, las hojas de un árbol así pueden contener cualquier tipo Lisp atómico (no contras). La flexibilidad de esta estructura es lo que hace que Lisp sea tan bueno en el manejo de cálculos simbólicos, AST y en la transformación del código Lisp en sí.

Entonces, ¿cómo modelaría esa estructura en un lenguaje de tipado estático? Probémoslo en Haskell, que tiene un sistema de tipo estático extremadamente potente y preciso:

type Symbol = String
data Atom = ASymbol Symbol | AInt Int | AString String | Nil
data Cons = CCons Cons Cons 
            | CAtom Atom

Su primer problema será el alcance del tipo Atom. Claramente, no hemos elegido un tipo de Atom con la suficiente flexibilidad para cubrir todos los tipos de objetos que queremos lanzar en contra. En lugar de intentar extender la estructura de datos de Atom como se enumera anteriormente (que puede ver claramente que es frágil), digamos que teníamos una clase de tipos mágicos Atomicque distinguía todos los tipos que queríamos hacer atómicos. Entonces podríamos intentar:

class Atomic a where ?????
data Atomic a => Cons a = CCons Cons Cons 
                          | CAtom a

Pero esto no funcionará porque requiere que todos los átomos del árbol sean del mismo tipo. Queremos que puedan diferir de una hoja a otra. Un mejor enfoque requiere el uso de cuantificadores existenciales de Haskell :

class Atomic a where ?????
data Cons = CCons Cons Cons 
            | forall a. Atomic a => CAtom a 

Pero ahora llega al meollo del asunto. ¿Qué se puede hacer con los átomos en este tipo de estructura? ¿Qué estructura tienen en común con la que se podría modelar Atomic a? ¿Qué nivel de seguridad de tipo tiene garantizado con un tipo de este tipo? Tenga en cuenta que no hemos agregado ninguna función a nuestra clase de tipos, y hay una buena razón: los átomos no comparten nada en común en Lisp. Su supertipo en Lisp simplemente se llama t(es decir, top).

Para poder usarlos, tendrías que idear mecanismos para convertir dinámicamente el valor de un átomo en algo que realmente puedas usar. ¡Y en ese punto, básicamente ha implementado un subsistema escrito dinámicamente dentro de su lenguaje escrito estáticamente! (Uno no puede dejar de notar un posible corolario de la Décima Regla de Programación de Greenspun ).

Tenga en cuenta que Haskell proporciona soporte para un subsistema dinámico con un Objtipo, usado junto con un Dynamictipo y una clase Typeable para reemplazar nuestra Atomicclase, que permite que los valores arbitrarios se almacenen con sus tipos y la coerción explícita de esos tipos. Ese es el tipo de sistema que necesitaría usar para trabajar con estructuras de contras de Lisp en toda su generalidad.

Lo que también puede hacer es ir al otro lado e incrustar un subsistema de tipo estático dentro de un lenguaje esencialmente de tipo dinámico. Esto le permite el beneficio de la verificación de tipo estática para las partes de su programa que pueden aprovechar los requisitos de tipo más estrictos. Este parece ser el enfoque adoptado en la forma limitada de verificación de tipo precisa de CMUCL , por ejemplo.

Finalmente, existe la posibilidad de tener dos subsistemas separados, de tipo dinámico y estático, que usan programación de estilo de contrato para ayudar a navegar la transición entre los dos. De esa manera, el lenguaje puede adaptarse a los usos de Lisp donde la verificación de tipos estáticos sería más un obstáculo que una ayuda, así como los usos donde la verificación de tipos estáticos sería ventajosa. Este es el enfoque adoptado por Typed Racket , como verá en los comentarios que siguen.

Owen S.
fuente
16
Esta respuesta adolece de un problema fundamental: está asumiendo que los sistemas de tipo estático deben ser de estilo HM. El concepto básico que no se puede expresar allí, y es una característica importante del código Lisp, es el subtipo. Si echas un vistazo a la raqueta escrita, verás que puede expresar fácilmente cualquier tipo de lista, incluidas cosas como (Listof Integer)y (Listof Any). Obviamente, sospecharía que este último es inútil porque no sabe nada sobre el tipo, pero en TR, puede usarlo más tarde (if (integer? x) ...)y el sistema sabrá que xes un Integer en la primera rama.
Eli Barzilay
5
Ah, y es una mala caracterización de la raqueta mecanografiada (que es diferente de los sistemas de tipos defectuosos que se encuentran en algunos lugares). Typed Racket es un lenguaje escrito de forma estática , sin sobrecarga de tiempo de ejecución para el código escrito. Racket todavía permite escribir algo de código en TR y algo en el lenguaje no mecanografiado habitual, y en estos casos se utilizan contratos (comprobaciones dinámicas) para proteger el código mecanografiado del código no mecanografiado que puede comportarse mal.
Eli Barzilay
1
@Eli Barzilay: Mentí, hay cuatro partes: 4. Es interesante para mí cómo el estilo de codificación C ++ aceptado por la industria se ha ido alejando gradualmente de los subtipos hacia los genéricos. La debilidad es que el lenguaje no proporciona ayuda para declarar la interfaz que va a usar una función genérica, algo en lo que las clases de tipos ciertamente podrían ayudar. Además, C ++ 0x puede agregar inferencia de tipos. No HM, supongo, ¿pero arrastrándose en esa dirección?
Owen S.
1
Owen: (1) el punto principal es que necesitas subtipos para expresar el tipo de código que escriben los lispers, y no puedes tener eso con los sistemas HM, por lo que estás obligado a personalizar tipos y constructores para cada uso, lo que hace que todo sea mucho más incómodo de usar. En la raqueta mecanografiada, el uso de un sistema con subtipos era un corolario de una decisión de diseño intencional: que el resultado debería poder expresar los tipos de dicho código sin cambiar el código o crear tipos personalizados.
Eli Barzilay
1
(2) Sí, los dynamictipos se están volviendo populares en los lenguajes estáticos como una especie de solución para obtener algunos de los beneficios de los lenguajes tipados dinámicamente, con la compensación habitual de estos valores que se envuelven de una manera que hace que los tipos sean identificables. Pero aquí también la raqueta mecanografiada está haciendo un muy buen trabajo haciéndola conveniente dentro del lenguaje: el verificador de tipos usa ocurrencias de predicados para saber más sobre los tipos. Por ejemplo, vea el ejemplo escrito en la página de la raqueta y vea cómo string?"reduce" una lista de cadenas y números a una lista de cadenas.
Eli Barzilay
10

Mi respuesta, sin un alto grado de confianza, probablemente sea . Si observa un lenguaje como SML, por ejemplo, y lo compara con Lisp, el núcleo funcional de cada uno es casi idéntico. Como resultado, no parece que tenga muchos problemas para aplicar algún tipo de tipado estático al núcleo de Lisp (aplicación de funciones y valores primitivos).

Sin embargo, su pregunta dice completa , y donde veo que surgen algunos de los problemas es el enfoque de código como datos. Los tipos existen a un nivel más abstracto que las expresiones. Lisp no tiene esta distinción: todo tiene una estructura "plana". Si consideramos alguna expresión E: T (donde T es una representación de su tipo), y luego consideramos que esta expresión es un dato simple, ¿cuál es exactamente el tipo de T aquí? ¡Bueno, es una especie! Un tipo es un tipo de orden superior, así que sigamos adelante y digamos algo sobre eso en nuestro código:

E : T :: K

Puede que veas a dónde voy con esto. Estoy seguro de que al separar la información del tipo del código sería posible evitar este tipo de autorreferencialidad de los tipos, sin embargo, eso haría que los tipos no fueran muy "lisos" en su sabor. Probablemente hay muchas formas de evitar esto, aunque no es obvio para mí cuál sería la mejor.

EDITAR: Oh, así que con un poco de búsqueda en Google, encontré Qi , que parece ser muy similar a Lisp, excepto que está escrito de forma estática. Quizás sea un buen lugar para comenzar a ver dónde hicieron cambios para que la escritura estática esté allí.

Gian
fuente
Parece que la siguiente iteración después de Qi es Shen , desarrollado por la misma persona.
Diagon
4

Dylan: Ampliación del sistema de tipos de Dylan para una mejor inferencia de tipos y detección de errores

Rainer Joswig
fuente
El vínculo está muerto. Pero en cualquier caso, Dylan no está tipado estáticamente.
Björn Lindqvist
@ BjörnLindqvist: ese enlace era a una tesis sobre la adición de escritura gradual a Dylan.
Rainer Joswig
1
@ BjörnLindqvist: Me vinculé a un artículo general.
Rainer Joswig
Pero la escritura gradual no cuenta como escritura estática. Si lo hiciera, Pypy se escribiría estáticamente como Python, ya que también usa escritura gradual.
Björn Lindqvist
2
@ BjörnLindqvist: si agregamos tipos estáticos mediante escritura gradual y estos se verifican durante la compilación, entonces esto es escritura estática. No es que todo el programa esté escrito de forma estática, sino partes / regiones. homes.sice.indiana.edu/jsiek/what-is-gradual-typing 'Gradual typing es un sistema de tipos que desarrollé con Walid Taha en 2006 que permite que partes de un programa se tipeen dinámicamente y otras partes se tipeen estáticamente.'
Rainer Joswig