Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Working with a list in TemplateHaskell

Here's the tutorial I'm working from.

He has an example, tupleReplicate, which returns a function that takes a value and replicates it:

tupleReplicate :: Int -> Q Exp
tupleReplicate n = do id <- newName "x"
                      return $ LamE (VarP id)
                                    (TupE $ replicate n $ VarE id)

So VarE id returns an expression that can then be used with replicate? My question is, how would this work if id was a list? I want to do something like:

let vals = mkName "vals"
LamE (ListP vals) (map reverse $ ListE vals)

except that doesn't work because ListE returns Exp, not [Exp].

More generally I want to write a function that takes a list and applies a function to it, in TemplateHaskell.


There's some example code here, and I'm trying to write a function like

makeModel (id_ : name_ : []) = Person (fromSql id_) (fromSql name_)
like image 298
Vlad the Impala Avatar asked Nov 03 '22 22:11

Vlad the Impala


1 Answers

First, let's turn on some extensions:

{-# LANGUAGE FlexibleInstances, TemplateHaskell #-}

import Language.Haskell.TH

Now I'll fake some data types and classes to keep the interaction with the Real World low:

data Person = Person Int String deriving Show

class SQL a where 
  fromSql :: String -> a

instance SQL Int where fromSql = read
instance SQL String where fromSql = id -- This is why I needed FlexibleInstances

OK, now we need to decide what code we want to generate. Sticking closely to your example, we could define makeModel as a lambda expression (translation underneath):

LamE [ListP [VarP id,VarP name]] (AppE (AppE (ConE Person) (AppE (VarE fromSql) (VarE id))) (AppE (VarE fromSql) (VarE name)))
\       [         id,     name ] -> (    (         Person     (        fromSql        id ))   (         fromSql        name ))
\       [         id,     name ] ->                Person $            fromSql        id   $            fromSql        name 

(I don't speak fluent Exp, I did runQ [| \[id,name] -> Person (fromSql id) (fromSql name) |] in ghci!)

I've chosen to use strings do define the identifiers id and name, because you could read that from the table, but you could just as well generate identifiers called field_1 etc.

makeMakeModel qFieldNames qMapFunction qConstructor =  -- ["id","name"] 'fromSql 'Person
      LamE [ListP (map VarP qFieldNames)]              -- \ [id,name]
           $ foldl AppE (ConE qConstructor)            -- Person  
                        [AppE (VarE qMapFunction) (VarE name)|name <- qFieldNames]
                                                       -- $ id $ name

makeModel fieldNames mapFunction constructor = do
   names <- mapM newName fieldNames
   return $ makeMakeModel names mapFunction constructor

In action in ghci -XTemplateHaskell:

*Main> runQ $ makeModel ["id","name"] 'fromSql 'Person
LamE [ListP [VarP id_0,VarP name_1]] (AppE (AppE (ConE Main.Person) (AppE (VarE Main.fromSql) (VarE id_0))) (AppE (VarE Main.fromSql) (VarE name_1)))

*Main> $(makeModel ["id","name"] 'fromSql 'Person) ["1234","James"]
Person 1234 "James"

Notice how the identifiers we made with newName have got serial numbers to make them unique, whereas the ones we passed in with a dash in front, 'fromSql and 'Person are preserved as their actual definitions.


If you'd rather not use a lambda expression, you can use

runQ [d| makeModel [id,name] = Person (fromSql id) (fromSql name) |]

as your starting point - [d| ... |] is for function definitions.

like image 194
AndrewC Avatar answered Nov 13 '22 14:11

AndrewC