Haskell中定义新建与持久化Record类型的惯用方式
解决Servant中创建记录的类型冗余问题
针对你的场景,这里有几种实用方案,既能避免手动重复定义字段,又能确保创建接口不接受自动生成的id字段:
方案1:基于泛型自动推导无ID的类型
利用generic-lens或generic-data这类库,通过泛型自动从持久化类型中移除id字段,生成创建用的类型,无需手动维护两个重复的类型定义。
示例代码:
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TypeApplications #-} import GHC.Generics import Data.Generics.Labels () import Data.UUID (UUID) -- 持久化用的记录类型 data Reading = Reading { id :: UUID , value :: String , temperature :: Double , timestamp :: Int } deriving (Generic, Show) -- 创建用的类型,通过泛型移除id字段 type CreateReading = GHC.Generics.Rep Reading `RemoveField` "id" `ToProductType` "CreateReading" -- 辅助类型家族(可封装在工具模块) type family RemoveField (rep :: * -> *) (field :: Symbol) :: * -> * where RemoveField (D1 m (C1 c (S1 s (Rec0 a) :*: rest))) field = If (s == 'MetaSel ('Just field) _ _ _) (D1 m (C1 c rest)) (D1 m (C1 c (S1 s (Rec0 a) :*: RemoveField rest field))) RemoveField rep _ = rep type family ToProductType (rep :: * -> *) (name :: Symbol) :: * where ToProductType rep name = Generic.ProductType name rep
CreateReading会自动拥有除id外的所有字段,在Servant的API类型中直接用它作为POST请求的Body类型即可。
方案2:定义基础无ID类型,扩展出持久化类型
反过来先定义不带id的核心类型,再让持久化类型基于它扩展,核心字段只需维护一次。
示例代码:
import Data.UUID (UUID) -- 核心字段类型,用于创建请求等无需id的场景 data ReadingCore = ReadingCore { value :: String , temperature :: Double , timestamp :: Int } deriving (Show) -- 持久化用的类型,包含自动生成的id和核心字段 data Reading = Reading { id :: UUID , core :: ReadingCore } deriving (Show) -- 若不想嵌套,可启用DuplicateRecordFields和RecordDotSyntax扩展 {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE RecordDotSyntax #-} data Reading = Reading { id :: UUID , value :: String , temperature :: Double , timestamp :: Int } deriving (Show) data ReadingCore = ReadingCore { value :: String , temperature :: Double , timestamp :: Int } deriving (Show) -- 转换函数:从创建用的ReadingCore生成待持久化的Reading(填充数据库生成的id) toReading :: UUID -> ReadingCore -> Reading toReading rid core = Reading { id = rid , value = core.value , temperature = core.temperature , timestamp = core.timestamp }
这种方式核心字段只定义一次,创建接口的Body类型ReadingCore天然不含id,完全符合需求。
方案3:使用模板Haskell自动生成创建类型
如果项目中有大量这类转换需求,可以用Template Haskell写一个宏,自动从带id的类型生成无id的创建类型。
示例代码:
{-# LANGUAGE TemplateHaskell #-} import Language.Haskell.TH import Data.UUID (UUID) -- 定义持久化类型 data Reading = Reading { id :: UUID , value :: String , temperature :: Double , timestamp :: Int } deriving (Show) -- 自动生成CreateReading类型,移除id字段 makeCreateType "Reading" "CreateReading" ["id"] -- 模板Haskell宏实现(可放在工具模块) makeCreateType :: String -> String -> [String] -> Q [Dec] makeCreateType origName newName excludedFields = do TyConI (DataD _ _ _ _ [RecC origConName fields] _) <- reify (mkName origName) let filteredFields = filter (\(name, _, _) -> name `notElem` map mkName excludedFields) fields let newConName = mkName newName return [DataD [] newConName [] Nothing [RecC newConName filteredFields] []]
运行后会自动生成CreateReading类型,包含除id外的所有字段,无需手动编写。
方案4:自定义JSON解析(备选,非类型安全)
若不想额外定义类型,可在JSON解析层拒绝包含id字段的请求,但这种方式编译期无法检查,仅作临时备选:
{-# LANGUAGE DeriveGeneric #-} import GHC.Generics import Data.Aeson import Data.UUID (UUID) import Servant data Reading = Reading { id :: UUID , value :: String } deriving (Generic, Show) -- 自定义JSON实例,禁止解析id字段 instance FromJSON Reading where parseJSON = withObject "Reading" $ \v -> Reading <$> pure undefined -- 实际不会用到,仅占位 <*> v .: "value" <* (if v .:? "id" /= Nothing then fail "请求体不能包含'id'字段" else pure ())
注意:这种方式下类型本身仍包含id,持久化时需替换为数据库生成的值,类型安全性不如前几种方案。
内容的提问来源于stack exchange,提问作者Razumov
相关产品推荐
相关产品推荐

