引入
之前和组内同学交流,说Java/Scala写法很啰嗦,类型标注一长串,set/get又是一长串,所以lombok才会火,lombok这种hook又不知道什么时候会被修复。而python就可以很简洁,可以表达丰富的语义。会python就够了,一招鲜吃遍天。
的确,python的dynamic type是一种很高级的泛型,很native,很高级。开发起来也很快,大家都很喜欢。
代价是什么呢?代码回归成本高、引用不透明、不可推理、运行时bug多,都是dynamic type的问题,所以现在新兴的语言,比如rust,ts,go都是静态类型系统。虽然开发效率不如python,但是安全性高,可讀性好,易於維護。普遍被认为更加适合大型的软件系统开发。
至于是不是一招鲜吃遍天,这就是一个跳出types的问题了,从我一个半吊子plt人的观点看,肯定不是的:不同的语言代表着不同的抽象,如果不去横向对比各种语言,是没有办法真正领悟语言feature trade off的精髓的。即使你不会用到scala或者haskell这些语言,也不妨碍你学习它们设计的优秀支持,不妨碍在你使用的语言里面表达设计的亮点。从不同的抽象看待问题,最终代码的复杂度也是不一样的。
比如C++里面STL库就借鉴了trait的思想,lombok里面的@cleanup注解就很类似于Go中的defer,rust中的宏就借鉴了schemer的思想等等。
好啦,不扯那么远,本篇的内容是介绍Scala的类型系统和一些其它语言的类型设计。
类型系统
什么是类型系统
Informally, a _type system _consists of (1) a mechanism to define types and asso- ciate them with certain language constructs, and (2) a set of rules for type equiv- alence, type compatibility, and type inference.
——《programming language pragmatics》
名字(Names,Scope,Bindings)、控制流(control flow)、类型系统和复合类型(type system and composite types)、子程序和控制抽象(subroutines and control abstraction)、数据抽象和面向对象(data abstraction and object orientation)是PL(Programming Language)的核心特性(Feature)。了解任何一种语言都要学习它所提供的这些特性。我们上一讲的函数式编程(FP)跟名字,作用域,子程序和数据抽象都有关系。除此之外,学习Scala很重要的是要了解它的类型系统。
这里我们介绍广义上的类型系统定义和Scala中的类型系统设计。
所谓类型系统指的是包括一种定义类型并将它们与特定的语言结构相关联的机制以及一集有关类型等价(type equivalence)、类型相容(type compatibility)和类型推理(type inference)的规则。
类型等价是决定什么时候两个值的类型相等的规则,类型相容是决定什么时候一个值可以用在给定上下文中的规则,类型推理根据表达式的组成和其上下文决定了表达式的类型。
强类型/弱类型,静态类型/动态类型,显式(Manifest)类型/推断(inferred)类型,名义(Nominal)类型/结构化(Structural)/duck type
Strong vs. Weak && Static vs. dynamic
如果一种语言以语言实现可以强制的方式禁止将任何操作应用到不打算支持该操作的任何对象,则该语言被称为强类型语言。如果一种语言是强类型的,并且可以在编译时执行类型检查,则该语言称为静态类型的。动态(运行时)类型检查可以被看作是延迟绑定的一种形式,而且往往在将其他问题延迟到运行时的语言中也会出现。因此,静态类型是旨在提高性能的语言的标准;动态类型在旨在简化编程的语言中更为常见。
上面的定义可能并不准确,还有一种更加形式化的定义:
导致程序终止执行的错误我们称为Program Errorstrapped errors,这种情况下程序无法继续执行,比如说除以0、Java中的数组越界访问;程序出错之后可以继续执行的错误我们称为untrapped errors,这种情况下可以继续执行,但是会导致未定义的行为(Undifined Behavior)。比如C中的缓冲区溢出、错误的地址访问。
有些语言指定了一组Forbidden Behaviours,包含所有的untrapped errors,如果程序运行时不会出现Forbidden Behaviours,那么它就是强类型的,反之为弱类型。
如果在编译时拒绝就可以拒绝Forbidden Behaviours,那么就是静态的,运行时拒绝Forbidden Behaviours,就是静态的。
根据如上的定义,我们可以知道,C++是一种弱类型静态的语言,Scala则是一种强类型静态的语言,python则是强类型动态的语言。
Manifest vs. inferred
显式类型和推断类型是对于静态语言而言的,因为显式类型和推断类型都是在编译期进行的。
如果变量中类型必须显式声明,那么就是显式类型的,比如C。否则是推断类型的,比如大多数的现代语言。
Nominal vs. Structural vs duck
名义类型和结构化类型同样是对于静态语言而言的。
名义类型就是指名字显式标注。也就是说,名义类型系统中,两个变量是否类型兼容(可以交换赋值)取决于这两个变量显式声明的类型名字是否相同。
结构化类型使用的比较少,typescript的类型系统是结构化的。结构化类型通过编译期的类型结构进行类型等价和类型推理。
如果这种能力出现在动态语言中,那么我们称之为duck type。
但是一般现代语言也会在某些feature中使用Structural type,比如:
Go的Interface(实现了String interface):
package main
import (
"fmt"
"math"
)
type geometry interface {
area() float64
perim() float64
}
type rect struct {
width, height float64
}
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
return 2 * math.Pi * c.radius
}
func measure(g geometry) {
fmt.Println(g)
fmt.Println(g.area())
fmt.Println(g.perim())
}
func main() {
r := rect{width: 3, height: 4}
measure(r)
}
Rust的Tuple也是一种Structural Type,Reddit上有一场关于这一点的讨论 Reddit,比较有趣的观点是:Structural Type是匿名的Nominal Type。
C++20的concepts也是Structural的,这主要是相对于trait/type class/protocol而言
template <typename T>
concept equality_comparable = requires (T obj) {
{ obj == obj } -> std::same_as<bool>;
{ obj != obj } -> std::same_as<bool>;
};
struct vec2
{
float x, y;
// define the required operators,
friend bool operator==(vec2 lhs, vec2 rhs)
{
return lhs.x == rhs.x && lhs.y == rhs.y;
}
// operator!= not needed in C++20 due to operator rewrite rules!
};
static_assert(equality_comparable<vec2>);
python的duck type,想必大家都已经非常熟悉。
# Python program to demonstrate
# duck typing
class Specialstring:
def __len__(self):
return 21
# Driver's code
if __name__ == "__main__":
string = Specialstring()
print(len(string))
Scala中也有structual type,下文中会提到这一点。
类型系统的正交性
语言设计十分注重正交性,在类型系统中也是如此。正交性意味着特征可以在任何组合中使用,这些组合都是有意义的,并且给定特征的意义是一致的,而不管与它结合的其他特征是什么。高度正交的语言更加容易被使用、理解和推理。
在现代语言中我们往往不会注意表达式和语句之间的区别,这正是它们模糊其语义而增强正交性的结果。比如C++中的Void和Scala中的Unit。它们意味着我们只是强调调用值的副作用。
如果没有这样的关键字,那么我们只能使用哑变量(dummy value)表达这一点:
dummy := insert_in_symbol_table(bar); //Pascal
除了这之外,另一个正交性的例子是Optional和Maybe,它们表示“不是我们所限定的类型”。C++和Scala采用这种模式。另一种被广泛使用的例子是尾部的"?"(tail question mark),C#,rust,swift采用了这种形式。
比如:
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("username.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
UB、soundness和completeness
在计算机编程中,未定义行为(UB)是在计算机代码所遵循的语言规范中执行其行为被规定为不可预测的程序的结果。
所谓sound,就是说程序永远不会进入表达式求值结果与表达式静态类型不匹配的状态。untrapped errors不会出现,而是由静态和动态检查抛出异常。java和scala是soundness的,而python就不是soundness的。
从类型系统到Scala
Scala类型系统的形式化基础
F System
HM是类型推导的基础,也是Scala类型系统的基础。大部分类型推导都是要求函数的参数显示的标注类型,HM把参数类型标注为一个未知变量,然后再后面的使用的地方列方程,解方程,把它的类型解出来。
在HM类型系统之上,定义了一套类型规则,包括变量查找,函数定义,函数调用,let绑定,类型泛化,类型实例化的六条规则。形式化的定义可以参考:https://www.zybuluo.com/darwin-yuan/note/424795
我们可以用这六条规则进行类型推导和类型检查(https://www.zybuluo.com/darwin-yuan/note/424936)。
Hindley–Milner type system
DOT
面向对象:Scala的演进——从class/struct/protocol/interface到trait/case class/abstract class/object
组合优于继承。继承(子类型)是anti-pattern。随着程序员们越来越难以忍受子类型带来的耦合,现代编程语言也越来越无法忍受子类型和extends了。
因此在Go使用implements替代了extends,rust/scala使用trait替代了interface和子类型。
trait在很多编程语言中都出现过。trait是面向对象编程中使用的一个概念,它表示一组可用于扩展类功能的方法,可以用于多继承。trait一般有默认的实现因此和interface不同。但是在默写语言中可以通过drive自动实现interface。
C++中trait指的是通过模板特化进行类型萃取的方法。
class Foo {
public:
Type type = TYPE_1;
};
class Bar {
public:
Type type = TYPE_2;
};
template<typename T>
struct type_traits {
Type type = T::type;
}
case class 就是我们之前讲过的ADT类型。它能够用于模式匹配、自动实现了hashcode和equals方法,和getter方法。
abstract class和trait的不同在于:abstract class不能用于多继承,并且可以指定构造参数。
object在scala中意为单例类型
多态的形式
sub type
针对超类型元素进行操作的子程序、函数等程序元素
如果S是T的子类型,这种关系写作S<:T 意思是在任何需要使用 T 类型对象的环境中,都可以安全地使用 S 类型的对象
Ad-hoc
特设多态
多态函数有多个不同的实现,依赖于其实参而调用的相应版本的函数。函数重载乃至运算符重载也是特设多态的一种。
参数化多态
参数多态(有限多态)
也就是泛型编程,将不确定的类型作为参数使用,使得该定义对于各种具体类型都适用。
显示要求标注实现
又分为可批量标注和不可批量标注
不要求标注实现
duck type 鸭子类型
structural type system 结构类型 及所谓的类型由具体的结构定义而非由定义定义(类型的结构等价原则)
行多态
(在编程语言类型理论中,行多态性是一种多态性,它允许人们编写记录字段类型多态的程序(也称为行,因此称为行多态性))
提供默认实现
php 的 trait,oc 的 interface 属于 6
oc 的 protocol 属于 1, 4
go,typescript 的 interface 属于 1, 5
java c# 等的 interface 属于 1, 4, 6
js 的 protocol 提案 属于 1, 2, 4
c艹 的 concept 属于 2, 3, 5
Haskell 的 typeclass,c# 的 concept 提案 属于 2, 3, 4, 4.a
swift 的 protocol, rust 的 trait,scala 的 trait 属于 1, 2, 3, 4, 4.a, 6
类型擦除和@specialization注解
之前我翻译过openjdk的一篇文章:为类型擦除辩护
JVM的类型擦除指的是JVM运行时泛型类型信息将会被擦出到其上界的情况,也就是JVM语言实现的是同构的泛型。这会造成语言动态能力的减弱(比如会难以实现Vistor模式)、无法实现布局特化、静态检查能力的减弱(如下):
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //编译通过
list2.add(1); //编译通过
Object object = list2.get(0); //返回类型就是Object
选择同构泛型的原因在于:
- 运行时间成本
- 迁移兼容性(源码级兼容)
- 庞大的静态检查开销
因此,我们要获取JVM泛型中的具体类型,要使用匿名内部类hook的方式——比如TypeToken:https://zhuanlan.zhihu.com/p/151438084
在Scala中也是相同的,但是Scala可以使用@specialization注解来使用异构翻译的策略。
类型约束
复合类型
交类型
和类型
型变和bounds
extends和super(Java)[User-site variance]
Java在通配符中提供了extends和super来表示协变和逆变:
// Collections.java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
根据阿里巴巴Java开发手册(嵩山版)第12条,也就是
【强制】泛型通配符来接收返回的数据,此写法的泛型集合不能使用 add 方法, 而不能使用 get 方法,两者在接口调用赋值的场景中容易出错。
说明:扩展说一下 PECS(Producer Extends Consumer Super)原则:第一、频繁往外读取内容的,适合用 。第二、经常往里插入的,适合用
in和out(Kotlin和C#)[Declaration-site variance]
在kotlin和C#中,提供了in和out两个泛型修饰符表示这一点:
// Contravariant interface.
interface IContravariant<in A> { }
// Extending contravariant interface.
interface IExtContravariant<in A> : IContravariant<A> { }
// Implementing contravariant interface.
class Sample<A> : IContravariant<A> { }
class Program
{
static void Test()
{
IContravariant<Object> iobj = new Sample<Object>();
IContravariant<String> istr = new Sample<String>();
// You can assign iobj to istr because
// the IContravariant interface is contravariant.
istr = iobj;
}
}
Rust和Go的视角
Go中没有实现真正的泛型,所以没有型变的概念。
Rust中则离开声明周期没有所谓子类型的概念,因此也无法讨论型变。
upper/lower bounds、view bounds、context bounds、mutiple bounds和+ -(Scala)
在Scala中,提供了一组关于bounds的概念
self type
trait B
trait A { this: B => } // A requires a B
trait B
trait A extends B // A is a B
self type主要的作用是用来实现DI:如果A requires 了B的话,那么A就可以使用B中的任意方法。这种设计模式也称为Cake pattern。
Java在JDK8之后提供了interface的默认方法,因此也可以实现Cake Pattern:
interface Service<T extends Service<?>>
{
T getThis();
}
interface EmptyService<T extends EmptyService<?>> extends Service<T>
{
String empty();
}
interface ConcatService<T extends ConcatService<?>> extends Service<T>
{
String concat(String first, String second);
}
interface EmptyServiceImpl<T extends EmptyService<?>> extends EmptyService<T>
{
@Override
default String empty()
{
return "";
}
}
interface ConcatServiceImpl<T extends ConcatService<?>> extends ConcatService<T>
{
@Override
default String concat(String first, String second)
{
return first + " " + second;
}
}
public class Main implements EmptyServiceImpl<Main>, ConcatServiceImpl<Main>
{
@Override
public Main getThis()
{
return this;
}
public static void main(String[] args)
{
Main main = new Main();
System.out.printf("1. empty().length() == %d\n", main.empty().length());
System.out.printf("2. concat('Yanbing', 'Zhao') == '%s'\n", main.concat("Yanbing", "Zhao"));
}
}
依赖类型
路径依赖类型
依赖类型(Dependent Type)指的是依赖于值的类型。路径依赖类型是一种特定类型的依赖类型,其中依赖值是路径。Scala有一个依赖于值的类型的概念。这种依赖关系不是在type signature中表达的,而是在type placement中表达的。
PDT可以用于帮助我们编写绑定到父亲类型的类。下面就使用PDT实现了AdHoc多态的Database。这个Database的Key是String,Value则是PDT。
abstract class Key(val name: String) {
type ValueType
}
trait Operations {
def set(key: Key)(value: key.ValueType)(implicit enc: Encoder[key.ValueType]): Unit
def get(key: Key)(implicit decoder: Decoder[key.ValueType]): Option[key.ValueType]
}
case class Database() extends Operations {
private val db = mutable.Map.empty[String, Array[Byte]]
def set(k: Key)(v: k.ValueType)(implicit enc: Encoder[k.ValueType]): Unit =
db.update(k.name, enc.encode(v))
def get(
k: Key
)(implicit decoder: Decoder[k.ValueType]): Option[k.ValueType] = {
db.get(k.name).map(x => decoder.encode(x))
}
}
trait Encoder[T] {
def encode(t: T): Array[Byte]
}
object Encoder {
implicit val stringEncoder: Encoder[String] = new Encoder[String] {
override def encode(t: String): Array[Byte] = t.getBytes
}
implicit val doubleEncoder: Encoder[Double] = new Encoder[Double] {
override def encode(t: Double): Array[Byte] = {
val bytes = new Array[Byte](8)
ByteBuffer.wrap(bytes).putDouble(t)
bytes
}
}
}
trait Decoder[T] {
def encode(d: Array[Byte]): T
}
object Decoder {
implicit val stringDecoder: Decoder[String] = (d: Array[Byte]) =>
new String(d)
implicit val intDecoder: Decoder[Double] = (d: Array[Byte]) =>
ByteBuffer.wrap(d).getDouble
}
val db = Database()
import Database._
val k1 = key[String]("key1")
val k2 = key[Double]("key2")
db.set(k1)("One")
db.set(k2)(1.0)
assert(db.get(k1).contains("One"))
assert(db.get(k2).contains(1.0))
类型投影则是可以绕过PDT的方案。scala3中已经用DOT取代了这种方案。
class A {
class B;
def foo(b: A#B) // 接收所有的B类型实例,而不只是foo的调用者实例(a1)路径下B类型的对象
println(b)
}