如何在Haskell的嵌入式DSL中捕获高阶函数?

pkmbmrz7  于 11个月前  发布在  其他
关注(0)|答案(1)|浏览(135)

我想在Haskell中编写一个嵌入式DSL,从中我可以用另一种语言生成代码(即Python,但这与问题无关)。有很多不同的方法可以做到这一点,比如使用基于 Free monad 的解释器或我所知道的无标签解释器。但我所知道的方法似乎都不能捕获 * 函数定义 *,这是有道理的,但也是相当有限的。
我的问题是:如何将一个实际的Haskell函数嵌入到DSL中?这个想法是将一个Haskell函数定义 * 捕获 * 到类似Lam var body构造函数的东西中,它看起来像:

data Var = Var ...  -- use DeBruijn numbering?

data Expr repr = Lam Var repr

字符串
理想情况下,我希望能够写这样的东西:

foo :: Expr repr
foo = \ n -> n + n * 12


然后能够以各种方式解释这个Expr,包括生成外来代码。
我尝试了https://hackage.haskell.org/package/data-reify,它提供了一些捕获函数的技术,但没有得到很远。我正在寻找的是可能的吗?

krcsximq

krcsximq1#

我自己对Haskell还是个新手,然而,我最近自己也在做DSL,所以这是我的方法:
假设你有一个简单的AST,包括一个小的lambda演算:

data Exp :: Type -> Type where
  Var ::
    String ->       -- Name
    Exp a
  Apply ::
    Exp (a -> b) -> -- Function
    Exp a ->        -- Parameter
    Exp b
  Lambda ::
    String ->       -- Bound variable name
    Exp b ->        -- "Right hand side" of lambda
    Exp (a -> b)
  ...

deriving instance Show (Exp t)

字符串
请注意,上面的结构实际上不是类型安全的:不能确保绑定变量的类型与其单独使用的类型相同:

unsafe :: Exp (Int -> String)
unsafe = Lambda "x" (Var "x" :: Exp String)


将编译没有问题。(注意内部变量与lambda表达式的参数具有不同的类型。)事实上,上述AST甚至不保证引用的变量被外部表达式绑定。
因为我们更喜欢使用Haskell函数类型,所以只要不将lambda演算导出到其他包就足够了。
现在让我们将一元函数“转换”为AST的表达式。我们想用Var占位符替换给定变量的所有占位符,然后用lambda绑定该变量:

convert :: String -> (Exp a -> Exp b) -> Exp (a -> b)
convert varName f = Lambda varName . f $ Var varName

ghci> convert "x" $ \x -> x
Lambda "x" (Var "x")


目前,我们还没有一种方法来确定一个独特的变量名,每当我们想要绑定一个变量。然而,我们需要这样做,以避免变量名冲突可靠。
一个简单的方法是在编译器中全局跟踪绑定的变量的数量,并根据该数量生成一个新的变量名。根据lambda表达式的使用/编译方式,由于作用域的原因,这可能不是必需的。
使用类型类,我们可以将其推广到n元函数的convert-函数:

class Convertible t d where
  convert :: Int -> t -> Exp d

instance Convertible (Exp t) t where
  convert :: Int -> Exp t -> Exp t
  convert _ = id

instance (Convertible b c) => Convertible (Exp a -> b) (a -> c) where
  convert :: Int -> (Exp a -> b) -> Exp (a -> c)
  convert varCount f = Lambda var . convert (varCount + 1) . f $ Var var
    where
      var = "v" ++ show varCount


这已经很好了,但是会导致一些模棱两可的类型,因为我们将c声明为自由类型变量,所以理论上可以有多个转换函数类型的示例(本质上,c不是由Exp a -> b唯一确定的)。
作为一种解决方案,你可以使用一个封闭类型族来确定c

type family Dropped a :: Type where
  Dropped (Exp a -> b) = a -> Dropped b
  Dropped (Exp t) = t


例如:

Dropped (Exp Int) ~ Int,
Dropped (Exp Int -> Exp String) ~ Int -> String
and
Dropped (Exp a1 -> ... -> Exp an) ~ a1 -> ... -> an


现在我们可以从上面重写我们的类:

class Convertible t where
  convert :: Int -> t -> Exp (Dropped t)

instance Convertible (Exp t) where
  convert :: Int -> Exp t -> Exp t
  convert _ = id

instance (Convertible b) => Convertible (Exp a -> b) where
  convert :: Int -> (Exp a -> b) -> Exp (a -> Dropped b)
  convert varCount f = Lambda var . convert (varCount + 1) . f $ Var var
    where
      var = "v" ++ show varCount


现在我们可以方便地转换AST的函数:

ghci> :{
ghci| fstE :: Exp a -> Exp b -> Exp a
ghci| fstE x _ = x
ghci| :}
ghci> convert 0 fstE
Lambda "v0" (Lambda "v1" (Var "v0"))

注1

上面的实现并没有覆盖我们AST表达式的 * 所有 * 函数,而是以下形式的函数

Exp t1 -> ... -> Exp tn


但是,可以添加更多类型的功能。

注2

根据DSL的实现/设计,最好直接从函数转换为生成语言的AST,跳过中间的lambda演算。这甚至可以帮助取消Haskell的函数,这些函数总是curry的。

注3

你必须启用一些扩展来运行代码。这些应该足够了:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}


我希望这能帮助你解决你所描述的问题。

相关问题