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

如何不使用TemplateHaskell手动实现Opaleye的pBirthday及相关实例?

手动实现Opaleye自定义类型的ProductProfunctor适配器与Default实例

我完全理解你不想依赖TemplateHaskell的心情——手动实现这些代码不仅能让你更清楚Opaleye的底层逻辑,还能避免TH带来的编译时依赖和调试复杂度。下面我会一步步带你实现pBirthday函数和对应的Default实例,和makeAdaptorAndInstance生成的效果完全一致。

1. 手动实现pBirthday适配器函数

首先,我们需要明确pBirthday的核心作用:它把作用在自定义类型每个字段上的ProductProfunctor实例,组合成一个作用在整个自定义类型上的ProductProfunctor。

先导入必要的模块,然后写出实现:

import Data.Profunctor.Product (ProductProfunctor(..))
import Opaleye (Column, PGText, PGDate, Day)

-- 你的自定义类型定义(保留原代码)
data Birthday' a b = Birthday { bdName :: a, bdDay :: b }
type Birthday = Birthday' String Day
type BirthdayColumn = Birthday' (Column PGText) (Column PGDate)

-- 手动实现pBirthday
pBirthday :: ProductProfunctor p => Birthday' (p a a') (p b b') -> p (Birthday' a b) (Birthday' a' b')
pBirthday Birthday { bdName = nameProfunctor, bdDay = dayProfunctor } =
  dimap unpackRecord packTuple (nameProfunctor ***! dayProfunctor)
  where
    -- 把自定义记录转换成元组,方便用ProductProfunctor的组合运算符
    unpackRecord :: Birthday' a b -> (a, b)
    unpackRecord Birthday { bdName = n, bdDay = d } = (n, d)
    
    -- 把组合后的元组转换回自定义记录
    packTuple :: (a', b') -> Birthday' a' b'
    packTuple (n', d') = Birthday { bdName = n', bdDay = d' }

代码解释:

  • dimap是Profunctor类型类的方法,用来在profunctor的输入和输出端做转换:这里我们把输入的Birthday'转换成元组,处理后再把元组转换回Birthday'。
  • ***!是ProductProfunctor的核心组合运算符,它能把两个作用在单个值上的profunctor,组合成一个作用在元组上的profunctor。
  • 我们通过unpackRecord和packTuple完成记录与元组的双向转换,让ProductProfunctor的组合逻辑能复用元组的实现。

2. 手动实现Default类型类实例

接下来是Default实例,它的作用是当自定义类型的每个字段都有对应的Default实例时,自动推导整个类型的Default实例——这在Opaleye中用来简化表定义、查询构造等场景。

实现代码如下:

import Data.Profunctor.Product.Default (Default(..))

instance (ProductProfunctor p, Default p a a', Default p b b') => Default p (Birthday' a b) (Birthday' a' b') where
  def = pBirthday Birthday { bdName = def, bdDay = def }

代码解释:

  • 这个实例的约束条件和makeAdaptorAndInstance生成的完全一致:要求p是ProductProfunctor,且每个字段的类型对(a, a')和(b, b')都有Default实例。
  • 我们直接复用pBirthday,把每个字段的def(默认profunctor实例)组合成整个记录的def。

3. 验证与原代码的兼容性

现在你可以完全替换原有的TH生成代码,用手动实现的版本来定义表:

import Opaleye (Table, table, tableColumn)

birthdayTable :: Table BirthdayColumn BirthdayColumn
birthdayTable = table "birthdayTable" (pBirthday Birthday { bdName = tableColumn "name" , bdDay = tableColumn "birthday" })

这段代码的行为和原TH版本完全一致,没有任何功能差异。

扩展:多字段自定义类型的实现思路

如果你的自定义类型有更多字段(比如3个),原理是一样的:

  1. 把记录转换成对应长度的元组(比如(a, b, c))
  2. 用***!多次组合profunctor(或者用product-profunctors提供的pTriple等辅助函数)
  3. 再把元组转换回记录

比如三个字段的类型:

data User' a b c = User { userName :: a, userAge :: b, userEmail :: c }

pUser :: ProductProfunctor p => User' (p a a') (p b b') (p c c') -> p (User' a b c) (User' a' b' c')
pUser User { userName = nP, userAge = aP, userEmail = eP } =
  dimap unpack pack (nP ***! aP ***! eP)
  where
    unpack User { userName = n, userAge = a, userEmail = e } = ((n, a), e)
    pack ((n', a'), e') = User { userName = n', userAge = a', userEmail = e' }

这样就能轻松扩展到任意字段数的自定义类型。

内容的提问来源于stack exchange,提问作者Damian Nadales

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 03:28:19