Representação da concatenação no nível de tipo

8

Eu gostaria de aprender mais sobre programação concatenativa através da criação de uma pequena linguagem simples, baseada na pilha e seguindo o paradigma concatenativo.

Infelizmente, não encontrei muitos recursos relacionados a linguagens concatenativas e sua implementação; portanto, desculpe-me antecipadamente por minha possível ingenuidade.

Portanto, defini minha linguagem como uma sequência simples de concatenação de funções, representada no AST como uma 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

O seguinte programa, 4 2 swap dup * +(correspondente a 2 * 2 + 4) uma vez analisado, fornecerá o seguinte AST:

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

Agora eu tenho que inferir e verificar os tipos.

Eu escrevi este sistema de tipos:

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

É aí que entra minha pergunta, porque não sei que tipo inferir dessa expressão. O tipo resultante é óbvio, é Int, mas eu não sei como realmente verificar totalmente esse programa no nível de tipo.

No começo, como você pode ver acima, pensei em um TConctipo que representa concatenação da mesma maneira que o TFuntipo representa uma função, porque no final a sequência de concatenação forma uma função única.

Outra opção, que ainda não explorei, seria aplicar a regra de inferência da composição da função a cada elemento dessa sequência de expressão. Não sei como isso funcionaria com o baseado em pilha.

A questão é a seguinte: como fazemos? Qual algoritmo usar e qual abordagem no nível de tipo deve ser preferida?

Foxy
fonte

Respostas:

9

Uma idéia importante das linguagens concatenativas é que a sintaxe e o domínio semântico formam monoides e a semântica é um homomorfismo monóide . A sintaxe é o monóide livre gerado pelas operações básicas, mais conhecido como lista. Sua operação é concatenação de lista, ou seja, (++)em Haskell. No contexto não digitado, o domínio semântico é apenas o monóide das funções finais (nas pilhas) com composição como a operação. Em outras palavras, um intérprete deve se parecer com o seguinte:

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 []

Criar um compilador ( muito ingênuo) é tão simples quanto. A única coisa que muda é o monoid alvo que será agora um monoid sintática construída a partir de um fragmento da sintaxe da língua-alvo, assim, interpretOpvai se tornar compileOp. Este monóide alvo pode ser sequências de afirmações com a operação de composição sequencial, ie ;. Você pode ser bem mais sofisticado .

Os sistemas de tipos para linguagens concatenativas não são tão óbvios e quase não existem linguagens concatenativas digitadas. Gato é o exemplo mais significativo que eu conheço. Uma maneira de começar a abordá-lo e experimentar alguns dos problemas que surgem é incorporar uma linguagem concatenativa em Haskell. Você descobre rapidamente que não deseja, add :: (Int, Int) -> Intpois isso não compõe. Em vez disso, você tem add :: (Int, (Int, s)) -> (Int, s). Isso funciona muito bem para coisas simples. Isso também é relativamente claramente tipos de linha do pobre homem. Um dos primeiros e mais significativos obstáculos que você enfrentará é lidar com cotações. O problema é que isso [add]deve corresponder a algo com um tipo como o s -> ((forall s'. (Int, (Int, s')) -> (Int, s')), s)que requer tipos de classificação mais alta e instanciação impredicativa. Gato parece ter ambos. Certamente possui tipos com classificação mais alta e substituirá um tipo de polímero por uma variável de tipo. Pode estar fazendo as coisas de uma maneira que possa ser entendida sem impredicatividade. Conseguir isso com uma incorporação em Haskell pode ser possível usando listas em nível de tipo, famílias de tipo (fechadas) e quantificação universal local. Neste ponto, porém, criar um sistema de tipo personalizado provavelmente faz mais sentido.

As operações com efeitos de pilha não uniformes também provavelmente serão problemáticas, mas, na maioria dos casos, faz sentido omiti-las e fornecer meios alternativos de fazer coisas que garantam uma pilha consistente.

Derek Elkins deixou o SE
fonte