5

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?

5
  • There are different database libraries that will do such boilerplate-generation for you. However, HDBC is known for giving you a very "raw" database interface, so I don't think that there's an abstraction like the one you're looking for. Commented May 18, 2012 at 13:29
  • Well, I learn Haskell and I found interesting how technically this made. Can you point some library where I can see how this solved? Commented May 18, 2012 at 13:34
  • 2
    There are two ways to solve it: Either use Template Haskell which will generate a different sqlToSomething function for each datatype, or use GHC Generics to tell the compiler how to deserialize any data type automatically. The persistent library uses the first method extensively; there's a tutorial available. There might also be a solution for HDBC, I just haven't heard about it. I could also just show you how to explicitly generate sqlToBla functions, without pre-existing code, with TH. Commented May 18, 2012 at 13:41
  • 1
    Why not in answers? Don't want some points? ;) Commented May 18, 2012 at 13:47
  • Please show how to generate sqlToXXX with TH please! Commented May 18, 2012 at 14:01

1 Answer 1

6

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])
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.