Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell: Convert list to data

I have a lot of code like this:

data Post =
    Post
    { postOwner :: Integer
    , postText :: ByteString
    , postDate :: ByteString
    }

sqlToPost :: [SqlValue] -> Post
sqlToPost [owner, text, date] = Post (fromSql owner) (fromSql text) (fromSql date)

(Library used here is HDBC). In future, there will be lot of data like Post and function like sqlToVal. Can I reduce this boilerplate code for sqlToVal?

like image 590
demi Avatar asked May 18 '12 13:05

demi


1 Answers

Template Haskell code generation is a very advanced topic. Still, if you master the art of TH it is possible to use said technique to generate the code that you're looking for.

Note that the following code will only work for data types with only one constructor (E.g. not data Foo = A String Int | B String Int, which has two constructors A and B) because you didn't say how that should be handled in the code.

We will make a Template Haskell function that runs at compile time, takes the name of a data type, and generates a function called sqlTo<nameofdatatype>. This function looks like this:

module THTest where

import Control.Monad (replicateM)

-- Import Template Haskell
import Language.Haskell.TH
-- ...and a representation of Haskell syntax
import Language.Haskell.TH.Syntax

-- A function that takes the name of a data type and generates a list of
-- (function) declarations (of length 1).
makeSqlDeserializer :: Name -> Q [Dec]
makeSqlDeserializer name = do
  -- Look up some information about the name. This gets information about what
  -- the name represents.
  info <- reify name

  case info of
    -- Is the name a type constructor (TyConI) of a data type (DataD), with
    -- only one normal constructor (NormalC)? Then, carry on.
    -- dataName is the name of the type, constrName of the constructor, and
    -- the paramTypes are the constructor parameter types.
    -- So, if we have `data A = B String Int`, we get
    -- dataName = A, constrName = B, paramTypes = [String, Int]
    TyConI (DataD _ dataName _ [NormalC constrName paramTypes] _) -> do

      -- If the dataName has a module name (Foo.Bar.Bla), only return the data
      -- name (Bla)
      let dataBaseName = nameBase dataName

      -- Make a function name like "sqlToBla"
      let funcName = mkName $ "sqlTo" ++ dataBaseName

      -- Also access the "fromSql" function which we need below.
      let fromSqlName = mkName "Database.HDBC.fromSql"

      -- Count how many params our data constructor takes.
      let numParams = length paramTypes

      -- Create numParams new names, which are variable names with random
      -- names.
      -- This could create names like [param1, param2, param3] for example,
      -- but typically they will look like
      -- [param[aV2], param[aV3], param[aV4]]
      paramNames <- replicateM numParams $ newName "param"

      -- The patterns are what's on the left of the `=` in the function, e.g.
      -- sqlToBla >>>[param1, param2, param3]<<< = ...
      -- We make a list pattern here which matches a list of length numParams
      let patterns = [ListP $ map VarP paramNames]

      -- The constructor params are the params that are sent to the
      -- constructor:
      -- ... = Bla >>>(fromSql param1) (fromSql param2) (fromSql param3)<<<
      let constrParams = map (AppE (VarE fromSqlName) . VarE) paramNames

      -- Make a body where we simply apply the constructor to the params
      -- ... = >>>Bla (fromSql param1) (fromSql param2) (fromSql param3)<<<
      let body = NormalB (foldl AppE (ConE constrName) constrParams)

      -- Return a new function declaration that does what we want.
      -- It has only one clause with the patterns that are specified above.
      -- sqlToBla [param1, param2, param3] =
      --   Bla (fromSql param1) (fromSql param2) (fromSql param3)
      return [FunD funcName [Clause patterns body []]]

Now, we use this function like so (Note the LANGUAGE pragma which enables Template Haskell):

{-# LANGUAGE TemplateHaskell #-}

-- The module that defines makeSqlDeserializer (must be in a different module!)
import THTest

-- Also import the fromSql function which is needed by the generated function.
import Database.HDBC

-- Declare the data type
data Bla = Bla String Int deriving (Show)

-- Generate the sqlToBla function
makeSqlDeserializer ''Bla

If you want to see the function that is generated, simply pass -ddump-splices to GHC when compiling. The output is something like this:

test.hs:1:1: Splicing declarations
    makeSqlDeserializer 'Bla
  ======>
    test.hs:7:1-25
    sqlToBla [param[aV2], param[aV3]]
      = Bla (Database.HDBC.fromSql param[aV2]) (Database.HDBC.fromSql param[aV3])
like image 141
dflemstr Avatar answered Oct 11 '22 17:10

dflemstr