Haskell:将列表转换为数据

Haskell:将列表转换为数据,haskell,type-conversion,Haskell,Type Conversion,我有很多这样的代码: data Post = Post { postOwner :: Integer , postText :: ByteString , postDate :: ByteString } sqlToPost :: [SqlValue] -> Post sqlToPost [owner, text, date] = Post (fromSql owner) (fromSql text) (fromSql date) module

我有很多这样的代码:

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

sqlToPost :: [SqlValue] -> Post
sqlToPost [owner, text, date] = Post (fromSql owner) (fromSql text) (fromSql date)
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 []]]
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])

(这里使用的库是HDBC)。在未来,将有大量的数据,如
Post
和函数,如
sqlToVal
。我可以为
sqlToVal
减少这个样板代码吗?

模板Haskell代码生成是一个非常高级的主题。不过,如果你掌握了TH的艺术,就有可能使用上述技术生成你正在寻找的代码

请注意,以下代码仅适用于只有一个构造函数的
数据
类型(例如,not
data Foo=A String Int | B String Int
,它有两个构造函数
A
B
),因为您没有说明在代码中应该如何处理

我们将创建一个在编译时运行的模板Haskell函数,它采用数据类型的名称,并生成一个名为
sqlTo
的函数。此函数如下所示:

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

sqlToPost :: [SqlValue] -> Post
sqlToPost [owner, text, date] = Post (fromSql owner) (fromSql text) (fromSql date)
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 []]]
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])
如果要查看生成的函数,只需在编译时将
-ddump拼接传递给GHC即可。输出如下所示:

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

sqlToPost :: [SqlValue] -> Post
sqlToPost [owner, text, date] = Post (fromSql owner) (fromSql text) (fromSql date)
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 []]]
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])

有不同的数据库库可以为您生成这样的样板文件。然而,HDBC以提供一个非常“原始”的数据库接口而闻名,所以我不认为有一个像你正在寻找的那样的抽象。好吧,我了解了Haskell,我发现这在技术上是多么有趣。你能指出一些我可以看到如何解决这个问题的库吗?有两种解决方法:要么使用Template Haskell为每个数据类型生成一个不同的
sqlToSomething
函数,要么使用GHC泛型告诉编译器如何自动反序列化任何
数据
类型。图书馆广泛使用第一种方法;有空的。HDBC可能也有解决方案,我只是没听说过。我还可以向您展示如何显式地生成
sqlToBla
函数,而不使用预先存在的代码,为什么不在答案中呢?不想要一些分数吗?;)请演示如何使用TH生成
sqlToXXX