此问题基于this Reasonably Polymorphic blog post中描述的高级数据模式。
在以下代码块中,我定义了一个类型系列HKD
和一个数据类型Person
,其中的字段可以是Maybe
或Identity
。
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeFamilies #-}
import Data.Aeson
-- "Higher Kinded Data" (http://reasonablypolymorphic.com//blog/higher-kinded-data)
type family HKD f a where
HKD Identity a = a
HKD Maybe a = Maybe a
data Person f = Person
{ pName :: HKD f String
, pAge :: HKD f Int
} deriving (Generic)
然后我尝试为此类型派生ToJSON
实例。
-- Already provided in imports:
-- instance ToJSON String
-- instance ToJSON Int
-- instance ToJSON a => ToJSON (Maybe a)
instance ToJSON (Person f) where
toJSON = genericToJSON defaultOptions
不幸的是我收到以下错误:
使用
(ToJSON (HKD f Int))
时没有genericToJSON
的实例。
鉴于我已经拥有ToJSON Int
和ToJSON (Maybe Int)
,GHC是否应该能够派生实例ToJSON (HKD f Int)
?我的理解是类型系列就实例而言就像类型别名一样。如果是这种情况,那么我就无法为它定义自己的实例,但它应该从其定义中接收实例,在本例中为Int
和Maybe Int
。不幸的是,错误似乎与此相矛盾。
如何为我的类型定义ToJSON
实例?
答案 0 :(得分:6)
类型系列HKD
需要应用于已知Identity
或Maybe
才能减少。否则,HKD f Int
的{{1}}版本被卡住,我们无法解析所有字段类型f
的{{1}}约束,除非在上下文中列出它们,例如{ {1}},这是一种可能的解决方案,但不能很好地扩展到大量字段。
主要问题是编写和维护字段约束列表的繁琐,这可以通过注意它实际上是记录类型的函数来解决,并且我们可以使用GHC泛型在Haskell中定义它。
HKD f a
然而,这个实例是非模块化且低效的,因为它仍然暴露了记录的内部结构(它的字段类型),并且每次使用a
时必须重新解决每个字段的约束。
我们真正想要作为一个实例编写的是
(ToJSON (HKD f Int), ToJSON (HKD f String))
使用量化约束,这是GHC目前正在实施的一项新功能;希望语法是自我描述的。但由于尚未发布,我们能在此期间做些什么呢?
量化约束目前可使用类型类进行编码。
type GToJSONFields a = GFields' ToJSON (Rep a)
-- Every field satisfies constraint c
type family GFields' (c :: * -> Constraint) (f :: * -> *) :: Constraint
type instance GFields' c (M1 i d f) = GFields' c f
type instance GFields' c (f :+: g) = (GFields' c f, GFields' c g)
type instance GFields' c (f :*: g) = (GFields' c f, GFields' c g)
type instance GFields' c U1 = ()
type instance GFields' c (K1 i a) = c a
instance (GToJSONFields (Person f)) => ToJSON (Person f) where
toJSON = genericToJSON defaultOptions
但ToJSON (Person f)
会在字段上使用instance (forall a. ToJSON a => ToJSON (HKD f a)) => ToJSON (Person f) where
-- ...
,而不是class ToJSON_HKD f where
toJSON_HKD :: ToJSON a => f a -> Value -- AllowAmbiguousTypes, or wrap this in a newtype (which we will define next anyway)
instance ToJSON_HKD Identity where
toJSON_HKD = toJSON
instance ToJSON_HKD Maybe where
toJSON_HKD = toJSON
。我们可以将genericToJSON
中的字段换行,并使用ToJSON
约束来调度ToJSON_HKD
约束。
newtype
ToJSON
的字段只能包含在ToJSON_HKD
或newtype Apply f a = Apply (HKD f a)
instance ToJSON_HKD f => ToJSON (Apply f a) where
toJSON (Apply x) = toJSON_HKD @f @a x
中。我们应该为Person
添加一个案例。实际上,让我们打开它,并重构类型构造函数的情况。我们写HKD Identity
而不是HKD Maybe
;这个时间更长,但HKD
标记可以重复用于任何其他类型的构造函数,例如HKD (Tc Maybe) a
。
HKD Maybe a
aeson 有一个Tc
类型类,其作用与HKD (Tc (Apply f)) a
非常相似,编码为-- Redefining HKD
type family HKD f a
type instance HKD Identity a = a
type instance HKD (Tc f) a = f a
data Tc (f :: * -> *) -- Type-level tag for type constructors
。偶然地,ToJSON1
是连接这些类的正确类型。
ToJSON_HKD
下一步是包装器本身。
forall a. ToJSON a => ToJSON (f a)
我们所做的就是将字段包裹在Tc
中(从instance ToJSON1 f => ToJSON_HKD (Tc f) where
toJSON1_HKD = toJSON1
到wrapApply :: Person f -> Person (Tc (Apply f))
wrapApply = gcoerce
,等于newtype
并且代表性地等同于HKD f a
)。所以这真的是一种强制。不幸的是,HKD (Tc (Apply f)) a
此处不会进行类型检查,因为Apply f a
具有名义类型参数(因为它使用HKD f a
,其匹配名称 coerce
减少)。但是,Person f
是HKD
类型,f
的输入和预期输出的通用表示实际上是可强制的。这导致了以下“通用强制”,这使Person
变得多余:
Generic
我们得出结论:将字段包装在wrapApply
中,然后使用wrapApply
。
gcoerce :: forall a b
. (Generic a, Generic b, Coercible (Rep a ()) (Rep b ()))
=> a -> b
gcoerce = to . (coerce :: Rep a () -> Rep b ()) . from
关于要点的注意事项:Apply
已重命名为genericToJSON
,从singletons借来的名称,instance ToJSON_HKD f => ToJSON (Person f) where
toJSON = genericToJSON defaultOptions . gcoerce @_ @(Person (Tc (Apply f)))
被重写为HKD
,明确了类型构造函数(@@)
与身份函数的defunctionalized symbol HKD Identity a
之间的区别。它看起来更整洁。
HKD博客文章结合了两个想法:
通过类型构造函数HKD Id a
(也称为"functor functor pattern")参数化记录;
将Identity
概括为类型函数,这是可能的,即使Haskell在类型级别没有第一类函数,感谢the technique of defunctionalization。
第二个想法的主要目标是能够使用未包装的字段重用记录Id
。对于家庭引入的复杂类型来说,这似乎是一个美化问题。
仔细观察,可以说,确实没有那么多额外的复杂性。到底值得吗?我还没有一个好的答案。
仅供参考,以下是将上述技术应用于没有f
类型系列的简单记录的结果。
f
我们可以删除两个定义:Person
(HKD
足够)和data Person f = Person
{ name :: f String
, age :: f Int
}
(ToJSON_HKD
就足够了)。我们将ToJSON1
替换为连接gcoerce
和coerce
的其他新类型:
Apply
我们得出ToJSON
如下:
ToJSON1
aeson 可以选择使newtype Apply' f a = Apply' (f a) -- no HKD
instance (ToJSON1 f, ToJSON a) => ToJSON (Apply' f a) where
toJSON (Apply' x) = toJSON1 x
个字段成为可选字段,因此可以在相应的JSON对象中丢失它们。那么,该选项不适用于上述方法。它只影响实例定义中已知为ToJSON
的字段,因此解决方案2和3因为所有字段的新类型而失败。
此外,对于解决方案1,这个
instance ToJSON1 f => ToJSON (Person f) where
toJSON = genericToJSON defaultOptions . coerce @_ @(Person (Apply' f))
在将事实之后的其他实例专门化为Maybe
时的行为会有所不同:
Maybe
答案 1 :(得分:3)
博客文章的作者在这里。这里最简单的解决方案可能就是将f
参数单一化:
instance ToJSON (Person Identity) where
toJSON = genericToJSON defaultOptions
instance ToJSON (Person Maybe) where
toJSON = genericToJSON defaultOptions
它有点难看,但肯定可以发货。我正在实验室里试图找出更好的解决这个问题的一般方法,如果我拿出任何东西,我会告诉你。