如何为Generic编写一个实例来派生一个像zero :: a(即常量)的函数?

时间:2018-03-14 11:15:06

标签: haskell

我想推导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的文档,但是不能通过常量函数从树示例跳到我的情况。 非常感谢帮助!

1 个答案:

答案 0 :(得分:6)

好的,既然你在评论中说过,你在语义上的目标是导出Monoid,那就让我们这样做。

一般性意见

Monoid这样的类很容易派生为"和类型",即具有多个构造函数的类型,但是可以为纯"产品类型&派生它#34;,即具有单个构造函数且只有一个或多个参数的类型。我们只关注与zero对应的mempty,并且是您的问题的主题:

  • 如果单个构造函数没有参数,我们只需使用该构造函数,

  • 如果单个构造函数有一个参数(作为B1示例),那么我们要求该参数已经有一个Zero实例并使用该类型的zero

  • 如果单个构造函数有多个参数,我们对所有这些参数都这样做:我们要求所有这些参数都有一个Zero实例,然后使用zero表示所有这些参数这些

实际上,我们可以将此作为一个简单的规则:对于单个构造函数的所有参数,只需应用zero

我们可以选择几种通用编程方法来实现此规则。您一直在询问GHC.Generics,我会解释如何在这种方法中做到这一点,但是让我首先解释如何使用generics-sop包来做,因为我认为可以更直接地将上述规则转录为此方法中的代码。

使用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泛型的解决方案

让我们看看我们如何直接用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的默认签名表示如果aGeneric个实例且类型的通用代表Rep aGZero的实例,我们可以通过先调用zero,然后使用gzero将通用表示转换为实际类型来获取to的定义。

我们现在必须为GZero类提供实例。我们为U1K1(:*:)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的互动完全相同,所以我不会在这里重复。

那么mappend呢?

现在我们zero对应mzero,或许您希望将该类扩展到下一个mappend。这也是可能的,当然,欢迎您尝试将其作为练习。

如果您想查看解决方案:

对于generics-sop,你可以查看我的ZuriHac talk from 2016,它更详细地解释了generics-sop,并使用如何派生Monoid个实例作为初始示例。

对于GHC泛型,您可以查看包含许多示例通用程序的generic-deriving包,包括monoids。源代码 Generics.Deriving.Monoid moduleGMonoid'包含与GZero对应的类实例,并且还包含mappend的代码。