在Julia中,typeof
可以获取对象类型,但和C++中的 decltype
不同,它获取的是对象的运行时类型,需要对表达式完成求值才能得到。然而有时我们可能希望不求值就得到表达式的类型,这在Julia中并没有直接提供内置方法来实现。
但是,我们注意到 Tests.@inferred
可以对推导出的类型和实际的运行时类型进行比较。显然,Julia其实提供了相应的方法获取推导的类型的。阅读其代码不难发现 Base.return_types
方法是推导类型的关键所在。这一方法在文档中并没有具体介绍,仅在手册中介绍方法分派的输出类型计算时作为一个反面例子出现。根据文档中的简单用例以及 @inferred
中的实现推断,可以知道它的签名类似
1 | Base.return_types(f, types::Tuple{Vararg{DataType}}) |
即根据函数 f
以及给定参数类型的元组 types
来得到可能的返回值类型。注意这个函数会返回一个类型数组,分别对应于可能匹配到方法的不同实例的返回类型。例如
1 | > f(x::Int) = x^2 |
不过 Base.return_types
的调用方式显然不太直观,毕竟Julia不能直接改变方法调用的求值顺序。但我们可以通过宏来对其“转译”一下,将普通的函数调用表达式转换成满足参数格式要求的结构。
一个粗浅的想法是直接照搬 @inferred
的实现,但由于 @inferred
的应用目标相对较窄,只处理了恰好是单个函数调用的情况,而对于内部的参数依然还是会进行求值。如果要进行推广,最简单的方式是递归地插入获取类型的代码。然而 Base.return_types
返回值是一个数组,需要将结果进行合并,则还要 typejoin
和 reduce
完成类型的合并,使调用即类似于
1 | reduce(typejoin, Base.return_types(f, types)) |
而对于非函数调用的叶子(参数)节点,通常的 typeof
固然可以,但是考虑到存在类似 parse
的类型选择器,typeof
为每个类型都返回 DataType
就显得有些模糊。因此,不妨自己定义一个更精确的 typeof
:
1 | exacttypeof(::T) where {T} = T |
在此基础上,加上简单的封装和宏展开基本就大功告成了
1 | function uneval_typeof_impl(expr) |
简单试一下:
1 | > 1+2*3 |
不过这个方法只适用于简单的函数调用表达式,对于包含表达式块或者生成器等不能简单表示为单个函数调用组合的情况则会出错。
而且,Base.return_types
也不适用于内置的函数(例如,===
、Core.sizeof
等),因此也无法处理包含这些函数的表达式。