Haskell支持类型反射



Haskell是否像其他语言(如Python)一样支持类型反射?

这与Haskell不同 如何在Haskell中打印某些函数的信息,如"ghci> :info func">

需要明确的是,GHCi中所有以冒号开头的特殊命令(如:type:info等)都不是Haskell语言的一部分。GHCi通过"环绕"实际的Haskell并跟踪额外信息来支持他们。因此,这些功能在Haskell本身中都无法访问。如果Haskell中有什么东西可以实现类似的目标,我们需要在一个完全不同的地方寻找它。

键入擦除

Haskell被设计为在编译过程中完全擦除类型。

假设我们有一个像Just True这样的值。在编译Haskell源代码时,我们知道它是Maybe Bool型 .但是当转换为实际的机器代码时,生成该值的代码只会分配一个小内存块,带有少量数字和指向True值的指针。该数字用于判断构造函数是Nothing还是Just。处理Maybe值的所有代码都将被编译,以便它使用较小的数字来判断它是否应该采用Nothing构造函数或Just构造函数的分支;假设编译器为Nothing选择0,为Just选择1

但是,在那个小内存块中,没有任何内容表明这是来自Maybe数据类型的值,也没有说明指针指向类型为Bool的值。任何其他单参数构造函数也将在内存中表示为基本上只是一个小数字和一个指针,其中一些甚至可能使用用于Just的相同数量1。数字标签只需要区分来自同一类型的其他构造函数;没有全局注册表分配唯一编号以确保不同类型的类型不使用相同的编号。

因此,编译的Haskell代码实际上不可能查看任意值并告诉您它是什么类型。这些信息就这样消失了。

反射

Haskell确实支持类型的运行时反射。但是,在没有准备的情况下,它不能用于任何价值;这是一个选择加入系统,要求您明确准备在运行时访问类型信息。实际上,它使用类型类系统来要求编译器保留有关类型的信息。

编译器特别支持Typeable类。它是可以在运行时表示和检查的类型类。该类实际上包括每种类型;编译器会自动为您创建实例。对函数施加Typeable约束意味着编译器将向您提供Typeable方法,在运行时保持足够的信息可用,以便您可以询问"此值的类型是什么?如果对类型变量没有Typeable约束,则该信息在运行时不可用,并且无法请求类型。(这也意味着需要将Typeable约束添加到调用堆栈中的每个函数中,直到类型变量实际使用具体类型实例化为止;如果您的调用方尚未选择加入运行时类型反射,则无法单方面将其取回)

您实际使用它的方式是,您可以使用typeOf x生成类型为TypeRep a的值(其中ax的类型)。typeOf True给你一个TypeRep BooltypeOf (Just 'a')给你一个TypeRep (Maybe Char),等等。如果需要使用它,您可能不知道类型变量a实际上是什么,但是您可以使用eqTypeRep来测试您的TypeRep是否等于其他已知类型的TypeRep,如果是,您现在知道x属于该类型,并且可以调用特定于该类型的其他函数。你不能简单地使用基本的==测试来测试它是否等于已知类型,因为这只会给你TrueFalse,这并不能向编译器证明任何事情;它需要看起来有点复杂,但基本上归结为这个简单的想法。它可能看起来像这样:

{-# LANGUAGE GHC2021, GADTs #-}
import Type.Reflection ( Typeable, typeOf, typeRep, eqTypeRep, (:~~:) (HRefl) )
foo :: Typeable a => a -> Integer
foo x = case typeOf x `eqTypeRep` typeRep @Integer of
Just HRefl -> x + 17
Nothing -> 0

caseJust HRefl臂内部,编译器知道xInteger类型,因此添加17并将其作为函数的结果返回是有效的(必须是类型Integer)。在Nothing臂中,编译器将不允许您在x上使用Integer功能或返回它,因此我们必须返回我们知道是Integer的其他内容。

Type.Reflection中的其余功能允许您执行一些更灵活的检查(例如,您可以测试值的类型是否Maybe应用于某些内容,而不关心它应用于什么类型)。但最终它归结为这种能力,即获取类型为类型变量的值,并对其类型进行有限数量的"猜测"。如果你的任何猜测是正确的,你可以根据这些信息采取行动,但总有可能它不是你专门检查的任何类型,你仍然必须有一个分支,它只是一个黑盒值(尽管如果你不关心你的函数是总数,你总是可以error出来的)。

这规避了对类型为变量的值的正常限制;如果没有Typeable,除了将其传递给期望相同类型变量值的其他东西(例如已传入的匹配函数,或者类型类约束函数,如果变量有可用的约束),则无法对它执行任何操作。

多态性

从上面看可能不明显的一件事是我们无法反映多态性。也就是说,无法获取本身反映包含变量类型的TypeRep。如果一个函数采用[a]a该类型变量将在每次调用某个特定类型时实例化,并且TypeRep最终将反映此特定调用中使用的特定类型(如[Integer][Maybe Bool][Char -> Maybe (IO String)];它不会反映多态类型[a]

一句警告

Haskell 对编程的大量用处实际上来自对类型包含变量的值可以做什么的限制。当你习惯了它时,纯粹根据函数的类型来知道函数不能做什么是非常强大的。

当存在Typeable限制时,这完全超出了窗口。调用这样的函数时,您无法推断出它可以或不能对Typeable约束类型的值执行任何操作,因为您不知道它内部知道哪些类型。

举个简单的例子,人们常说函数id :: a -> a我们可以准确地从它的类型中知道它做了什么,因为类型为a -> a的函数唯一可能的(总)实现是返回其参数不变。但是如果它的类型是Typeable a => a -> a的,我们几乎无法知道它的作用。例如,这是有效的:

fakeId :: Typeable a => a -> a
fakeId x = case typeOf x `eqTypeRep` typeRep @Bool of
Just HRefl -> not x
Nothing -> x

fakeId返回大多数参数不变,但如果收到Bool则否定它。而且没有办法从外面看出它正在检查Bool并用它们做一些不同的事情;它也可以拥有大量具有特殊行为的类型。如果我们正在测试它的功能以查看它是否满足我们的要求,则无法保证我们会找到它具有特殊行为的所有类型,因此我们很容易在最终程序中出现错误。

因此,虽然Haskell有这个反射系统,但它真的不应该成为你的"标准"工具包的一部分。拥有大量具有Typeable约束的函数的 API 几乎可以肯定是一个糟糕的 API;我们想要类型擦除附带的限制。你需要做的99%的事情可以而且应该在没有反思的情况下完成。

相关内容

  • 没有找到相关文章

最新更新