2012-05-18 10 views
5

mam dużo kodu tak:Haskell: lista Konwersja danych

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

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

(Biblioteka użyte tutaj jest HDBC). W przyszłości będzie wiele danych, takich jak Post i funkcji takich jak sqlToVal. Czy mogę zredukować ten kod dla standardowego sqlToVal?

+0

Istnieją różne biblioteki baz danych, które wykonają dla Ciebie takie generatory. Jednak HDBC znany jest z tego, że oferuje bardzo "surowy" interfejs bazy danych, więc nie sądzę, że istnieje abstrakcja podobna do tej, której szukasz. – dflemstr

+0

Cóż, uczę się Haskella i odkryłem ciekawe, jak technicznie to się stało. Czy możesz wskazać bibliotekę, w której widzę, jak to rozwiązano? – demi

+2

Są dwa sposoby na rozwiązanie tego problemu: użyj szablonu Haskell, który wygeneruje inną funkcję 'sqlToSomething' dla każdego typu danych, lub użyj GHC Generics, aby poinformować kompilator, jak automatycznie przekształcić * dowolny typ' 'data'. Biblioteka ['persistent'] (http://hackage.haskell.org/package/persistent) używa szeroko tej pierwszej metody; dostępny jest [samouczek] (http://www.yesodweb.com/book/persistent). Może być również rozwiązanie dla HDBC, po prostu o tym nie słyszałem. Mogę też po prostu pokazać ci, jak jawnie generować funkcje 'sqlToBla', bez wcześniejszego kodu, za pomocą TH. – dflemstr

Odpowiedz

6

Szablon Generowanie kodu Haskella jest bardzo zaawansowanym tematem. Mimo to, jeśli opanujesz sztukę TH, możesz użyć tej techniki do wygenerowania kodu, którego szukasz.

Zauważ, że ten kod zadziała tylko dla data typów tylko jeden konstruktor (na przykład nie data Foo = A String Int | B String Int, który ma dwa konstruktory A i B), ponieważ nie powiedzieć, w jaki sposób należy obchodzić się w kodzie.

Wykonamy funkcję szablonu Haskella, która działa w czasie kompilacji, pobiera nazwę typu danych i generuje funkcję o nazwie sqlTo<nameofdatatype>. Funkcja ta wygląda następująco:

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

Teraz możemy korzystać z tej funkcji jak tak (Uwaga LANGUAGE pragmy który umożliwia 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 

Jeśli chcesz zobaczyć funkcji, który jest generowany, wystarczy przekazać -ddump-splices do GHC podczas kompilacji. Dane wyjściowe są następujące:

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