Representación de la concatenación a nivel de tipo.

8

Me gustaría aprender más sobre la programación concatenante a través de la creación de un lenguaje pequeño y simple, basado en la pila y siguiendo el paradigma de la concatenación.

Desafortunadamente, no he encontrado muchos recursos relacionados con los lenguajes concatenantes y su implementación, así que discúlpeme de antemano por mi posible ingenuidad.

Por lo tanto, definí mi lenguaje como una secuencia simple de concatenación de funciones, representada en el AST como una lista:

data Operation
    = Concat [Operation]
    | Quotation Operation
    | Var String
    | Lit Literal
    | LitOp LiteralOperation

data Literal
    = Int Int
    | Float Float

data LiteralOperation
    = Add | Sub | Mul | Div

El siguiente programa, 4 2 swap dup * +(correspondiente a 2 * 2 + 4) una vez analizado, proporcionará el siguiente AST:

Concat [Lit (Int 4), Lit (Int 2), Var "swap", Var "dup", LitOp Mul, LitOp Add]

Ahora tengo que inferir y verificar los tipos.

Escribí este tipo de sistema:

data Type
    = TBasic BasicType   -- 'Int' or 'Float'
    | TVar String        -- Variable type
    | TQuoteE String     -- Empty stack, noted 'A'
    | TQuote String Type -- Non empty stack, noted 'A t'
    | TConc Type Type    -- A type for the concatenation
    | TFun Type Type     -- The type of functions

Ahí es donde entra mi pregunta, porque no sé qué tipo inferir de esa expresión. El tipo resultante es obvio, lo es Int, pero no sé cómo verificar completamente este programa a nivel de tipo.

Al principio, como puede ver arriba, había pensado en un TConctipo que representa la concatenación de la misma manera que el TFuntipo representa una función, porque al final la secuencia de concatenación forma una función única.

Otra opción, que aún no he explorado, sería aplicar la regla de inferencia de composición de funciones a cada elemento de esta secuencia de expresión. No sé cómo funcionaría con el basado en pila.

La pregunta es así: ¿cómo lo hacemos? ¿Qué algoritmo usar y qué enfoque a nivel de tipo debería preferirse?

astuto
fuente

Respuestas:

9

Una idea importante de los lenguajes concatenativos es que la sintaxis y el dominio semántico forman monoides y la semántica es un homomorfismo monoide . La sintaxis es el monoide libre generado por las operaciones básicas, mejor conocido como una lista. Su operación es concatenación de listas, es decir, (++)en Haskell. En el contexto sin tipo, el dominio semántico es solo el monoide de las funciones finales (en pilas) con composición como la operación. En otras palabras, un intérprete debería tener el siguiente aspecto:

data Op = PushInt Int| Call Name | Quote Code | Add | ... -- etc.
type Code = [Op]

-- Run-time values
data Value = Q (Endo Stack) | I Int | ... -- etc.
type Stack = [Value]

-- You'd probably add an environment of type Map Name (Endo Stack)
interpretOp :: Op -> Endo Stack
interpretOp (PushInt n) = Endo (I n:)
interpretOp (Quote c) = Endo (Q (interpetCode c):)
interpretOp op = ... -- etc.

interpretCode :: Code -> Endo Stack
interpretCode = foldMap interpretOp

runCode :: Code -> Stack
runCode code = case interpretCode code of Endo f -> f []

Hacer un compilador ( muy ingenuo) es igual de simple. Lo único que cambia es el monoide objetivo que ahora será un monoide sintáctica construido a partir de un fragmento de la sintaxis de la lengua meta de este modo interpretOpse convertirá compileOp. Este monoide objetivo puede ser secuencias de enunciados con la operación de composición secuencial, es decir ;. Sin embargo, puedes ser mucho más sofisticado .

Los sistemas de tipos para lenguajes concatenativos no son tan obvios, y casi no hay lenguajes concatenativos mecanografiados. Cat es el ejemplo más significativo que conozco. Una forma de comenzar a abordarlo y experimentar algunos de los problemas que surgen es incrustar un lenguaje concatenante en Haskell. Rápidamente descubres que no quieres, add :: (Int, Int) -> Intya que esto no compondrá. En cambio, tienes add :: (Int, (Int, s)) -> (Int, s). Esto funciona extremadamente bien para cosas simples. Esto también es relativamente claro en los tipos de filas de los pobres. Uno de los primeros y más importantes obstáculos que enfrentarás es el de las citas. El problema es que [add]debería corresponder a algo con un tipo como el s -> ((forall s'. (Int, (Int, s')) -> (Int, s')), s)que requiere tipos de rango superior e instanciación impredecible. Cat parece tener ambos. Ciertamente tiene tipos mejor clasificados, y sustituirá un politipo por una variable de tipo. Puede estar haciendo las cosas de una manera que se pueda entender sin impredicatividad. Lograr esto con una incrustación en Haskell podría hacerse utilizando listas de nivel de tipo, familias de tipos (cerradas) y cuantificación universal local. Sin embargo, en este punto, crear un sistema de tipo personalizado probablemente tenga más sentido.

Las operaciones con efectos de pila no uniformes también pueden ser problemáticas, pero, en la mayoría de los casos, tiene sentido simplemente omitirlas y proporcionar medios alternativos para hacer cosas que garanticen una pila consistente.

Derek Elkins dejó SE
fuente