人类思维是不考虑类型的。要写类型的语言,都会妨害写代码的思维流畅度。设计得差的类型系统,还会妨碍你写出通用的代码,为了适应它,你不得不学习奇特的技巧和设计模式。比较强力的类型系统,写类型 spec 可能是普通代码量的好几倍,经常好几小时跑不完,完全检查几乎不可能。
另一方面,人类思维也经常出 bug,怕改错代码也会需要一些额外手段去保证程序的正确性。理想的类型标注是程序正确性的证明 —— 但现实生活中 QA 员工才是程序正确性的“证明”……
这次 Hello World,就造一个半吊子的类型系统。
类型是什么
众所周知,类型就是集合。构造主义要求高些:类型是可构造的集合。构造法有以下几种:
有些集合可以被"计算",也就是可以一一到上至某个自然数的子集。编程中碰到的所有类型其实都是可枚举的。
几个例子:
- ⊥: {}
- Bool: {true, false}
- Int: {0, 1, -1, 2, -2, 3, -3, ...}
- Prime: {1, 2, 3, 5, 7, 11, ...}
- Double: {(double)0x0, (double)0x1, (double)0x2, (double)0x3, ...}
- String: {"a", "b", "aa", "ab", "ba", "bb", ...}
通过“精炼类型” (Refinement type) 的方法,也可构造:
- Odd: f(Int) , f(x) = 2x + 1
- UnsignedInt: {x | x <- Int, x >= 0}
或者说,可枚举的集合,通过对其元素列表领悟 (List comprehension) 操作,产生的结果也是可枚举集合。
函数也可以有类型,函数类型(Function type)是类型系统开始变复杂的地方。在类型推断很弱或者没有的静态类型语言里写 lambda 很累。
参数化类型(Parametric type)允许定义类型函数,通过这个函数复合出新的类型。
类型函数如果支持非类型的参数,那就是依赖类型(Dependent type)。
如果类型没法在编译时检查好,得在运行时检查,那就是契约(Contract)。
类型还有两种复合:和与积(Union, Cartesian product)。
子类型(Subtyping)在很多语言中很常见,但其使用场景可以用类型复合代替。
我们还有行为类型(Behavioral type),通过对象所支持的操作来定义。不过行为类型的计算不太好区分编译时和运行时,这里暂且不搞。
综上需求,先给我们的轮子设计点语法。
表达类型的语法
如果要进行编译期类型检查,往往需要一套特殊语法,把类型表达式和普通表达式隔离开来,方便在编译期完成类型表达式的求值。
我们用顶行的冒号,来表达下个表达式的类型;用 =>
表达函数类型。例如:
: Int
x = 0
: Double Double Double Double => Double
def minkowski_distance t x y z
t ** 2 - x ** 2 - y ** 2 - z ** 2
end
用括号表达参数化类型
: Matrix(t, a, b) Matrix(t, b, c) => Matrix(t, a, c)
def matrix_mul ma mb
(ma.rows.zip mb.cols).map -> [x, y] # Tomb 语言和 Hongmeng 一样,是开源的
x * y
end
end
: => Array(Int, 3)
def one
[1, 1, 1]
end
类型的积和结构体的 field 是一回事,留给结构体定义就可以了:
struct Foo[
xip: Int
baz: String
]
类型的和 (Union) 用 |
表达:
: Int | String
a = 3
类型也可以嵌套,用括号包起来
: Array (Any => Any) => Array
def map a f
对于精炼类型,我们先添加一个声明断言的语法 pred,就像这样:
pred GT8
self.size > 8
end
pred EQ8
self.size == 8
end
然后再在类型里代入断言,这样类型的语法可以保持比较简单,利用上面声明的两个断言就像这样:
: String>8 => String&EQ8
def take8 s
s.take 8
end
综上,这是我们设想的类型表达式语法:
Type : (Term* induce)* Term # 注意 => 是右结合的
Term : lparen Type (or Type)* rparen | const Param? Refinement?
Param : tlparen Vars? rparen
Refinement: ampersand const
Vars : var (comma var)*
对应的词法:
[a-z_][a-z_0-9]* var
[A-Z][a-z_0-9]* const
(?<=[a-z_0-9})])( tlparen # tight left paren
( lparen
) rparen
& ampersand
, comma
=> induce
| or
下面实现个类型运算的原型。先准备点基础设施,然后加上类型运算。
嗯用 Ruby 写比用相继式演算写要简单些 (题图误导)……
作用域
众所周知周知,作用域分词法作用域和动态作用域。词法作用域只用分析当前文件就能弄清楚;而动态作用域需要运行时才能弄清楚,JavaScript 那么难用,部分原因就是放太多东西在动态作用域里了。还有的作用域规则通过分析多个文件
变量、常量和结构体一样响应 name 方法
Var = Struct.new :name, :type
Const = Struct.new :name, :type, :methods # 为 struct 时带 methods
作用域用哈希表达即可,例如
{
"x": "Int"
"y": "String"
}
函数类型用数组表达, 例如
#: Int Int => Int
["Int", "Int", "Int"]
考虑到预置的环境,初始化类型系统
class Type
VAR_PRELUDE = {}
CONST_PRELUDE = {}
def initialize
@var_scopes = [VAR_PRELUDE, {}]
@const_scopes = [CONST_PRELUDE, {}]
end
end
作用域的操作 (lambda / method 会给变量作用域)
def push_var_scope args
new_scope = {}
args.each do |name, ty|
new_scope[name] = Var[name, ty]
end
@var_scopes.push new_scope
end
def pop_var_scope
raise "mismatch var push/pop" if @var_scopes.size == 1
@var_scopes.pop
end
def push_const_scope
@const_scopes.push
{}
end
def pop_const_scope
raise "mismatch const push/pop" if @const_scopes.size == 1
@const_scopes.pop
end
定义常量 / 变量
def define_var name, type
@var_scopes.last[name] = Var[name, type]
end
def define_const name, type, methods=nil
@const_scopes.last[name] = Const[name, type, methods]
end
查找变量/常量/结构体
def lookup_var name
@var_scopes.reverse_each do |scope|
var = scope[name]
return var if var
end
nil
end
def lookup_const name
@const_scopes.reverse_each do |scope|
const = scope[name]
return const if const
end
nil
end
查找方法类型
def lookup_method const, method_name
if const.methods
method = const.methods[method_name]
raise "method #{method_name} not found" if !method
method
else
raise "const is not a struct"
end
end
匹配类型麻烦些,因为嵌套的函数类型也要判断相等。
如果认为任意类型可以和 "Any" 类型互换,那就支持了渐进类型
def type_match? param_ty, arg_ty
if arg_ty == 'Any' or param_ty == 'Any'
true
elsif !param_ty.is_a?(Array) and !arg_ty.is_a?(Array)
arg_ty == param_ty
elsif param_ty.is_a?(Array) and arg_ty.is_a?(Array)
param_ty.size == arg_ty.size and
param_ty.zip(arg_ty).all?{|(a, b)| type_match? a, b }
else
false
end
end
类型运算
由于是半吊子类型系统,不用实现推导(Type inference),就实现个类型演绎(Type induction)好了。另外为了代码更简单,和 Scala 一样做乞丐版类型演绎好了——也就是只对 lambda / method 内进行类型推导,不进行全局类型推导。
没标注类型的变量,如果位于赋值表达式左边或者函数参数,可以自动标注其类型。
进行类型运算,我们先得对源程序 AST 做遍历,转换成类似三位址码 (Three address code)的赋值表达式(实际上我们用不限定地址的更灵活的方式)。对字面量,我们替换成一个特殊的无参函数赋值表达式,用函数的返回值类型去标注,这样代码要处理的 case 就少很多了。
CallAssignment = Struct.new :dst, :receiver, :method_name, :args, :method
ValueAssignment = Struct.new :dst, :src
我们的类型演绎就和数独填空一样,循环填空直至到达不动点
def annotate_types
updated = false
until not updated
updated = false
assignments.each do |a|
annotate a
end
end
end
def annotate obj
updated = false
case a
when CallAssignment
if a.receiver.type != 'Any'
a.method ||= lookup_method a.receiver, a.method_name
if a.method
if a.dst.type != a.method.type.last
a.dst.type = a.method.type.last
updated = true
end
a.args.zip a.method.type[0...-1] do |arg, param|
if !arg.is_a? Literal and arg.type != 'Any' and param.type != 'Any'
arg.type = param.type
updated = true
end
end
end
end
when ValueAssignment
if obj.dst.type == 'Any' and obj.src.type != 'Any'
obj.dst.type == obj.src.type
updated = true
elsif obj.dst.type != 'Any' and obj.src.type == 'Any' and !obj.is_a? Literal
obj.src.type == obj.dst.type
updated = true
end
end
updated
end
对于精炼类型,我们可以生成断言的表达式,连同其他类型信息一起扔给 Z3 计算。当然,由于我们是半吊子的类型系统,就先不管了。
类型校验
在类型计算中检查矛盾,抛错就可以了。
搞定。