我想推导anyclass
的{{1}}策略。为此,我需要一个默认实现和Generics的相应实例:
class Zeros
我收到错误消息(堆栈LTS 10.8 - GHC 8.2.2):
import GHC.Generics
class Zeros z where
zero :: z
default zero :: (Generic z, Gzero (Rep z)) => z
zero = gzero (from z)
class Gzero f where
gzero :: f a -> a
instance Gzero (Rec0 Int) where
gzero (Rec0 i a) = a
data B1 = B1 Int
deriving stock (Show, Read, Eq, Ord, Generic)
deriving instance Zeros B1
instance Zeros Int where zero = 0
我已经阅读了GHC.Generics的文档,但是不能通过常量函数从树示例跳到我的情况。 非常感谢帮助!
答案 0 :(得分:6)
好的,既然你在评论中说过,你在语义上的目标是导出Monoid
,那就让我们这样做。
像Monoid
这样的类很容易派生为"和类型",即具有多个构造函数的类型,但是可以为纯"产品类型&派生它#34;,即具有单个构造函数且只有一个或多个参数的类型。我们只关注与zero
对应的mempty
,并且是您的问题的主题:
如果单个构造函数没有参数,我们只需使用该构造函数,
如果单个构造函数有一个参数(作为B1
示例),那么我们要求该参数已经有一个Zero
实例并使用该类型的zero
,
如果单个构造函数有多个参数,我们对所有这些参数都这样做:我们要求所有这些参数都有一个Zero
实例,然后使用zero
表示所有这些参数这些
实际上,我们可以将此作为一个简单的规则:对于单个构造函数的所有参数,只需应用zero
。
我们可以选择几种通用编程方法来实现此规则。您一直在询问GHC.Generics
,我会解释如何在这种方法中做到这一点,但是让我首先解释如何使用generics-sop包来做,因为我认为可以更直接地将上述规则转录为此方法中的代码。
使用generics-sop,您的代码如下所示:
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE StandaloneDeriving #-}
module Zero where
import qualified GHC.Generics as GHC
import Generics.SOP
class Zero a where
zero :: a
default zero :: (IsProductType a xs, All Zero xs) => a
zero = to (SOP (Z (hcpure (Proxy @Zero) (I zero))))
instance Zero Int where
zero = 0
大多数代码都支持语言扩展和模块标头。让我们来看看剩下的事情:
我们正在使用Zero
方法声明zero
类。然后我们为zero
方法提供一个默认签名,解释我们可以在哪个条件下导出它。类型签名表示类型必须是产品类型(即,具有单个构造函数)。然后将xs
绑定到与所有构造函数参数的类型对应的类型列表。 All Zero xs
约束表示所有这些参数类型也必须是Zero
类的实例。
然后,代码就是单行代码,尽管可以肯定的是,该代码正在进行中。 to
调用最终将生成通用表示转换为实际所需类型的值。 SOP . Z
组合表示我们想要生成数据类型的第一个(也是唯一的)构造函数的值。 hcpure (Proxy @Zero) (I zero)
调用产生与zero
一样多的调用副本,因为有构造函数的参数。
为了尝试它,我们现在可以为它们定义数据类型并派生Zero
的实例:
data B1 = B1 Int
deriving (GHC.Generic, Generic, Show)
deriving instance Zero B1
data B2 = B2 Int B1 Int
deriving (GHC.Generic, Generic, Show)
deriving instance Zero B2
因为generics-sop建立在GHC泛型之上,所以我们必须定义两个Generic
类。 GHC中内置了GHC.Generic
类,generics-sop提供了Generic
类。 Show
课程只是为了方便和测试。
有点不幸的是,即使使用DeriveAnyClass
扩展名,我们也不能简单地将Zero
添加到此处的派生实例列表中,因为GHC难以推断实例上下文实际应该是空。也许GHC的未来版本将足够聪明地认识到这一点。但是在一个独立的派生声明中,我们可以显式地提供(空)实例上下文,它很好。在GHCi中,我们可以看到这有效:
GHCi> zero :: B1
B1 0
GHCi> zero :: B2
B2 0 (B1 0) 0
让我们看看我们如何直接用GHC泛型做同样的事情。这里的代码如下:
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeOperators #-}
module Zero where
import GHC.Generics
class Zero a where
zero :: a
default zero :: (Generic a, GZero (Rep a)) => a
zero = to gzero
instance Zero Int where
zero = 0
class GZero a where
gzero :: a x
instance GZero U1 where
gzero = U1
instance Zero a => GZero (K1 i a) where
gzero = K1 zero
instance (GZero a, GZero b) => GZero (a :*: b) where
gzero = gzero :*: gzero
instance GZero a => GZero (M1 i c a) where
gzero = M1 gzero
开始的主要是你在问题中所拥有的。 zero
的默认签名表示如果a
有Generic
个实例且类型的通用代表Rep a
是GZero
的实例,我们可以通过先调用zero
,然后使用gzero
将通用表示转换为实际类型来获取to
的定义。
我们现在必须为GZero
类提供实例。我们为U1
,K1
,(:*:)
和M1
提供实例,告诉GHC如何处理单位类型(即没有参数的构造函数),常量,对(二进制产品) )和元数据。如果不为(:+:)
提供实例,我们会隐式排除和类型(通过generics-sop中的IsProductType
约束更加明确。)
U1
的实例表示,对于单位类型,我们只返回唯一值。
常量的实例(这些是构造函数的参数)说,对于这些,我们需要它们也是Zero
类的实例,并使用递归调用zero
。 / p>
对的实例说,在这种情况下,我们会产生一对gzero
个调用。如果构造函数具有两个以上的参数,则会重复应用此实例。
元数据实例表示我们要忽略所有元数据,例如构造函数名称和记录字段选择器。我们没有对generics-sop中的元数据做任何事情,因为GHC泛型将元数据混合到每个值的表示中,而在泛型中它是独立的。
从这里开始,它基本相同:
data B1 = B1 Int
deriving (Generic, Show, Zero)
data B2 = B2 Int B1 Int
deriving (Generic, Show, Zero)
这有点简单,因为我们只需要派生一个Generic
类,在这种情况下,GHC足够聪明地找出Zero
的实例上下文,所以我们可以将其添加到派生实例列表中。与GHCi的互动完全相同,所以我不会在这里重复。
现在我们zero
对应mzero
,或许您希望将该类扩展到下一个mappend
。这也是可能的,当然,欢迎您尝试将其作为练习。
如果您想查看解决方案:
对于generics-sop,你可以查看我的ZuriHac talk from 2016,它更详细地解释了generics-sop,并使用如何派生Monoid
个实例作为初始示例。
对于GHC泛型,您可以查看包含许多示例通用程序的generic-deriving包,包括monoids。源代码
Generics.Deriving.Monoid
module的GMonoid'
包含与GZero
对应的类实例,并且还包含mappend
的代码。