Typed abstract syntax and DSL design in Haskell

前端 未结 3 949
别跟我提以往
别跟我提以往 2021-02-06 15:32

I\'m designing a DSL in Haskell and I would like to have an assignment operation. Something like this (the code below is just for explaining my problem in a limited context, I d

3条回答
  •  闹比i
    闹比i (楼主)
    2021-02-06 16:10

    You should know that your goals are quite lofty. I don't think you will get very far treating your variables exactly as strings. I'd do something slightly more annoying to use, but more practical. Define a monad for your DSL, which I'll call M:

    newtype M a = ...
    
    data Exp a where
        ... as before ...
    
    data Var a  -- a typed variable
    
    assign :: Var a -> Exp a -> M ()
    declare :: String -> a -> M (Var a)
    

    I'm not sure why you have Exp a for assignment and just a for declaration, but I reproduced that here. The String in declare is just for cosmetics, if you need it for code generation or error reporting or something -- the identity of the variable should really not be tied to that name. So it's usually used as

    myFunc = do
        foobar <- declare "foobar" 42
    

    which is the annoying redundant bit. Haskell doesn't really have a good way around this (though depending on what you're doing with your DSL, you may not need the string at all).

    As for the implementation, maybe something like

    data Stmt = forall a. Assign (Var a) (Exp a)
              | forall a. Declare (Var a) a
    
    data Var a = Var String Integer  -- string is auxiliary from before, integer
                                     -- stores real identity.
    

    For M, we need a unique supply of names and a list of statements to output.

    newtype M a = M { runM :: WriterT [Stmt] (StateT Integer Identity a) }
        deriving (Functor, Applicative, Monad)
    

    Then the operations as usually fairly trivial.

    assign v a = M $ tell [Assign v a]
    
    declare name a = M $ do
        ident <- lift get
        lift . put $! ident + 1
        let var = Var name ident
        tell [Declare var a]
        return var
    

    I've made a fairly large DSL for code generation in another language using a fairly similar design, and it scales well. I find it a good idea to stay "near the ground", just doing solid modeling without using too many fancy type-level magical features, and accepting minor linguistic annoyances. That way Haskell's main strength -- it's ability to abstract -- can still be used for code in your DSL.

    One drawback is that everything needs to be defined within a do block, which can be a hinderance to good organization as the amount of code grows. I'll steal declare to show a way around that:

    declare :: String -> M a -> M a
    

    used like

    foo = declare "foo" $ do
        -- actual function body
    

    then your M can have as a component of its state a cache from names to variables, and the first time you use a declaration with a certain name you render it and put it in a variable (this will require a bit more sophisticated monoid than [Stmt] as the target of your Writer). Later times you just look up the variable. It does have a rather floppy dependence on uniqueness of names, unfortunately; an explicit model of namespaces can help with that but never eliminate it entirely.

提交回复
热议问题