2012-05-18 19 views
5

Tengo un montón de código como este:Haskell: Convertir a la lista de datos

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

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

(Biblioteca utiliza aquí es HDBC). En el futuro, habrá muchos datos como Post y funcionarán como sqlToVal. ¿Puedo reducir este código repetitivo para sqlToVal?

+0

Existen diferentes bibliotecas de bases de datos que harán la generación de palabras clave para usted. Sin embargo, HDBC es conocido por brindarle una interfaz de base de datos muy "en bruto", por lo que no creo que haya una abstracción como la que está buscando. – dflemstr

+0

Bueno, me enteré de Haskell y me pareció interesante cómo técnicamente esto hizo. ¿Puedes señalar alguna biblioteca donde pueda ver cómo se resuelve? – demi

+2

Hay dos formas de resolverlo: use Template Haskell que generará una función 'sqlToSomething' diferente para cada tipo de datos, o use Generics de GHC para decirle al compilador cómo deserializar * cualquier tipo de' 'datos' automáticamente. La biblioteca ['persistent'] (http://hackage.haskell.org/package/persistent) usa el primer método extensamente; hay [un tutorial] (http://www.yesodweb.com/book/persistent) disponible. También podría haber una solución para HDBC, simplemente no he oído hablar de eso. También podría mostrarle cómo generar explícitamente funciones 'sqlToBla', sin código preexistente, con TH. – dflemstr

Respuesta

6

Plantilla La generación de código Haskell es un tema muy avanzado. Aún así, si dominas el arte de TH, es posible usar dicha técnica para generar el código que estás buscando.

Tenga en cuenta que el siguiente código sólo funcionará para data tipos con sólo un constructor (Por ejemplo, no data Foo = A String Int | B String Int, que tiene dos constructores A y B) porque usted no dijo cómo debería ser manejado en el código.

Haremos una función de plantilla Haskell que se ejecuta en tiempo de compilación, toma el nombre de un tipo de datos y genera una función llamada sqlTo<nameofdatatype>. Esta función tiene el siguiente aspecto:

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 []]] 

Ahora, utilizamos esta función como tal (Nota del pragma LANGUAGE que permite Plantilla 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 

Si quieres ver la función que se genera, simplemente pase -ddump-splices a GHC al compilar. La salida es algo como esto:

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])