Типизированные финальные переводчики без тегов - интересная альтернатива бесплатной монаде.
Но даже с довольно простым примером 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
(.+) неприятен по двум причинам:
(.+)находится не в монадеm, потому что в противном случае мы не можем написатьa .= a + num 1, но, например, для экземпляраWriter Stringмонада необходима дляtell.Проверка типов лает на неоднозначные типы, созданные
(.+) :: 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.
Рекомендации:
- Типизированные финальные переводчики без тегов Олега Киселева
- Необычный полиморфизм, вариант использования by augustss
- Почему Free Monads Matter Габриэля Гонсалеса
- Окончательное кодирование без тегов в Haskell от J. P. Royo Sales