如何编写支持obj与Maybe obj调用getField@k的Haskell表单辅助函数
解决方案:支持普通对象与Maybe对象的表单辅助函数
我们可以通过定义类型类抽象fieldNameFor和fieldValueFor的行为,分别为普通obj和Maybe obj实现实例,同时正确传递必要的类型约束,解决原实现无法适配Maybe类型的问题。
1. 基础定义与类型类声明
首先扩展必要的语言扩展,并定义核心类型类,为后续实例实现提供抽象接口:
{-# LANGUAGE AllowAmbiguousTypes, DataKinds, FlexibleInstances, OverloadedStrings, TypeApplications, TypeFamilies #-} import Data.Proxy import Data.Text as Text import GHC.Generics import GHC.Records import GHC.TypeLits import Data.Maybe (fromMaybe) data FormCtx obj = FormCtx { ctxFieldNamePrefix :: !Text, ctxObject :: !obj } data MyRecord = MyRecord { field1 :: !Text, field2 :: !Text } deriving (Generic, Show) -- 抽象fieldNameFor的行为 class FieldNameFor k obj where fieldNameFor :: FormCtx obj -> Text -- 抽象fieldValueFor的行为,用关联类型指定返回值类型 class FieldValueFor k obj where type FieldValueType k obj fieldValueFor :: FormCtx obj -> FieldValueType k obj
2. 为普通obj实现实例
对应你的第一个使用场景,直接复用原有逻辑:
-- 普通对象的字段名生成实例 instance (KnownSymbol k, HasField k obj a) => FieldNameFor k obj where fieldNameFor FormCtx {ctxFieldNamePrefix} = ctxFieldNamePrefix <> "[" <> Text.pack (symbolVal (Proxy @k)) <> "]" -- 普通对象的字段值提取实例,直接返回字段类型a instance (KnownSymbol k, HasField k obj a) => FieldValueFor k obj where type FieldValueType k obj = a fieldValueFor FormCtx {ctxObject} = getField @k ctxObject
3. 为Maybe obj实现实例
将约束传递到Maybe包裹的内部对象,同时根据需求选择返回值形式:
方案A:保留空值信息,返回Maybe a
-- Maybe对象的字段名生成实例,逻辑与普通对象一致 instance (KnownSymbol k, HasField k obj a) => FieldNameFor k (Maybe obj) where fieldNameFor FormCtx {ctxFieldNamePrefix} = ctxFieldNamePrefix <> "[" <> Text.pack (symbolVal (Proxy @k)) <> "]" -- Maybe对象的字段值提取实例,返回Maybe a instance (KnownSymbol k, HasField k obj a) => FieldValueFor k (Maybe obj) where type FieldValueType k (Maybe obj) = Maybe a fieldValueFor FormCtx {ctxObject} = getField @k <$> ctxObject
方案B:返回默认值,隐藏空值细节
如果希望Nothing时返回指定默认值,可以封装一个辅助函数:
fieldValueForWithDefault :: forall k obj a. (FieldValueFor k (Maybe obj), FieldValueType k (Maybe obj) ~ Maybe a) => a -> FormCtx (Maybe obj) -> a fieldValueForWithDefault def ctx = fromMaybe def (fieldValueFor ctx)
4. 使用示例
现在两种使用场景都能正常工作:
-- 场景1:普通对象 test1 :: (Text, Text) test1 = let obj = MyRecord "val1" "val2" ctx = FormCtx "MyRecord" obj in (fieldNameFor @"field1" ctx, fieldValueFor @"field1" ctx) -- 输出:("MyRecord[field1]", "val1") -- 场景2:Maybe对象(Just情况) test2 :: (Text, Maybe Text) test2 = let obj = MyRecord "val1" "val2" ctx = FormCtx "MyRecord" (Just obj) in (fieldNameFor @"field1" ctx, fieldValueFor @"field1" ctx) -- 输出:("MyRecord[field1]", Just "val1") -- 场景3:Maybe对象(Nothing情况) test3 :: (Text, Maybe Text) test3 = let ctx = FormCtx "MyRecord" Nothing in (fieldNameFor @"field1" ctx, fieldValueFor @"field1" ctx) -- 输出:("MyRecord[field1]", Nothing) -- 场景4:使用默认值处理Nothing test4 :: (Text, Text) test4 = let ctx = FormCtx "MyRecord" Nothing in (fieldNameFor @"field1" ctx, fieldValueForWithDefault @"field1" "" ctx) -- 输出:("MyRecord[field1]", "")
核心说明
- 类型类的方式避免了重叠实例的冲突,通过明确的约束传递将
KnownSymbol和HasField绑定到内部对象类型 - 关联类型
FieldValueType让我们能根据上下文类型灵活指定返回值,适配普通对象与Maybe对象的差异 - 两种返回值方案可根据业务需求选择,保留空值信息或隐藏细节
内容的提问来源于stack exchange,提问作者Saurabh Nanda
相关产品推荐
相关产品推荐

