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

luk*_*all 7 dsl haskell

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

长格式:假设以下数据类型

data Circle = Circle { radius :: Integer, origin :: Point }
data Square = Square { side  :: Integer }
Run Code Online (Sandbox Code Playgroud)

和一些样板代码

circle = Circle 3 (Point 0 0)
square = Square 5
Run Code Online (Sandbox Code Playgroud)

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

circle.origin
square.side
Run Code Online (Sandbox Code Playgroud)

它会生成类似的代码

origin . circle
side . square
Run Code Online (Sandbox Code Playgroud)

在解析这个时,我会将字符串"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
Run Code Online (Sandbox Code Playgroud)

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

其次,仍然存在解析"circle""square"需要调用适当circlesquare函数的问题.如果我要使用类型类来实现它,我可以做类似的事情:

instance Lookup Circle where
    lookupF2I "radius" = Just $ IntegerE radius
    lookupF2I _        = Nothing
    lookupF2P "origin" = Just $ PointE origin
    lookupF2P _        = Nothing
Run Code Online (Sandbox Code Playgroud)

但随后离开我必须弄清楚对查找函数执行哪种类型,并且更糟糕不必手工编写实例为每个(很多),我想用这个记录上.

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

Bol*_*eth 1

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

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

它可以这样使用:

import THRecSyntax
circleOrigin = compDSL "circle.origin.x"
Run Code Online (Sandbox Code Playgroud)

并且定义如下:

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

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

所以结果表达式将是: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)
Run Code Online (Sandbox Code Playgroud)

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