Ben*_*Ben 6

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

类型擦除

Haskell 被设计为在编译期间完全擦除类型。

假设我们有一个像 的值Just True。当编译 Haskell代码时,我们知道它的类型是Maybe Bool。但是,当转换为实际的机器代码时,生成该值的代码将仅分配一小块内存,其中包含一个小数字和一个指向该True值的指针。该数字用于判断构造函数是NothingJust。所有处理值的代码都将被编译,以便它使用小数字来判断是否应该采用构造函数或构造函数Maybe的分支;假设编译器选择for和for 。NothingJust0Nothing1Just

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

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

反射

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

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

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

{-# 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
Run Code Online (Sandbox Code Playgroud)

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

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

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

多态性

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

警告的话

Haskell 对编程的大部分用处实际上来自于对类型包含变量的值可以执行的操作的限制。当您习惯了它后,纯粹根据函数的类型来了解函数不能做什么是非常强大的。

当有限制时,这完全是不可能的Typeable。当调用这样的函数时,您无法推断出它可以或不能对Typeable-constrained 类型的值执行什么操作,因为您不知道它内部知道什么类型。

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
Run Code Online (Sandbox Code Playgroud)

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

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