跳至内容
GitHub 存储库 论坛 RSS 新闻源

这是一种魔法

Ary Borenzweig

typeof 的故事

表达式 typeof 的故事始于数组字面量。在 Crystal 中,你可以写

array = [1, 2, 3]

编译器会推断该数组为 Array(Int32),这意味着它只能包含 32 位整数。你也可以写

array = [1, 'a', true]

编译器会推断它是一个 Array(Int32 | Char | Bool),其中 Int32 | Char | Bool 表示这些类型的并集:数组可以在程序执行期间的任何时间点包含这些类型中的任何一个。

语言中的字面量,如数组、哈希和正则表达式 (regex) 字面量,是对标准库调用的简单语法重写。对于正则表达式,以下内容

/fo(o+)/

被重写为

Regex.new("fo(o+)")

数组字面量的重写需要更多思考。数组是泛型的,这意味着它们是用类型 T 参数化的,该类型指定它们可以容纳的类型,就像前面提到的 Array(Int32)Array(Int32 | Char | Bool) 一样。创建数组的非字面量方法是

Array(Int32 | Char | Bool).new

对于数组字面量,我们需要该类型成为数组字面量中所有元素的并集类型。因此,typeof 诞生了。最初,这被称为 type merge,它是一个编译器内部的东西,你无法表达(没有语法),但编译器将它用于这些字面量。一个重写示例

array = [1, 'a', true]

# Rewritten to this, where <type_merge>(exp1, exp2, ...) computes
# the union type of the expressions:
Array(<type_merge>(1, 'a', true)).build(3) do |buffer|
  buffer[0] = 1
  buffer[1] = 'a'
  buffer[2] = true
  3
end

现在,这个字面量正在调用一个 普通方法 来构建一个数组。问题是你不能这样写:<type_merge> 只是这个内部节点的表示,允许你计算一个类型,但如果你写了上面的内容,你将得到语法错误。

我们后来决定,因为这个 <type_merge> 节点运行良好,我们希望字面量没有魔法,让用户使用这个 <type_merge> 节点,并将其命名为 typeof,因为这个名字在其他语言中很熟悉。现在,这样写

array = [1, 'a', true]

和这个

Array(typeof(1, 'a', true)).build(3) do |buffer|
  buffer[0] = 1
  buffer[1] = 'a'
  buffer[2] = true
  3
end

完全等效:没有魔法(但当然第一种语法更容易编写和阅读)。

我们当时并不知道,typeof 会给这门语言带来巨大的力量。

typeof 的简单用法

typeof 的一个明显用例是询问编译器表达式的推断类型。例如

puts typeof(1) #=> Int32
puts typeof([1, 2, 3].map &.to_s) #=> Array(String)

在这一点上,你可能会认为 typeof(exp)exp.class 类似。然而,前者提供编译时类型,而后者提供运行时类型

exp = rand(0..1) == 0 ? 'a' : true
puts typeof(exp) #=> Char | Bool
puts exp.class   #=> Char (or Bool, depending on the chosen random value)

另一个简单的用例是根据另一个对象的类型创建类型

hash = {1 => 'a', 2 => 'b'}
other_hash = typeof(hash).new #:: Hash(Int32, Char)

通过这种方式,我们可以避免重复或硬编码类型名称。

但这些都过于简单,没有趣味性。

typeof 的高级用法

让我们编写 Array#compact 方法。此方法返回一个 Array,其中删除了 nil 实例。当然,如果我们从一个 Array(Int32 | Nil) 开始,即一个整数和 nil 的数组,我们希望以一个 Array(Int32) 结束。

类型语法允许创建并集。例如,Int32 | Char 创建一个 Int32Char 的并集。但是,没有办法减去类型。没有 T - Nil 语法。但是,使用 typeof,我们仍然可以编写此方法。

首先,我们定义一个类型为我们想要的类型的函数

def not_nil(exp)
  if exp.is_a?(Nil)
    raise "oops, nil"
  else
    exp
  end
end

如果 expnil,我们抛出异常,否则我们返回 exp。让我们检查它的类型

puts typeof(not_nil(1))   #=> Int32
puts typeof(not_nil(nil)) #=> NoReturn

由于 if var.is_a?(…) 的工作方式,当我们给它一个不是 nil 的东西时,它告诉我们类型是相同的类型。但是,当我们给它 nil 时,if 中唯一可以执行的分支是 raise 分支。现在,raise 有这个 NoReturn 类型,它基本上意味着该表达式没有返回值……因为它抛出了异常!另一个具有 NoReturn 的表达式是,例如,exit

让我们尝试给 not_nil 一个并集类型的东西

element = rand(0..1) == 0 ? 1 : nil
puts typeof(element)          #=> Int32 | Nil
puts typeof(not_nil(element)) #=> Int32

注意,NoReturn 类型消失了:“预期”的最后一个表达式的类型将是 Int32 | NoReturn,即该方法的可能类型的并集。但是,NoReturn 没有实际值,所以将 NoReturn 与任何类型 T 混合,基本上会给你 T 回来。因为,如果 not_nil 方法成功(即它没有抛出),你将获得一个整数,否则一个异常将通过堆栈冒泡。

现在我们已经准备好实现 compact 方法

class Array
  def compact
    result = Array(typeof(not_nil(self[0]))).new
    each do |element|
      result << element unless element.is_a?(Nil)
    end
    result
  end
end

ary = [1, nil, 2, nil, 3]
puts typeof(ary)       #=> Array(Int32 | Nil)

compacted = ary.compact
puts compacted         #=> [1, 2, 3]
puts typeof(compacted) #=> Array(Int32)

神奇的一行是该方法中的第一行

Array(typeof(not_nil(self[0]))).new

我们创建一个数组,其类型是调用数组第一个元素上的 not_nil 所得到的类型。请注意,编译器不知道数组中每个位置的类型,所以使用 01123 将是一样的。

通过这种方式,我们能够创建一个排除 Nil 的类型,而无需扩展类型语法:编译器类型推断算法的机制是我们需要的全部。

但这仍然很简单。让我们继续讨论一些**真正**有趣和好玩的东西。

typeof 魔法

我们的下一个任务是实现 Array#flatten。此方法返回一个 Array,它是原始数组的一维扁平化(递归)。也就是说,对于每个是数组的元素,将它的元素提取到这个新数组中。

注意,这必须递归地工作。让我们看看一些预期的行为

ary1 = [1, [2, [3], 'a']]
puts typeof(ary1)             #=> Array(Int32 | Array(Int32 | Array(Int32) | Char))

ary1_flattened = ary1.flatten
puts ary1_flattened           #=> [1, 2, 3, 'a']
puts typeof(ary1_flattened)   #=> Array(Int32 | Char)

和以前一样,让我们先写一个类型是我们要用于扁平数组的类型的函数

def flatten_type(object)
  if object.is_a?(Array)
    flatten_type(object[0])
  else
    object
  end
end

puts typeof(flatten_type(1))                          #=> Int32
puts typeof(flatten_type([1, [2]]))                   #=> Int32
puts typeof(flatten_type([1, [2, ['a', 'b']]]))       #=> Int32 | Char

该函数很简单:如果该对象是一个数组,我们希望获取其任何元素的扁平化类型。否则,类型是该对象的类型。

有了这些,我们就可以实现扁平化了

class Array
  def flatten
    result = Array(typeof(flatten_type(self))).new
    append_flattened(self, result)
    result
  end

  private def append_flattened(object, result)
    if object.is_a?(Array)
      object.each do |sub_object|
        append_flattened(sub_object, result)
      end
    else
      result << object
    end
  end
end

在这个第二个示例中,我们能够创建一个数组扁平化的类型。

结论

最后,typeof 本身并没有什么真正神奇的地方。它只是让你能够很好地查询和使用编译器推断表达式的类型的能力。

在之前的示例中,我们能够使用常规的东西(类型和方法)从其他类型中构建类型。这里没有新的知识需要学习,也没有专门的语法来描述类型。这很好,因为它简单而强大。