利用编程语言的类型系统降低复杂性
柏舟 新冠4年 04-27
初学者常常发现动态语言编码简单,其中一个原因是因为无法充分利用静态语言的类型系统,降低心智负担。
当我初学编程的时候,我喜欢使用数组存数据,大脑记忆每个字段的含义。在刚学python的时候,我被python的类型系统震惊了,只学过C的我根本不理解为什么存在dict/map这种数据结构。后来了解了json和lisp才发现使用list和dict居然可以组合出所有想要表达的数据结构。但是,随着代码越写越多,我发现单纯使用list和dict的数据结构并不适合大型项目。
这是因为大型项目对象与对象之间存在一些业务规则,其中一些从属于、继承、引用的关系可以使用类型系统进行约束检查。这时,业务的部分复杂性就转移到类型系统中。而动态语言,虽然初期使用list和dict迅速写出数据结构,但是每个字段的含义和业务规则只通过约定,使用大脑记忆推理,项目后期基本无法维护。
通用技巧:使用类型包装规则
只要数据存在规则,数据之间存在约束,请定义新的类型。
典型的例子是每个物理量单独定义类型,而不是使用double。尤其是对于一个物理量同时存在标准和非标准定义,比如定义角度Angle,就可以通过定义初始化方法区分数据的单位是Radians还是Degree,并且可以实现ToString方法提高Print的可读性。此外,Modelica语言通过检查量纲是否存在错误来辅助编码,Rust也有类似的Units库。
第二个例子是URI、URL,一般的做法是直接使用uint或者int表示ID。ID从int改成string是一个很常见的情况,尤其是用户ID引入第三方认证,你很可能想要在ID加一个标记变成URI,却发现相应的SQL语句使用了int,可能还有一些细枝末节的地方需要修改,最后根本不敢改动。其次,int或string很容易和其它类型搞混,特别是在序列化和反序列化的代码中,很容易忘了处理。使用类型系统可以减少这些错误,当你编码的时候,借助IDE,可以清晰的看到ID和其它类型是不同的。并且如果需求改变,需要增加序列化过程也可以包装到类型的相关方法中,方便维护。
通过将规则包装到类型和相应的方法中,能够让人更清晰的认识到类型的定义域/约束是与普通类型(如int, string)和其它类型不同,并且提高代码的可维护性。如果发现项目中类型太多了,连写代码的人都记不清类型和规则,那就应该思考是系统本身确实复杂还是系统设计本身是一坨屎。
不同语言的类型系统
类型系统基本决定了一个语言的主要领域,它与GC、泛型高度相关。这篇文章偏向初学者,所以有的概念不好深入。而且我有的语言(C/C++,Java)写得很少,说得不对的地方敬请指正。
这个部分主要介绍不同语言的类型系统可以实现的额外约束,从而将业务的复杂性转移。
- Rust的类型系统是我用过的最灵活的类型系统(C++和一些函数式语言可能更强)。这个灵活指的是它的泛型远比Go、Java表达能力强而且安全。它可以很方便的描述类型具有的方法,使用泛型时描述类型之间的约束关系。由于Rust语言设计时非常重视安全性,所以,可以通过定义约束关系,充分利用Rust的类型系统检查代码问题。然而缺点是代码虽然小的改动检查很方便,但是大的改动非常坐牢。再加上它的零成本抽象只针对计算机,而不是人脑,所以狗都不用Rust写业务。
- Go和Java的类型系统比较一般,尤其是泛型同样地弱。如果只是使用interface还好,一般的多态需求都可以实现,但是就是有些约束没有办法使用类型系统实现。比如,浮点数、多项式、矩阵都有加法减法,但是你很难把它们抽象成同一个interface,因为多项式和矩阵之间无法加减。如果硬写的话,可以在运行时判断interface的类型,如果类型不同就抛出异常,但是这个和动态语言没有什么区别。反而是Rust的trait可以表达这个约束。而且,Go和Java的泛型基本什么都做不了,唯一的用处就是实现一些容器和简单类型的通用算法。Go的泛型虽然使用interface作为泛型约束,但是大多数情况和直接使用interface没有什么区别。
- C#比Go和Java强,可以看出来Rust借用了C#的where关键字。但是截止目前没有办法表示类型支持+,-符号这些约束,但可以通过dynamic解决。C#赋予了程序员足够的自由,你可以放弃类型系统的约束检查,但是可以通过这些不安全的行为换取更大的自由度和表达能力。
- Modelica实现了单位的量纲检查。
- Ada可以定义类型的定义域,数组的大小和起始索引。
- smalltalk基于方法(消息)实现多态。它可以使用反射实现动态类型,比如说一个数组存在搜索方法,当数组排序后,数组类型变为有序数组,此时搜索自动使用二分搜索。如果使用Java实现类似特性,可能需要针对每个类型实现无序数组和有序数组,但基本是不可能的。
为什么需要使用类型系统?主要是因为类型系统契合面向对象设计的部分理念。程序员通过与编程语言约定,将一部分心智负担转移到类型系统中,从而实现更复杂的设计。否则很容易导致业务规则的泄漏,在各个函数内部硬编码实现一些序列化逻辑和规则,然后需求一更改,屎山就成了。
也不知道有没有人喜欢看,有没有人对面向对象的设计模式和群论感兴趣。