如何为Generic编写一个实例来派生函数,例如zero :: a(即常量)? [英] How to write an instance for Generic to derive a function like zero::a (i.e. a constant)?

查看:123
本文介绍了如何为Generic编写一个实例来派生函数,例如zero :: a(即常量)?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想用 anyclass 策略来为类别Zeros 派生。为此,我需要一个默认实现和相应的泛型实例:

  import GHC.Generics 

类零点z其中
zero :: z
默认零点::(Generic z,Gzero(Rep z))=> z
zero = gzero(from z)

class Gzero f其中
gzero :: f a - > a
实例Gzero(Rec0 Int)其中
gzero(Rec0 ia)= a


数据B1 = B1 Int
派生股票(Show,Read ,Eq,Ord,Gen​​eric)
导出实例零点B1


实例零点Int其中零= 0

我得到错误信息(堆栈LTS 10.8 - GHC 8.2.2):

 不在范围内:数据构造函数'Rec0'
也许你的意思是'Rec1'(从GHC.Generics中导入)
|
37 | gzero(Rec0 i a)= a
| ^^^^

我已阅读GHC.Generics的文档,但无法从树例子,以我的情况与一个常量函数。
帮助非常感谢!

解决方案

好吧,既然你在评论中说过,你在语义上瞄准什么类似于派生 Monoid ,让我们做到这一点。



一般观察



Monoid 这样的类很容易为sum类型派生,也就是说,具有多个构造函数的类型,但是可以派生它产品类型,即具有单个构造函数和仅一个或多个参数的类型。让我们关注,它对应于 mempty ,并且是您问题的主题:




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

  • 如果单个构造函数有一个参数(例如 B1 示例),那么我们需要该参数具有 Zero 实例已经存在并且使用该类型的 zero ,如果单个构造函数具有多于一个的类型,则使用
  • 参数,我们对所有这些参数都做同样的事情:我们要求所有这些参数都有一个 Zero 实例,然后使用所有这些。




所以,我们可以将这个短语作为一个简单的规则:单一构造函数,只需应用 zero



我们可以选择几种通用编程方法来实现这个规则。你一直在问 GHC.Generics ,我会解释如何用这种方法来做,但是让我先解释一下如何用 generics-sop 包,因为我认为在这种方法中可以更直接地将上面确定的规则转化为代码。



解决方案使用泛型-SOP



使用泛型-SOP,您的代码如下所示:

  { - #LANGUAGE DefaultSignatures# - } 
{ - #LANGUAGE DeriveAnyClass# - }
{ - #LANGUAGE DeriveGeneric # - }
{ - #LANGUAGE FlexibleContexts# - }
{ - #LANGUAGE TypeApplications# - }
{ - #LANGUAGE TypeFamilies# - }
{ - #LANGUAGE StandaloneDeriving# - }
模块零其中

导入合格的GHC.Generics作为GHC
导入Generics.SOP

类零a其中
zero ::
默认零::(IsProductType a xs,All Zero xs)=> a
zero = to(SOP(Z(hcpure(Proxy @Zero)(I zero))))

instance Zero Int其中
zero = 0

大部分代码都支持语言扩展和模块头文件。让我们看看其余部分:

我们声明了 Zero 类,其中 zero 方法和你一样。然后我们为 zero 方法给出一个默认签名,说明我们可以在哪些条件下派生它。类型签名表示该类型必须是产品类型(即具有单个构造函数)。然后将 xs 绑定到与所有构造函数参数类型相对应的类型列表。 All Zero xs 约束表明所有这些参数类型也必须是 Zero 类的实例。

然后代码是一行代码,虽然承认这个代码很多。 调用将生成的通用表示转换为最终实际需要的类型的值。 SOP。 Z 组合表示我们想要产生数据类型的第一个(也是唯一的)构造函数的值。 hcpure(Proxy @Zero)(I zero)调用会产生与 zero 一样多的调用副本,因为有参数

为了试用它,我们可以定义数据类型并为它们派生 Zero 的实例:

 数据B1 = B1 Int 
派生(GHC.Generic,Generic,Show)

派生实例Zero B1

data B2 = B2 Int B1 Int
派生(GHC.Generic,Generic,Show)

派生实例Zero B2

因为泛型-SOP是建立在GHC泛型之上的,所以我们必须定义两个 Generic 类。 GHC中内置的 GHC.Generic 类,以及generics-sop提供的 Generic 类。 Show 类只是为了便利和测试。



有点不幸,即使使用 DeriveAnyClass 扩展名,我们不能简单地在这里将 Zero 添加到派生实例列表中,因为GHC很难推断实例上下文实际上应该是空。 GHC未来的版本可能会足够聪明地认识到这一点。但是在独立的派生声明中,我们可以明确地提供(空的)实例上下文,这很好。在GHCi中,我们可以看到这一点:

  GHCi>零:: B1 
B1 0
GHCi>零:: B2
B2 0(B1 0)0



使用GHC泛型的解决方案 h3>

让我们来看看我们如何直接使用GHC泛型来做同样的事情。在这里,代码如下所示:

  { - #LANGUAGE DeriveAnyClass# - } 
{ - #LANGUAGE DefaultSignatures# - }
{ - #LANGUAGE DerivedGeneric# - }
{ - #LANGUAGE FlexibleContexts# - }
{ - #LANGUAGE TypeOperators# - }
module zero其中

import GHC.Generics

class Zero a其中
zero :: a
default zero ::(Generic a,GZero(Rep a))=> a
zero = to gzero

实例Zero Int其中
zero = 0

class GZero a其中
gzero :: ax

实例GZero U1其中
gzero = U1

实例Zero a => GZero(K1 i a)其中
gzero = K1零

实例(GZero a,GZero b)=> GZero(a:*:b)其中
gzero = gzero:*:gzero

实例GZero a => GZero(M1 ica)其中
gzero = M1 gzero

在你的问题中。 zero 的默认签名表示如果 a 具有 Generic >实例和类型的通用表示 Rep a GZero 的一个实例,我们可以获得<$ c首先调用 gzero ,然后使用来转换generic代表到实际的类型。

我们现在必须给 GZero 类的实例。我们为 U1 K1 (:*:) M1 ,告诉GHC如何分别处理单位类型(即没有参数的构造函数),常量,对(二元产品)和元数据。通过不提供(:+:)的实例,我们隐式地排除了总和类型(通过 IsProductType

$ U1 的实例表示,对于单位类型,我们只需简单地返回唯一值。

常量的实例(这些是构造函数的参数)表示,对于这些,我们需要它们也是 Zero 类并使用递归调用< zero



对的实例表示在这种情况下,我们产生一对 gzero 调用。如果构造函数有两个以上的参数,则重复应用此实例。元数据实例表示我们要忽略所有元数据,如构造函数名称和记录字段选择器。我们不需要对泛型中的元数据进行任何操作,因为GHC泛型将元数据混合到每个值的表示形式中,而在泛型中,它是单独的。



从这里开始,它基本上是相同的:




$ b $(b)

data B2 = B2 Int B1 Int
派生(Generic,Show,Zero)

这更简单一些,因为我们只需要派生一个 Generic 类,并且在这种情况下,GHC足够聪明以找出实例上下文为 Zero ,所以我们可以将它添加到派生实例列表中。与GHCi的互动是完全一样的,所以我不会在这里重复。

那么mappend呢?



现在我们有 zero ,它对应于 mzero ,或许你想扩展这个类来覆盖 mappend 下一步。这也是可能的,当然,欢迎您尝试它作为练习。



如果您想查看解决方案:



对于泛型-sop,您可以查看我的 2016年ZuriHac谈话 a>,它更详细地解释了泛型sop,并使用如何派生 Monoid 实例作为初始示例。



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


I would like to derive with the anyclass strategy for the class Zeros. For this i need a default implementation and the corresponding instances for Generics:

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

I get the error message (with stack 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
   |            ^^^^

I have read the documentation for GHC.Generics, but cannot make the leap from the tree example to my case with a constant function. Help is greatly appreciated!

解决方案

Ok, since you said in the comments that what you're aiming for semantically is something like deriving Monoid, let's do that.

General observations

A class like Monoid is easy to derive for "sum types", i.e., types having more than one constructor, but it is possible to derive it for pure "product types", i.e., types having a single constructor and just one or more arguments. Let's focus just on zero, which corresponds to mempty, and is the subject of your question:

  • if the single constructor has no arguments, we simply use that constructor,

  • if the single constructor has one argument (as your B1 example), then we require that argument to have a Zero instance already and use the zero of that type,

  • if the single constructor has more than one argument, we do the same for all these arguments: we require all of them to have a Zero instance and then use zero for all of these.

So really, we can phrase this as one simple rule: for all arguments of the single constructor, just apply zero.

We have the choice of several approaches to generic programming to implement this rule. You've been asking about GHC.Generics, and I will explain how to do it in that approach, but let me nevertheless first explain how to do it with the generics-sop package, because I think one can more directly transcribe the rule identified above into code in this approach.

Solution using generics-sop

With generics-sop, your code looks as follows:

{-# 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

Most of the code is enabling language extensions and the module header. Let's look at the rest:

We are declaring the Zero class with the zero method as you did. Then we give a default signature for the zero method explaining under which conditions we can derive it. The type signature says that the type has to be a product type (i.e., have a single constructor). The xs is then bound to a list of types corresponding to the types of all the constructor arguments. The All Zero xs constraint says that all these argument types also have to be an instance of the Zero class.

The code is then a one-liner, although admittedly a lot is going on in that line. The to call in transforms the produces generic representation into a value of the actually desired type in the end. The SOP . Z combination says we want to produce a value of the first (and only) constructor of the datatype. The hcpure (Proxy @Zero) (I zero) call produces as many copies of calls to zero as there are arguments of the constructor.

In order to try it, we can define datatypes and derive instances of Zero for them now:

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

Because generics-sop is built on top of GHC generics, we have to define two Generic classes. The GHC.Generic class built into GHC, and the Generic class provided by generics-sop. The Show class is just for convience and testing.

It's a bit unfortunate that even with the DeriveAnyClass extension, we cannot simply add Zero to the list of derived instances here, because GHC has difficulties inferring that the instance context should actually be empty. Perhaps a future version of GHC will be clever enough to recognise this. But in a standalone deriving declaration, we can explicitly provide the (empty) instance context and it is fine. In GHCi, we can see that this works:

GHCi> zero :: B1
B1 0
GHCi> zero :: B2
B2 0 (B1 0) 0

Solution using GHC generics

Let's look how we can do the same thing directly with GHC generics. Here, the code looks as follows:

{-# 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

The start is mostly what you also had in your question. The default signature for zero says that if a has a Generic instance and the type's generic representation Rep a is an instance of GZero, we can obtain a definition of zero by first calling gzero, and then using to to transform the generic representation into the actual type.

We now have to give instances for the GZero class. We provide instances for U1, K1, (:*:) and M1, telling GHC how to deal with unit types (i.e., constructors without arguments), constants, pairs (binary products) and metadata, respectively. By not providing an instance for (:+:), we implicitly exclude sum types (which was a bit more explicit via the IsProductType constraint in generics-sop).

The instance for U1 says that for a unit type we simply return the unique value.

The instance for constants (these are the arguments of a constructor) says that for these, we need them to also be an instance of the Zero class and use a recursive call to zero.

The instance for pairs says that in this case we produce a pair of gzero calls. This instance is applied repeatedly if a constructor has more than two arguments.

The instance for metadata says that we want to ignore all metadata such as constructor names and record field selectors. We did not have to do anything about metadata in generics-sop, because GHC generics mixes metadata into the representation of every value, whereas in generics-sop it is separate.

From here one, it's basically the same:

data B1 = B1 Int
  deriving (Generic, Show, Zero)

data B2 = B2 Int B1 Int
  deriving (Generic, Show, Zero)

This is a bit simpler, as we only have to derive a single Generic class, and in this scenario, GHC is clever enough to figure out the instance context for Zero, so we can just add it to the list of derived instances. The interaction with GHCi is exactly the same, so I won't repeat it here.

So what about mappend?

Now that we have zero which corresponds to mzero, perhaps you want to extend the class to cover mappend next. This is also possible, and of course, you're welcome to try it as an exercise.

If you want to see solutions:

For generics-sop, you can look at my ZuriHac talk from 2016 which explains generics-sop in a bit more detail and uses how to derive Monoid instances as the initial example.

For GHC generics, you can look at the generic-deriving package which contains many example generic programs, including monoids. The source code of the Generics.Deriving.Monoid module contains the class instances for GMonoid' which are corresponding to GZero above and also contain code for mappend.

这篇关于如何为Generic编写一个实例来派生函数,例如zero :: a(即常量)?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆