TL;DR: I need help figuring out how to generate code that will return one of a small number of data types (probably just Double and Bool) from various fields on disparate records.
Long form: Assuming the following data types
data Circle = Circle { radius :: Integer, origin :: Point }
data Square = Square { side :: Integer }
and some boilerplate code
circle = Circle 3 (Point 0 0)
square = Square 5
I'm building a small DSL, and want the user to be write something like the following
circle.origin
square.side
and it will generate code similar to
origin . circle
side . square
In parsing this, I would have the strings "circle" and "origin" for example. I now need to turn those into function calls. I could obviously have something like this:
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
and have one lookup function per returned data type. Having one function per data type is practical from the DSL point of view in that it will only really deal with 2 or 3 data types. However, this hardly seems like a particularly effective way of doing things. Is there a better way (surely) of doing this? If not, is there a way that I can generate the code for the various lookup functions from the various records that I want to be able to lookup fields from?
Secondly, there's still the matter of the parsed "circle"
or "square"
needing to call the appropriate circle
or square
function. If I were to implement this using type classes, I could do something like:
instance Lookup Circle where
lookupF2I "radius" = Just $ IntegerE radius
lookupF2I _ = Nothing
lookupF2P "origin" = Just $ PointE origin
lookupF2P _ = Nothing
but then that leaves me with having to figure out which type to enforce on the lookup function, and worse having to hand write instances for each (of many) records that I want to use this on.
Note: The fact that Circle
and Square
could be represented using a single ADT is incidental to my question in that this is a contrived example. The actual code will entail various very different records, of which the only thing they have in common is having fields of the same type.
I tried using Template Haskell to provide a nice and type safe way to solve this problem. To do this, I constructed the expressions from a given string.
I suppose that Lens package can do that, but it may be a simpler and more flexible solution.
It can be used like this:
import THRecSyntax
circleOrigin = compDSL "circle.origin.x"
And is defined like this:
{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
compDSL :: String -> Q Exp
compDSL s = return
$ foldr1 AppE
$ map (VarE . mkName)
(reverse $ splitEvery '.' s)
So the result expression will be: x (origin circle)
Note: splitEvery
is a function that splits a list into sublists taking out the given element. Example implementation:
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)
This is a heavyweight but type-safe way to create an embedded DSL with the given syntax.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With