Типизированные финальные переводчики без тегов - интересная альтернатива бесплатной монаде.
Но даже с довольно простым примером 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