带有RValue LValue问题的无标签最终DSL

mcm*_*yer 11 dsl haskell tagless-final

类型化的无标签最终解释器免费的monad方法的一种有趣替代方法。

但是,即使使用无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
Run Code Online (Sandbox Code Playgroud)

当然,总的目标是在此EDSL中尽可能多地使用Haskell类型系统,并使用多态来实例化各种解释器。

如果不是用于(.+)导致左值和右值概念的运算,那么一切都会很好:赋值运算符(.=)的左侧是左值,而右侧是左值或右值。基本思想来自于“ 强制多态性”中的两个注释,一个用例

{-# 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
Run Code Online (Sandbox Code Playgroud)

漂亮的“解释器”将像这样开始:

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)
    ...
Run Code Online (Sandbox Code Playgroud)

小帮手toString必须从GADT的加法中找出值:

toString :: Expr lr -> String
toString (Var n mi) = n
toString (Num i)    = show i
Run Code Online (Sandbox Code Playgroud)

聪明的构造函数num很简单

num :: Int -> Expr R
num = Num . Just
Run Code Online (Sandbox Code Playgroud)

(.+)很麻烦,原因有两个:

  1. (.+)不在monad中,m因为否则我们将无法编写a .= a + num 1,但Writer String例如,需要monad才能tell

  2. 类型检查器将阻止由创建的歧义类型(.+) :: Expr lr -> Expr lr' -> Expr R。显然,如果没有进一步的注释,它将无法确定该实例的含义。但是a .= a .+ num 1,如果有可能,对这样的子句进行注释将使DSL非常笨拙。

使类型起作用的一种方法是(.+)在某种程度上进入monad,并且(.=)

 class Monad m => ToyLang m where
    ...
    (.=) :: Expr L -> m (Expr lr) -> m (Expr L)
    (.+) :: Expr lr -> m (Expr lr') -> m (Expr R)
    ...
Run Code Online (Sandbox Code Playgroud)

尽管这一切都很奇怪:

  • (.=)并且(.+)是在他们需要的单子不对称m和他们没有。

  • 即使在Writer Stringmonad中,我也不得不执行整数算术以创建m (Expr R)返回类型,尽管实际上并不需要结果

  • 实例化ToyLang一个Writer String看起来整洁,但并没有真正做的工作。a .= a .+ num 1不能这样漂亮地打印,因为a .+ num 1在之前进行评估(因此打印).=

我觉得这完全是错误的。有一个更好的方法吗?

ToyLang示例的源代码在github上

参考文献: