Typeclass Metaproanganging介绍

2021-03-26 19:55:11

TypeClass Metaprogramming是一种强大的技术,可用于Haskell程序员,以自动生成静态类型信息的术语级代码。它已被习惯于几个流行的Haskell库(例如仆人生态系统)的巨大效果,并且它是用于通过GHC泛型实现通用编程的核心机制。尽管如此,存在很少的物质,阐述了这种技术,降级它仅仅为高级Haskell程序员已知的民间知识。

此博客员将尝试通过提供TypeClass Metaproanganging背后的基础概念概述。它不会尝试成为Haskell中类型级别编程的完整指南 - 这种任务可以轻松填写​​一本书 - 但它确实提供了最重要组件的解释和插图。这也不是Haskell初学者的博客帖子 - 熟悉Haskell类型系统的必要性,并且假设几个常见的GHC扩展 - 但它不假设类型级别编程的任何先前知识。

TypeClass Metaproanging是一个很大的主题,它在博客文章中删除它。要将其分解为更可管理的块,这篇文章分为几个部分,每个部分引入了新型系统功能或类型级编程技术,然后提出了如何应用它们的示例。

由于其名称意味着,TypeClass MetaCrogramming(HellentForth TMP 1)中心周围的Typeclass构造。传统上,键盘被视为原理操作员过载的机制;例如,它们通过EQ类支付了Haskell的多态==运算符。虽然这通常是思考TypeClasses的最有用方式,但TMP鼓励不同的视角:TypeClasses是从类型到(运行时)术语的函数。

那是什么意思?让我们用一个例子来说明。假设我们定义一个名为typeof的typeclass:

这个想法是这个类型的标准称为一些值,并将其类型的名称返回为字符串。为了说明,这里有几个潜在的实例:

实例TypeOf Bool其中Typeof _ =" BOOL"实例类型_ =" char"实例(类型A,Typeof B)=> Typeof(a,b)typeof(a,b)="(" ++ typeof a ++"" ++ typeof b ++")& #34;

请注意,Bool和Typeof Char实例都忽略了完全键入的参数。这是有道理的,因为TypeOf类的整个点是访问类型信息,无论提供哪个值,它都是相同的。要使此更明确,我们可以利用一些GHC扩展来消除值级参数:

此类型字母定义是一个很不寻常的,因为类型参数A不会出现在正文中的任何位置。要了解它的意思,请回想一下,用Typeclass的约束隐式扩展了TypeClass的每个方法的类型。例如,在定义中

通过表示产生的约束,隐式扩展了显示方法的全类型:

此外,如果我们明确地编写Foralls,则每个类型eclass方法也在类的类型参数上隐式量化,这使得以下完整类型的显示:

在相同的静脉中,我们可以写出全型类型,如我们类型的新定义所给出的:

此类型仍然不寻常,因为类型参数不会出现在=&gt右侧的任何位置;箭。这使得类型参数变得琐碎,这就是说GHC无法在任何呼叫网站上推断出什么是不可能的。幸运的是,我们可以使用TypeApplications直接传递一个类型,因为我们可以在更新的类型的类型(a,b)的定义中看到:

实例TypeOf Bool其中Typeof =" BOOL" char的实例类型=" char"实例(类型A,Typeof B)=> Typeof =&#34的类型(a,b);(" ++ typeof @ a ++"" ++ typeof @ b ++")"

这简明扼要地说明了如何将类型的类型视为从类型到术语的函数。我们的TypeOf函数非常简单地,一个接受单个类型作为参数的函数,并返回一个术语级字符串。当然,TypeClass的类型不是这种函数的特别有用的示例,但它展示了构造的容易程度。

消除Typeof的值级别参数的一个重要结果是,实际上不需要其参数类型。例如,从Data.void中考虑Void的TypeOf实例:

即使空隙是完全无人居住的类型,上面的例子也没有与Bool和Char上的一个不同。这是一个重要的点:当我们进入类型级编程时,重要的是要记住,类型的语言大多是这些类型的术语级别的含义。虽然我们通常会写入值为值的类型,但这并不重要。这在实践中表明是非常重要的,即使在某种内容中作为关于类型的定义:

实例类型a => typeof =" [" ++ typeof @ a ++"]"

如果需要一个值级别参数,而不仅仅是一种类型,我们上面的实例将在给定空列表时在泡沫中,因为它没有类型a的值来递归地应用typeof。但由于Typeof仅接受类型级参数,因此列表类型的术语级别含义不会造成障碍。

A可能对此属性的无意后果是我们可以使用TypeClasses在类型上写入有趣的功能,即使没有任何类型都没有居住。例如,考虑以下对类型定义:

构建这些类型的任何值是不可能的,但我们可以使用它们在类型级别构建自然数:

等等。这些类型可能看起来不太有用,因为它们没有任何值居住,但值得注意的是,我们仍然可以使用typeclass来区分它们并将它们转换为术语级别值:

导入数字.ReifyNAT A ReifyNAT :: Natural实例ReifyNAT Z其中ReifyNat = 0实例ReifyNAT A => Reifynat(s a)其中ReifyNat = 1 + ReifyNat @ a

随着其名称暗示,ReifyNAT在使用上面的数据类型中重新使用上述数据类型的类型自然数来重新定义为期级别自然值:

考虑ReifyNat的一种方法是作为类型级语言的解释器。在这种情况下,类型级语言非常简单,只捕获自然数,但通常,它可能是任意复杂的,并且可以使用类型来给它一个有用的含义,即使它没有术语级别表示。

通常,Typeclass实例不应该重叠。也就是说,如果您编写一个显示的实例(可能是a),则不应该为show(可能bool)编写一个实例,因为它不明确显示(刚刚真实)应该使用第一个实例或第二。因此,默认情况下,GHC一旦检测到它就会拒绝任何形式的实例重叠。

通常,这是正确的行为。由于Haskell的Typeclass系统旨在保留一致性 - 即,如果定义孤立实例,则始终选择相同的参数的相同组合始终选择相同的实例 - 重叠实例。但是,在执行TMP时,对该拇指规则进行异常是有用的,因此GHC提供了显式选择重新替换到重叠实例的选项。

作为一个简单的例子,假设我们想写一个类型的类型,检查给定类型是否是()是:

如果我们要编写一个普通的价值级别函数,我们可以写这样的东西,如伪哈尔克尔:

- 实际上不是有效的haskell,只是一个例子isUnit :: * - > BOOL ISUNIT()= TRUE ISUNIT _ = FALSE

但是,如果我们尝试将此转换为typeclass实例,我们会出现问题:

问题是函数定义具有从上到下匹配的已封闭的子句组,但键入的实例是打开和无序的。 2如果我们尝试评估IsUnit @(),则这意味着GHC将抱怨实例重叠,:

ghci> ISUnit @()错误:•使用“ISUnit”匹配实例引起的ISUnit()的重叠实例:实例ISUnit一个实例IsUnit()

{ - #重叠# - } pragma确实是什么? Gory详细信息在GHC用户指南中拼写出来,但简单的解释是{ - #重叠# - }只要实例严格更具体地比与它重叠的实例,就可以放松重叠检查器。在这种情况下,这是真的:IsUnit()是比IsUnit A更具体的特定于IsUnit A,因为前者只匹配(),而后者匹配任何东西。这意味着我们的重叠是良好的,实例分辨率应该表现得我们想要的方式。

在执行TMP时,重叠实例是一个有用的工具,因为它们使得可以以相同的方式编写类型的分段功能,以便在术语上写入分段功能。但是,他们仍然必须谨慎使用,因为没有理解他们的工作方式,他们都可以产生不行性的结果。有关如何出现问题的示例,请考虑以下定义:

守护::突破一个。 a - >串一个守护x = case isunit @ a true - >左"单位不允许"假 - >右X.

GuardUnit的意图是使用IsUnit来检测其参数是否为类型(),如果是,则返回错误。但是,即使我们标记了IsUnit()重叠,我们仍然会获得重叠的实例错误:

错误:•使用“ISUnit”匹配实例引起的ISUnit A的重叠实例:实例ISUnit A实例[重叠] IsUnit()•在表达式中:IsUnit @A

是什么赋予了?问题是GHC根本不知道A型在编译GuardUnit时是什么类型的。它可以将其调用的()实例化,但可能不是。因此,GHC不知道要选择哪个实例,并且仍然报告重叠的实例错误。

这种行为实际上是一个非常非常好的东西。如果在这种情况下,GHC盲目地选择ISUnit一个实例,那么守护箱总是掌握虚假分支,即使通过了类型()的值!这肯定不会是预期的,所以最好拒绝这个程序而不是默默地做错事。然而,在更复杂的情况下,即使使用使用{ - #重叠# - }注释,GHC也抱怨GHC抱怨的GHC抱怨的情况非常令人惊讶。

在这种情况下,在这种特殊情况下,易于纠正错误。我们只需为GuardUnit的类型签名添加ISUnit约束:

守护::突破一个。 IsUnit A => a - >串一个守护x = case isunit @ a true - >左"单位不允许"假 - >右X.

现在挑选正确的isUnit实例被推迟到使用遮盖套房的地方,并且接受定义。 3.

在上一节中,我们讨论了TypeClasses如何从类型到术语的函数,但类型为类型的功能呢?例如,假设我们希望总结两个类型的自然数,并因此获得新的类型级自然数量?为此,我们可以使用类型:

{ - #language typefamilies# - }类型系列总和a b其中sum z b = b sum(s a)b = s(总和a b)

以上是一个封闭式家庭,它可以像普通的haskell函数定义一样工作,只是在类型级别而不是值级别。例如,总和的等效值级别定义如下所示:

数据NAT = Z | S NAT SUM :: NAT - > nat - > NAT Z B = B SUM(S A)B = S(SUM A B)

正如你所看到的,这两个是非常相似的。两者都是通过一对模式匹配的条款定义,虽然这里无关紧要,但是封闭类型的系列和普通函数都将其子句顶到底部评估。

要测试我们在GHCI中的总和的定义,我们可以使用:善良!命令在尽可能减少时打印出类型及其类:

ghci> : 种类 !总和(s z)(s(s z))和总和(s z)(s(s z)):: * = s(s(s z))

类型族是在执行类型级编程时对类型的有用补充。它们允许计算完全在类型级别发生,这必须在编译时完全发生的计算,并且然后可以将结果传递给类型的方法以产生从结果的术语级值。

最后,到目前为止,使用我们讨论的内容,我们可以做我们的第一杆实用的TMP。具体而言,我们将定义类似于许多动态类型语言提供的类似相同的函数的平坦函数。在这些语言中,展平就像Concat一样,但它适用于任意深度的列表。例如,我们可能会使用它:

>扁平[[[1,2],[3,4]],[[5,6],[7,8]]] [1,2,3,4,5,6,7,8]

在Haskell中,不同深度的列表具有不同的类型,因此必须明确地应用多个级别的芯片。但是使用TMP,我们可以编写一个在任何深度的列表上运行的通用扁平函数!

我们的第一个挑战是写出换达的换态。由于参数可能是任何深度的列表,因此没有直接的方法来获取其元素类型。幸运的是,我们可以定义一个典型的家庭,即:

键入aupgy uponalofof [a] = [a]元素的元素[a] =一个扁平的扁平扁平:: a - > [ElementFa]

现在我们可以写下我们的扁平实例。基本情况是当类型是深度1的列表时,在这种情况下,我们没有任何展平:

归纳案例是类型是嵌套列表,在这种情况下,我们要应用Concat和Recur:

实例{ - #重叠# - }扁平[a] =>平坦[[a]]扁平x =扁平(concat x)

可悲的是,如果我们尝试编译这些定义,GHC将拒绝我们的扁平[a]实例:

错误:•CONN' t匹配类型'a',其中[a]'a'是一个刚性类型变量,由实例声明预期类型绑定:[componenof [a]]实际类型:[a] “扁平”的等式中的表达式:x在“扁平[a]'的实例声明中扁平x = x |扁平x = x | ^

起初腮红,这个错误看起来非常令人困惑。为什么GHC认为A和EMPORMOF [A]是相同的类型?好吧,如果我们为[INT]挑选了一种类型,请考虑会发生什么。然后[a]将是[[int]],嵌套列表,因此将适用第一种元素。因此,GHC拒绝匆忙地选择元素的第二方程。

在这种特殊情况下,我们可能会认为这是愚蠢的。毕竟,如果a是[int],那么GHC就不会选择展平[a]实例开始,它将选择下面定义的更具体的平坦[[a]]实例。因此,上面的假设情况永远不会发生。不幸的是,GHC没有意识到这一点,所以我们发现自己处于僵局。

幸运的是,我们可以通过向我们的平坦约束增加额外的限制来抚慰GHC的焦虑:

这是一个类型的平等约束。类型平等约束用语法A〜B写入,并且它们表示A必须与B的类型相同。类型平等约束在涉及类型的系列时主要有用,因为它们可以被使用(如在这种情况下),以便需要类型的系列减少到某种类型。在这种情况下,我们断言[a]必须始终是a的元素,这允许实例到typecheck。

请注意,这并不让我们完全触及我们的义务,因为当实例实际使用时必须先检查类型平等约束,因此最初可能似乎我们只推迟到以后延迟问题。但是,在这种情况下,这正是我们需要的:通过选择展平[A]实例时,GHC会知道,是不是列表类型,它就能ElementOf [A]减少到没有困难。实际上,我们可以通过在GHCI中使用扁平来实现这一点:

有用!但为什么我们需要1型注释?如果我们留出来,我们会得到一个相当毛茸茸的错误:

错误:•CONN' t匹配类型的“[A0]'与”ElementOF [A]'的预期类型:[ELEMENTOF [A]]实际类型:[ELEMENTOF [A0]] NB:'Elementof'是非嵌入型家庭类型变量'A0'是模棱两可的

这里的问题源于Haskell数字文字的多态性质。从理论上讲,有人可以定义一个num [a]实例,在这种情况下,其中1个实际上可以具有列表类型,并且可以根据num实例的选择匹配的任何一个元素。当然,没有存在这样的NUM实例,也不应该是它,但它被定义的可能性意味着GHC无法确定参数列表的深度。

在TMP的简单示例中,这个问题发生了很多,因为多态数字文字引入了歧义程度。在真实的程序中,这是一个问题的少得多,因为没有理由在完全硬编码的清单上呼叫平坦!但是,了解这些类型错误的含义和原因仍然很重要。

皱折,扁平是有用的TMP可以看起来像的一个功能示例。我们写了一个单一的通用定义,可以使用任何深度的列表,利用静态类型信息来选择运行时要做什么。

提出了上面的扁平定义,可能不会立即显而易见如何考虑从类型到术语的函数的趋势。毕竟,它看起来更像是“普通的”类型的类型(如,例如,Say,EQ或Show),而不是我们在上面定义的类型和ReifyNat类。

移动我们的角度的一种有用方法是考虑使用无点样式编写的等效变平实例:

实例([a]〜a)=>扁平[a]扁平= id实例{ - #重叠# - }扁平[a] =>平坦[[a]]扁平=扁平。拍

扁平的这些定义不再(句法)依赖于术语级参数,就像我们对TypeOf和ReifyNAT的定义不接受上述任何术语级别参数。这使我们能够考虑一个扁平可能“扩展为”给定一个类型的参数:

扁平@ [[int]]是扁平的@ [int]。拍摄,因为选择了扁平[[a]]实例。然后成为身份证。 Concat,可以进一步简化到求解。

扁平@ [[[int]]] flatten @ [[int]]。 Concat,简化了Concat。同样通过上述相同的推理。

这种网格非常自然地通过我们的类型作为从类型到术语的函数。 扁平的每个应用都需要一个类型作为参数,并产生一些组合的芯片。 从这个角度来看,扁平性正在执行一种编译时代代码生成,通过检查类型信息,合成表达式以执行级联。 这个框架是使TMP如此强大的关键思想之一,而且事实上,它解释了它是如何值得这个元标记的。 随着我们继续更复杂的TMP示例,尝试记住这种观点。 本博客文章的第1部分建立了在TMP中使用的基础技术,所有这些技术都与自己有用。 如果您阅读了这一点,您现在就知道它可以自己开始应用TMP,而这篇博客文章的其余部分将简单地继续构建您已经知道的内容。 在上一节中,我们讨论了如何使用TMP写一个 ......