如何构建DSL以从Haskell中的记录中查找字段

时间:2012-11-02 16:50:23

标签: haskell dsl

TL; DR:我需要帮助找出如何生成代码,这些代码将从不同记录的各个字段返回少量数据类型之一(可能只是Double和Bool)。

长篇:假设以下数据类型

data Circle = Circle { radius :: Integer, origin :: Point }
data Square = Square { side  :: Integer }

和一些样板代码

circle = Circle 3 (Point 0 0)
square = Square 5

我正在构建一个小型DSL,并希望用户编写如下内容

circle.origin
square.side

它将生成类似于

的代码
origin . circle
side . square

在解析这个时,我会将字符串“circle”和“origin”作为例子。我现在需要把它们变成函数调用。我显然可以这样:

data Expr a = IntegerE (a -> Integer)
            | PointE (a -> Point)

lookupF2I "side"   = Just $ IntegerE side
lookupF2I "radius" = Just $ IntegerE radius
lookupF2I _        = Nothing

lookupF2P "origin" = Just $ PointE origin
lookupF2P _ = Nothing

每个返回的数据类型都有一个查找函数。从DSL的角度来看,每种数据类型具有一个功能是实用的,因为它只能真正处理2或3种数据类型。然而,这似乎不是一种特别有效的做事方式。有没有更好的方法(肯定)这样做?如果没有,有没有办法可以从我希望能够从中查找字段的各种记录生成各种查找函数的代码?

其次,解析后的"circle""square"仍然需要调用相应的circlesquare函数。如果我要使用类型类来实现它,我可以做类似的事情:

instance Lookup Circle where
    lookupF2I "radius" = Just $ IntegerE radius
    lookupF2I _        = Nothing
    lookupF2P "origin" = Just $ PointE origin
    lookupF2P _        = Nothing

但是这让我不得不弄清楚要在查找函数上强制执行哪种类型,更糟糕的是,我必须为每个(很多)记录写入实例,我希望在

注意:使用单个ADT可以表示CircleSquare的事实是我的问题附带的事实,因为这是一个人为的例子。实际代码将包含各种非常不同的记录,其中唯一的共同点是具有相同类型的字段。

1 个答案:

答案 0 :(得分:1)

我尝试使用Template Haskell来提供一种很好的和类型安全的方法来解决这个问题。为此,我从给定的字符串构造了表达式。

我认为Lens包可以做到这一点,但它可能是一个更简单,更灵活的解决方案。

可以像这样使用:

import THRecSyntax
circleOrigin = compDSL "circle.origin.x"

并且定义如下:

{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH

compDSL :: String -> Q Exp
compDSL s = return 
            $ foldr1 AppE 
            $ map (VarE . mkName) 
            (reverse $ splitEvery '.' s)

因此结果表达式为:x (origin circle)

注意:splitEvery是一个将列表拆分为取出给定元素的子列表的函数。示例实现:

splitEvery :: Eq a => a -> [a] -> [[a]]
splitEvery elem s = splitter (s,[])
  where splitter (rest, res) = case elemIndex elem rest of
            Just dotInd -> let (fst,rest') = splitAt dotInd rest
                            in splitter (tail rest', fst : res)
            Nothing -> reverse (rest : res)

这是一种重量级但类型安全的方法,可以使用给定的语法创建嵌入式DSL。