为更高级别的数据派生实例

时间:2018-04-02 20:55:39

标签: haskell type-families higher-kinded-types

此问题基于this Reasonably Polymorphic blog post中描述的高级数据模式。

在以下代码块中,我定义了一个类型系列HKD和一个数据类型Person,其中的字段可以是MaybeIdentity

{-# 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 IntToJSON (Maybe Int),GHC是否应该能够派生实例ToJSON (HKD f Int)?我的理解是类型系列就实例而言就像类型别名一样。如果是这种情况,那么我就无法为它定义自己的实例,但它应该从其定义中接收实例,在本例中为IntMaybe Int。不幸的是,错误似乎与此相矛盾。

如何为我的类型定义ToJSON实例?

2 个答案:

答案 0 :(得分:6)

类型系列HKD需要应用于已知IdentityMaybe才能减少。否则,HKD f Int的{​​{1}}版本被卡住,我们无法解析所有字段类型f的{​​{1}}约束,除非在上下文中列出它们,例如{ {1}},这是一种可能的解决方案,但不能很好地扩展到大量字段。

解决方案1:导出上下文

主要问题是编写和维护字段约束列表的繁琐,这可以通过注意它实际上是记录类型的函数来解决,并且我们可以使用GHC泛型在Haskell中定义它。

HKD f a

然而,这个实例是非模块化且低效的,因为它仍然暴露了记录的内部结构(它的字段类型),并且每次使用a时必须重新解决每个字段的约束。

Gist of solution 1

解决方案2:概括上下文

我们真正想要作为一个实例编写的是

(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_HKDnewtype 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 fHKD类型,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

Gist of solution 2

关于要点的注意事项: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之间的区别。它看起来更整洁。

解决方案3:没有类型系列

HKD博客文章结合了两个想法:

  1. 通过类型构造函数HKD Id a(也称为"functor functor pattern")参数化记录;

  2. Identity概括为类型函数,这是可能的,即使Haskell在类型级别没有第一类函数,感谢the technique of defunctionalization

  3. 第二个想法的主要目标是能够使用未包装的字段重用记录Id。对于家庭引入的复杂类型来说,这似乎是一个美化问题。

    仔细观察,可以说,确实没有那么多额外的复杂性。到底值得吗?我还没有一个好的答案。

    仅供参考,以下是将上述技术应用于没有f类型系列的简单记录的结果。

    f

    我们可以删除两个定义:PersonHKD足够)和data Person f = Person { name :: f String , age :: f Int } ToJSON_HKD就足够了)。我们将ToJSON1替换为连接gcoercecoerce的其他新类型:

    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

它有点难看,但肯定可以发货。我正在实验室里试图找出更好的解决这个问题的一般方法,如果我拿出任何东西,我会告诉你。