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

use*_*443 3 haskell

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

kos*_*kus 6

好的,既然你在评论中说过,你在语义上的目标就像衍生一样Monoid,那就让我们这样做吧.

一般观察

类似的类Monoid很容易为"和类型"派生,即具有多个构造函数的类型,但是可以为纯"产品类型"派生它,即具有单个构造函数和仅一个或多个参数的类型.让我们专注于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
Run Code Online (Sandbox Code Playgroud)

大多数代码都支持语言扩展和模块头.让我们看看其余的:

我们正如你所做的那样Zerozero方法声明这个类.然后我们给出方法的默认签名,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泛型的解决方案

让我们看看我们如何直接用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的互动完全相同,所以我在此不再重复.

那么mappend呢?

既然我们已经zero对应了mzero,也许你想扩展这个类来覆盖mappend下一个.这也是可能的,当然,欢迎您尝试将其作为练习.

如果你想看到解决方案:

对于generics-sop,您可以查看我从2016年开始的ZuriHac讲话,其中更详细地介绍了泛型,并使用如何派生Monoid实例作为初始示例.

对于GHC泛型,您可以查看泛型派生包,其中包含许多示例通用程序,包括monoids.Generics.Deriving.Monoid模块的源代码包含GMonoid'GZero上面对应的类实例,并且还包含代码mappend.

  • 使用另一个名为[one-liner](https://hackage.haskell.org/package/one-liner)的库,它有点类似于generics-sop,但在类型级别的东西上稍微不那么重,你可以写"零"以更直接的方式.请参阅https://gist.github.com/sjoerdvisscher/050ceb08b6fcb68cda64d5b5eac7e235 (2认同)