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