这是一种魔法
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
创建一个 Int32
和 Char
的并集。但是,没有办法减去类型。没有 T - Nil
语法。但是,使用 typeof
,我们仍然可以编写此方法。
首先,我们定义一个类型为我们想要的类型的函数
def not_nil(exp)
if exp.is_a?(Nil)
raise "oops, nil"
else
exp
end
end
如果 exp
是 nil
,我们抛出异常,否则我们返回 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
所得到的类型。请注意,编译器不知道数组中每个位置的类型,所以使用 0
、1
或 123
将是一样的。
通过这种方式,我们能够创建一个排除 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
本身并没有什么真正神奇的地方。它只是让你能够很好地查询和使用编译器推断表达式的类型的能力。
在之前的示例中,我们能够使用常规的东西(类型和方法)从其他类型中构建类型。这里没有新的知识需要学习,也没有专门的语法来描述类型。这很好,因为它简单而强大。