You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Lens修改多态记录类型并适配任意DbRecord

解决方案

要实现适配任意同类多态DbRecord的toRecordWrite函数,核心是重新构造目标类型实例——因为原类型和目标类型的类型参数不同,属于完全不同的类型,Lens的同类型修改无法直接使用。以下是几种可行方案:

1. 针对单个多态类型的直接实现

如果只处理DbRecordPoly这类固定结构的类型,直接通过模式匹配提取需要保留的字段,再构造目标类型即可:

{-# LANGUAGE TemplateHaskell #-}

module Try2 where

import Control.Lens
import Data.Time
import Prelude hiding (id)

data DbRecordPoly id createdAt updatedAt name = DbRecordPoly
  { _dbrId        :: id
  , _dbrCreatedAt :: createdAt
  , _dbrUpdatedAt :: updatedAt
  , _dbrName      :: name
  }
$(makeLensesWith abbreviatedFields ''DbRecordPoly)

type DbRecordRead = DbRecordPoly Int UTCTime UTCTime String
type DbRecordWrite = DbRecordPoly () () () String

-- 针对DbRecordPoly的实现
toRecordWrite :: DbRecordPoly i c u n -> DbRecordPoly () () () n
toRecordWrite (DbRecordPoly _ _ _ name) = DbRecordPoly () () () name

-- 或者用Lens提取字段(效果一致)
toRecordWrite' :: DbRecordPoly i c u n -> DbRecordPoly () () () n
toRecordWrite' r = DbRecordPoly () () () (r ^. name)

2. 用类型类适配任意同类多态类型

如果需要适配UserPoly、PostPoly等多个类似结构的多态类型,可以定义一个类型类,为每个目标类型手动实现实例:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE TemplateHaskell #-}

import Control.Lens
import Data.Time
import Prelude hiding (id)

-- 定义类型类:src是源类型,dst是对应的写类型,src可唯一推导dst
class ToRecordWrite src dst | src -> dst where
  toRecordWrite :: src -> dst

-- 示例:DbRecordPoly的实例
data DbRecordPoly id createdAt updatedAt name = DbRecordPoly
  { _dbrId        :: id
  , _dbrCreatedAt :: createdAt
  , _dbrUpdatedAt :: updatedAt
  , _dbrName      :: name
  }
$(makeLensesWith abbreviatedFields ''DbRecordPoly)

instance ToRecordWrite (DbRecordPoly i c u n) (DbRecordPoly () () () n) where
  toRecordWrite (DbRecordPoly _ _ _ n) = DbRecordPoly () () () n

-- 示例:UserPoly的实例
data UserPoly id createdAt updatedAt email username = UserPoly
  { _userId        :: id
  , _userCreatedAt :: createdAt
  , _userUpdatedAt :: updatedAt
  , _userEmail     :: email
  , _userUsername  :: username
  }
$(makeLensesWith abbreviatedFields ''UserPoly)

instance ToRecordWrite (UserPoly i c u e un) (UserPoly () () () e un) where
  toRecordWrite (UserPoly _ _ _ e un) = UserPoly () () () e un

-- 使用示例
type UserRead = UserPoly Int UTCTime UTCTime String String
type UserWrite = UserPoly () () () String String

testUser :: UserRead
testUser = UserPoly 1 (UTCTime (toEnum 0) 0) (UTCTime (toEnum 0) 0) "test@example.com" "testuser"

testConvert :: UserWrite
testConvert = toRecordWrite testUser

3. 用Template Haskell自动生成实例

如果需要适配大量同结构的多态类型,手动写实例会很繁琐,可以用Template Haskell自动生成ToRecordWrite实例:

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}

import Control.Lens
import Data.Time
import Prelude hiding (id)
import Language.Haskell.TH

-- 定义类型类
class ToRecordWrite src dst | src -> dst where
  toRecordWrite :: src -> dst

-- 自动生成ToRecordWrite实例的TH函数
deriveToRecordWrite :: Name -> Q [Dec]
deriveToRecordWrite tyName = do
  -- 获取目标类型的元数据
  TyConI (DataD _ _ typeVars _ [RecC conName fields] _) <- reify tyName
  -- 拆分类型变量:前3个是id/createdAt/updatedAt,剩下的是需要保留的字段类型
  let [tvId, tvCreated, tvUpdated] = take 3 typeVars
      restTVs = drop 3 typeVars
      -- 构造源类型和目标类型
      srcTy = foldl AppT (ConT tyName) (map VarT typeVars)
      dstTy = foldl AppT (ConT tyName) (ConT ''() : ConT ''() : ConT ''() : map VarT restTVs)
      -- 模式匹配:忽略前3个字段,绑定剩下的字段
      pats = map (const WildP) (take 3 fields) ++ map (\(fieldName, _, _) -> VarP (mkName (nameBase fieldName))) (drop 3 fields)
      -- 构造目标实例:前3个字段填(),剩下的字段用绑定的变量
      args = map (\_ -> ConE '()) (take 3 fields) ++ map (\(fieldName, _, _) -> VarE (mkName (nameBase fieldName))) (drop 3 fields)
      body = foldl AppE (ConE conName) args
  -- 生成实例声明
  instanceD (cxt [])
            (AppT (AppT (ConT ''ToRecordWrite) srcTy) dstTy)
            [funD 'toRecordWrite [clause [ConP conName pats] (normalB body) []]]

-- 定义DbRecordPoly并自动生成实例
data DbRecordPoly id createdAt updatedAt name = DbRecordPoly
  { _dbrId        :: id
  , _dbrCreatedAt :: createdAt
  , _dbrUpdatedAt :: updatedAt
  , _dbrName      :: name
  }
$(makeLensesWith abbreviatedFields ''DbRecordPoly)
$(deriveToRecordWrite ''DbRecordPoly)

-- 定义UserPoly并自动生成实例
data UserPoly id createdAt updatedAt email username = UserPoly
  { _userId        :: id
  , _userCreatedAt :: createdAt
  , _userUpdatedAt :: updatedAt
  , _userEmail     :: email
  , _userUsername  :: username
  }
$(makeLensesWith abbreviatedFields ''UserPoly)
$(deriveToRecordWrite ''UserPoly)

-- 使用示例
type DbRecordRead = DbRecordPoly Int UTCTime UTCTime String
type DbRecordWrite = DbRecordPoly () () () String

testConvert :: DbRecordWrite
testConvert = toRecordWrite (DbRecordPoly 1 (UTCTime (toEnum 0) 0) (UTCTime (toEnum 0) 0) "test")

这个TH函数会自动识别目标类型的前3个字段(对应id、createdAt、updatedAt),将它们替换为(),保留后续所有字段的值,生成对应的ToRecordWrite实例。

内容的提问来源于stack exchange,提问作者Saurabh Nanda

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 10:13:16