Окончательный DSL без тегов с проблемами RValue LValue

Типизированные финальные переводчики без тегов - интересная альтернатива бесплатной монаде.

Но даже с довольно простым примером ToyLang в окончательном стиле без тегов появляются неоднозначные переменные типа.

ToyLang - это EDSL, который должен выглядеть примерно так:

toy :: ToyLang m => m (Maybe Int)
toy = do
    a <- int "a"       -- declare a variable and return a reference
    a .= num 1         -- set a to 1
    a .= a .+ num 1    -- add 1 to a
    ret a              -- returns a

Общая цель, конечно же, состоит в том, чтобы максимально использовать систему типов Haskell в этом EDSL и использовать полиморфизм для создания экземпляров различных интерпретаторов.

Все было бы хорошо, если бы не операция (.+), которая приводит к понятию lvalue и rvalue: оператор присваивания (.=) имеет lvalue слева и либо lvalue или rvalue справа. Основная идея взята из двух комментариев в Необычные полиморфизмы, вариант использования:

{-# LANGUAGE GADTs #-}

data L -- dummies for Expr (see the comments for a better way)
data R

-- An Expr is either a lvalue or a rvalue
data Expr lr where
    Var :: String -> Maybe Int -> Expr L
    Num :: Maybe Int -> Expr R

-- tagless final style
class Monad m => ToyLang m where
    int :: String -> m (Expr L)             -- declare a variable with name
    (.=) :: Expr L -> Expr lr -> m (Expr L) -- assignment
    (.+) :: Expr lr -> Expr lr' -> Expr R   -- addition operation - TROUBLE!
    ret :: Expr lr -> m (Maybe Int)         -- return anything
    end :: m ()                             -- can also just end

"Переводчик" с красивым шрифтом начинался бы так:

import Control.Monad.Writer.Lazy

-- A ToyLang instance that just dumps the program into a String
instance ToyLang (Writer String) where
    int n = do
        tell $ "var " <> n <> "\n"
        return (Var n Nothing)
    (.=) (Var n _) e = do
        tell $ n <> " = " <> toString e <> "\n"
        return $ Var n (toVal e)
    ...

где маленький помощник toString должен выкопать значения из слагаемых GADT:

toString :: Expr lr -> String
toString (Var n mi) = n
toString (Num i)    = show i

Умный конструктор num просто

num :: Int -> Expr R
num = Num . Just

(.+) неприятен по двум причинам:

  1. (.+) находится не в монаде m, потому что в противном случае мы не можем написать a .= a + num 1, но, например, для экземпляра Writer String монада необходима для tell.

  2. Проверка типов лает на неоднозначные типы, созданные (.+) :: Expr lr -> Expr lr' -> Expr R. Понятно, что без дальнейших аннотаций он не может решить, какой экземпляр имеется в виду. Но комментирование такого пункта, как a .= a .+ num 1, если это вообще возможно, сделает DSL очень неуклюжим.

Один из способов заставить типы работать, переместив (.+) в монаду до некоторой степени, и (.=) тоже:

 class Monad m => ToyLang m where
    ...
    (.=) :: Expr L -> m (Expr lr) -> m (Expr L)
    (.+) :: Expr lr -> m (Expr lr') -> m (Expr R)
    ...

Все это странно, хотя:

  • (.=) и (.+) асимметричны там, где им нужна монада m, а где нет.

  • Даже в монаде Writer String я вынужден выполнять целочисленную арифметику, чтобы создать тип возвращаемого значения m (Expr R), хотя в действительности нет необходимости в результате

  • Инстанцирование ToyLang как Writer String выглядит аккуратно, но на самом деле не выполняет свою работу. a .= a .+ num 1 не может быть красиво напечатан как таковой, потому что a .+ num 1 оценивается (следовательно, печатается) до .=.

Это как-то все неправильно, я чувствую. Есть ли лучший способ сделать это?

Исходный код этого примера ToyLang находится на github.

Рекомендации:

Ответы