我想得出的anyclass策略class Zeros.为此我需要一个默认实现和泛型的相应实例:
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
Run Code Online (Sandbox Code Playgroud)
我收到错误消息(堆栈LTS 10.8 - GHC 8.2.2):
Not in scope: data constructor ‘Rec0’
Perhaps you meant ‘Rec1’ (imported from GHC.Generics)
|
37 | gzero (Rec0 i a) = a
| ^^^^
Run Code Online (Sandbox Code Playgroud)
我已经阅读了GHC.Generics的文档,但是不能通过常量函数从树示例跳到我的情况.非常感谢帮助!
好的,既然你在评论中说过,你在语义上的目标就像衍生一样Monoid,那就让我们这样做吧.
类似的类Monoid很容易为"和类型"派生,即具有多个构造函数的类型,但是可以为纯"产品类型"派生它,即具有单个构造函数和仅一个或多个参数的类型.让我们专注于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
Run Code Online (Sandbox Code Playgroud)
大多数代码都支持语言扩展和模块头.让我们看看其余的:
我们正如你所做的那样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
Run Code Online (Sandbox Code Playgroud)
因为generics-sop是建立在GHC泛型之上的,所以我们必须定义两个Generic类.这个GHC.Generic类内置于GHC中,Generic类由generics-sop提供.这Show堂课只是为了方便和测试.
有点不幸的是,即使使用DeriveAnyClass扩展,我们也不能简单Zero地在这里添加到派生实例列表中,因为GHC很难推断出实例上下文实际上应该是空的.也许GHC的未来版本将足够聪明地认识到这一点.但是在一个独立的派生声明中,我们可以显式地提供(空)实例上下文,它很好.在GHCi中,我们可以看到这有效:
GHCi> zero :: B1
B1 0
GHCi> zero :: B2
B2 0 (B1 0) 0
Run Code Online (Sandbox Code Playgroud)
让我们看看我们如何直接用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
Run Code Online (Sandbox Code Playgroud)
一开始就是你在问题中的主要内容.默认签名zero表示如果a有一个Generic实例和类型的泛型表示Rep a是一个实例GZero,我们可以zero通过首先调用获得定义gzero,然后使用to将泛型表示转换为实际类型.
我们现在必须为GZero该类提供实例.我们提供的实例U1,K1,(:*:)和M1,告诉GHC如何分别处理单元类型(即构造不带参数),常量,对(二进制产品)和元数据.通过不提供实例(:+:),我们隐式地排除了sum类型(通过IsProductTypegenerics-sop中的约束更加明确).
该实例U1表示,对于单位类型,我们只返回唯一值.
常量的实例(这些是构造函数的参数)说,对于这些,我们需要它们也是Zero类的实例并使用递归调用zero.
对的实例说,在这种情况下,我们产生一对gzero调用.如果构造函数具有两个以上的参数,则会重复应用此实例.
元数据实例表示我们要忽略所有元数据,例如构造函数名称和记录字段选择器.我们没有对generics-sop中的元数据做任何事情,因为GHC泛型将元数据混合到每个值的表示中,而在泛型中它是独立的.
从这里开始,它基本相同:
data B1 = B1 Int
deriving (Generic, Show, Zero)
data B2 = B2 Int B1 Int
deriving (Generic, Show, Zero)
Run Code Online (Sandbox Code Playgroud)
这有点简单,因为我们只需要派生一个Generic类,在这种情况下,GHC足够聪明地找出实例上下文Zero,所以我们可以将它添加到派生实例列表中.与GHCi的互动完全相同,所以我在此不再重复.
既然我们已经zero对应了mzero,也许你想扩展这个类来覆盖mappend下一个.这也是可能的,当然,欢迎您尝试将其作为练习.
如果你想看到解决方案:
对于generics-sop,您可以查看我从2016年开始的ZuriHac讲话,其中更详细地介绍了泛型,并使用如何派生Monoid实例作为初始示例.
对于GHC泛型,您可以查看泛型派生包,其中包含许多示例通用程序,包括monoids.Generics.Deriving.Monoid模块的源代码包含GMonoid'与GZero上面对应的类实例,并且还包含代码mappend.