Cool语言是一个面向对象的语言。虽然比较小型,但也包含了许多现代语言的特性,如对象、强类型和自动化内存管理。如果你之前熟悉c++、java等面向对象语言,那么会很容易上手。
学一门新语言,最急迫想知道的就是,如何编写“hello world”程序,下面就来尝个鲜。
class Main inherits IO {
main(): SELF_TYPE {
out_string("Hello, World.\n")
};
};
打眼一看,外形很像java,其实意思也很相近了。下面我来细细详谈。全文大部分翻译cool语言手册。
1,类结构 classes
一个Cool程序是由许多的类组成,每个类的定义必须在一个源文件里。但是多个类可以定义在一个源文件中。类的形式:
class <type> [ inherits <type> ] {
<feature_list>
};
<type>代表类名,[]里的内容是可选项。{}内是feature集合。所有类名都是全局可见的。类名是以大写字母开头的字符串。类不能重复定义(不能有两个类名字相同)。
“hello world”例子,就只有一个类,名字是Main。
2,features
feature也就是我们说的函数和属性的统称。一个类是由数个features组成。
2.1 函数 methods
函数结构如下:
<id> ( <id> : <type> , ... , <id> : <type>) : <type> { <expr> }
<id>是标示符,也就是名字。<type>是类型。<expr>是表达式。()里面是函数的形参定义。例如:
add ( a : Int, b : Int ) : Int {
a + b
}
add是函数名,a和b是参数名字,":"后跟着的是参数的类型。") :"后跟着的是返回类型。{}里面就是函数内容,也就是表达式组成。表达式后面会讲到。
类函数是外部可见的,也就是外部可以调用类的任何函数。
函数参数可以有1个或更多,也可以没有,并且参数的名字不能相同。函数的返回值必须明确给出。如果参数的名字和类属性名字相同,那么就会隐藏掉类属性。也就是说在这个函数里,参数和类属性名字相同,那么同名用的都是参数。
2.2 属性 attributes
属性结构如下:
<id> : <type> [ <- <expr> ];
<id>是属性名,<type>是属性类型。[]是可选的,属性的初始化。也就是把<expr>表达式的值赋给<id>。例如:
class A {
x : Int <- 1;
}
类A有一个属性为Int型的x,初始化为1。
初始化操作是在新建立一个类的对象的时候进行。在一个类内,属性初始化的顺序就是定义的顺序。类属性是局部变量,是类私有的。外部不能直接访问类的属性,只能通过函数去修改值。这也体现了现代OO语言的封装性。
所有的属性,如果没有指定初始化,那么默认初始为void。void类似于c语言里面的NULL,或者java里的null。不过在Cool语言里没有void的定义。唯一一个创建void值得方法就是在某个类里建立一个变量,除了Int,String,Bool类,并让这个void初始化,或者保存while循环的结果。有一个表达式isvoid expr,用来判断expr的值是不是void(会在之后表达式里讲到)。
基础类Int,Bool和String的属性定义和初始化是特殊的,会在后面基础类里讲到。
3,继承 inherits
继承的结构如下:
class C inherits P { ... }
类C是类P的子类,类P是类C的父类。
类C除了自己定义的features,还继承了所有父类P的所有features。如果子类父类定义了相同的函数,那么子类函数有更高的优先级。子类父类不定定义相同名字的属性。
有一个特殊的类Object(后面基础类会讲到)。如果一个类没有继承任何类,那么会默认的继承类Object。子类最多只能继承一个父类,这也叫做“单继承”。根据类的父子关系,可以定义一个集成体系图。这个图不能包含环。例如:类C继承类P,类P就不能继承类C。如果类C继承类P,那么类P必须在程序中有定义。因为是单继承,所以形成的图是一个树形结构,根节点就是类Object。
新对象建立,所有的基类的属性也都要初始化操作,基类优先,逐步向外。
为了确保类型安全,继承的函数有一些限制。规则很简单:如果子类C从父类P继承了函数f,那么类C必须覆盖类P中f的定义,参数和返回类型必须保持一致。
class P {
f(): Int { 1 };
};
class C inherits P {
f(): String { "1" };
};
如果p是动态类型类P的对象,那么p.f() + 1的值就是2。但是如果p赋值为类C的对象,那么结果为string + Int,也就会出现问题。也就是如果子类随意的覆盖父类的函数,那么就不是对父类函数行为的扩展,继承也就没有作用。
4,类型 types
在Cool语言里,每一个类名也是一个类型。SELF_TYPE类型用在比较特殊的场合。
类型声明通过x : C的方式。x是变量,C是类型。在使用一个变量前,这个变量必须声明过。
在Cool里有一个基本规则。如果类C继承类P,那么用到P的值地方,也可以用C的值代替。
如果类C的值可以代替类P的值,那么我们说C从属于P,或者表示为C《 P(可以认为类C是继承树的位置更低)。就像上面所说的那样,从属关系根据的是继承树。
定义,A,C和P都是类型:
A 《 A
如果 C继承P,那么C《 P
如果A《 C,并且C 《 P,那么A 《 P
因为Object是继承树的根节点,所有对于所有类型A,都有A 《 Object
4.1 SELF_TYPE
SELF_TYPE就是self变量的类型。在继承的时候很好用,尤其是当避免写类的具体类型。举个例子:
class Silly {
copy() : SELF_TYPE { self };
};
class Sally inherits Silly { };
class Main {
x : Sally <- (new Sally).copy();
main() : Sally { x };
}; Silly的copy函数返回值的SELF_TYPE,那就意味着返回的是类调用者的类型。所以子类Sally调用copy函数,返回的也是Sally,而不是Silly。
我们可以注意到,SELF_TYPE类型并不是固定的,是根据相关类哪里出现。
还有一个用法,就是: new SELF_TYPE,可以作为函数的返回类型,或者let变量的类型声明,或者属性的声明。除此之外,其它的用法是错误的。
4.2 类型检查 Type Checking
Cool的类型检查系统保证在编译期通过的程序,不会在运行时候出现类型错误。程序员需要提供变量的类型,然后类型检查器就会据此在程序用到的地方来推断类型。
在编译期,类型检查器给变量定义类型,我们叫它表达式的静态类型;在运行期一个表达式的计算的类型值,称为动态类型。
有这种区别是因为,在编译器,类型检查器没有在运行期时的动态数据,所以一般区别这两种类型。然而我们关心的是静态类型,可以保证程序运行时正确性。
5,表达式 expressions
5.1 常量 Constants
最简单的表达式就是常量。布尔常量是true或false。整型常量是无符号的字符串,如:0,123,007. 字符串常量是被两个双引号包围的字符序列,例如:"this is a string." 字符串之多只能有1024个字符长度。
这三个常量分别对应三个基础类:Bool,Int,String。
5.2 标示符 Identifiers
局部变量的名字,函数的参数名字,self,和类的属性都是标示符。self标示符可以被调用,但是不能给self赋值,或者在let、case或者函数参数里绑定它。也不允许把类属性命名为self。
局部变量和函数参数都有词法控件约束。类属性的作用域在那个类或继承的类里,尽管它可能被同名局部变量锁隐藏。绑定的标示符作用在声明的最内层空间。一个例外就是self,永远表示的每个类。
5.3 分配 Assignment
形式结构如下:
<id> <- <expr>
expr表达式的静态类型必须从属于id的声明类型。分配表达式的值就是expr的值,分配表达式的静态类型就是expr的静态类型。
5.4 函数调用 Dispatch
在Cool里,有三种形式的函数调用。之间的区别取决于如何选择调用函数。最常见的形式是:
<expr>.<id>(<expr>, ... ,<expr>)
考虑一个一般的形式 e0.f(e1, ... ,en). 要计算整个表达式的值,先要计算参数的值。先从e1开始,从左到右直到en计算出值。下一步计算e0的值,查看e0属于哪个类C(e0值如果是void,那么就会产生一个运行时错误)。最后,调用类C的函数f,并且e0的值绑定到函数f的self变量,实参绑定到形参里。Dispatch表达式的值,就是函数的返回值。
函数调用的类型检查包括很多步。假设e0的静态类型是A。那么A必须有一个函数f,这个f的参数和调用的参数数量必须相同,并且每个参数对应的静态类型也必须符合。
如果函数返回类型B,并且B是一个类名。那么Dispatch表达式的静态类型就是B。相反,如果函数f返回类型为SELF_TYPE,那么Dispatch的静态类型就是A。因为函数f里的self变量绑定的是类A,返回SELF_TYPE类型,所以我们推断返回类型的结果就是类A。
其它的两种调用形式:
<id>(<expr>, ... ,<expr>)
<expr>@<type>.id(<expr>, ... ,<expr>)
前一种形式是 self.<id>(<expr>, ... ,<expr>)的简写形式。
后一种形式提供了一个方法,用来调用父类的函数。如果子类覆盖了父类的函数,那么子类里只能调用子类定义的函数,父类函数因为隐藏掉了。这种形式指定了类型,所以就可以调用继承关系任意类的函数。例如:e @B.f()调用类B的函数f。但是e的类型必须从属于类B,也就是说必须是类B或B的子类。
5.5 条件 Conditionals
条件的形式如下:
if <expr> then <expr> else <expr> fi
一个标准的条件语法。先判断第一个expr的值,如果是true,那么就计算then后expr的值;如果是false,那么就计算else后expr的值,fi表示条件语句结束。条件语句的值,就是所属分支的值。
第一个expr的静态类型必须是类型Bool,各分支没有限制。至于条件表达式的类型,我们先定义一个符号“U",让A,B,D为出了SELF_TYPE的任意一个类型。那么有:
A U B = C (C类型是A和B共同从属的最小类型,也就是A、B相同的最小父类,小的意思是子类比父类小)
A U A = A
A U B = B U A
SELF_TYPE U A = D U A (假如SELF_TYPE指定类型D)
T代表为真的表达式的静态类型,F代表为假的表达式的静态类型。那么整个条件表达式的静态类型为T U F。
5.6 循环 Loops
循环的形式如下:
while <expr> loop <expr> pool
先计算第一个expr的值,如果类型是true,那么计算第二个expr值,然后回到第一个expr计算;如果false,那么整个循环表达式返回void。第一个expr的静态类型必须是Bool。循环表达式的静态类型是类Object。
5.7 代码块 Blocks
代码块的形式如下:
{ <expr>; .. <expr>;}
表达式的计算顺序是从左到右。每一个代码块必须至少有一个表达式;代码块的值就是最后一个表达式的值。代码块的静态类型是最后一个表达式的静态类型。
在Cool里,很容易把";”号用错。分号是表达式的终结符,不是表达式的分隔符。详细例子以后遇见了会介绍。
5.8 Let 表达式
let表达式形式如下:
let <id1> : <type1> [ <- <expr1> ], ... , <idn> : <typen> [ <- <exprn> ] in <expr>
初始化的表达式如<expr1>是可选择的。如果有,先计算<expr1>的值,绑定到<id1>;然后在计算<expr2>,绑定到<id2>。如此顺序直到<idn>。接下来进行let的主体,in后面的<expr>的计算。let的值也就是主体表达式的值。
<id1>到<idn>只有在let的主体里才是可见。并且当m > k对于<idm>,<idk>是可见的。
如果let定义的标示符名字重复,那么前面定义的会被隐藏掉。每个let必须至少声明一个标示符。
每个初始化表达式的类型必须从属于标示符声明的类型。主体表达式的类型就是let的类型。
5.9 分支 Case
case形式如下:
case <expr0> of
<id1> : <type1> => <expr1>;
. . .
<idn> : <typen> => <exprn>;
esac
case提供了运行时的检测。首先,计算<expr0>的动态类型为C(如果类型为void,那么会报运行时错误)。下一步,找出符合条件的分支。当C 《 <typek>,那么就选择这个类型的分支。<idk>绑定<expr0>的值,然后计算<exprk>。如果没有一个分支符合,那么产生一个运行时错误。每个case必须至少包含一个分支。
设Ti为<expri>的静态类型,那么T1 U T2 U ... U Tn就是case的静态类型。分支里声明的<idk>,作用域在<exprk>里,并且隐藏了外部同名的标示符。
case没有特殊的结构代表“default”或“otherwise”的分支。有一个相同的效果是包含分支:
x : Object => ...
因为每个类型都有 《 Object。
case提供程序员一个进行运行时类型检测的方法,之前的都是类型检测器推断的保守的静态类型。一个典型的应用场合:程序员写一个表达式e,并检测e是否有静态类型P。然而事实上这个程序员可能知道e的动态类型总是C(C 《 P)。如下写法可以捕获这个信息:
case e of x : C => ...
在这个分支里,x绑定了更具体的静态类型C的e表达式的值。
5.10 New 表达式
new的形式如下:
new <type>
根据<type>申请一个新的类。如果类型是SELF_TYPE,那么会根据当前空间self绑定的对象类型来建立。new的静态类型为<type>。
5.11 Isvoid
isvoid的形式如下:
isvoid expr
如果expr值为true,那么isvoid为true;如果expr值为not void,那么isvoid为false。
6,基础类 basic classes
Object
Obejct类是继承树的根节点类。定义了以下函数:
abort() : Object
type_name() : String
copy() : SELF_TYPE
abort()函数会终止程序,并携带错误信息。type_name函数返回类的名字。copy函数是对自身的一个浅拷贝(浅拷贝就是拷贝自身,不进行指针指向的内存拷贝)。
IO
提供了一些简单的输入输出操作函数:
out_stirng( x : String ) : SELF_TYPE
out_int( x : Int) : SELF_TYPE
in_string() : String
in_int() : Int
out_string和out_int函数打印参数并返回self。in_string函数从标准输入读入一个字符串,直到有一个换行符。in_int函数读入一个整型,直到一个空格。任何在整型之后的字符,直到并包括下个换行符,那么这个整型会被丢掉。
一个类可以继承IO来用IO的函数。不能重新定义IO类。
Int
Int类是提供整型。这个类没有函数。默认初始化时0(不是void)。不能继承或重定义Int。
String
String类提供字符串。定义了以下函数:
length() : Int
concat( s : String) : String
substr( i : Int, l : Int) : String
length函数返回self参数的长度。concat函数连接self和s字符串。substr函数返回self参数从位置i到l的字串(从0 开始计算)。如果超界了,会产生一个运行时错误。
默认的初始化为""(不是void)。不能继承或重定义String。
Bool
Bool类提供true和false。默认的初始化是false(不是void)。不能继承或重定义Bool。
7,Main Class
每个程序必须有一个类Main,并且Main里必须有一个没有参数的函数main。main函数必须在Main里定义(不能通过继承其它类)。程序通过(new Main).main()来执行。
8,注释 Comments
Cool里有两种注释。
一种是以"--"开头,直到行尾都被注释掉。
一种注释以"(*"和"*)"包围着。
—— Pinkman