好久没有更新博客了,那是因为最近博主成加班狗了 MDZZ(微笑)
从 GitHub 上看到一门国产编程语言 Lite,是国人自主开发的编程语言,先不说语言本身的好坏,但是有这个想法,能让中国人有自己的编程语言(易语言不算?),那也是很值得鼓励的!!!
Lite 教程 GitHub 地址:https://github.com/kulics/lite/blob/master/book-zh/document.md
Lite 编程语言
Lite 是一个专注于工程化的开源跨平台编程语言。
这门语言的设计目标是简单性、可读性、可理解性。
通过去除关键字,减少语法特性,统一表达规范,这门语言可以有效降低读写负担,让使用者能够把真正的注意力放在解决问题上。
特性
- 精心设计的语法,易于编写和阅读。
- 规则明确并且统一,符合直觉。
- 目前支持输出到 C#/Go/TypeScript,借助它们的资源,我们已经可以在非常广泛的场景下使用这门语言。
- 未来将会支持输出到LLVM,以支持更全面的场景。
目录
安装与使用
目前 Lite
支持编译到 C#/Go/TypeScript/Kotlin
,因此需要在系统中安装 .NET Core/Go/NodeJS/JDK
环境。
执行编译器就会扫描当前文件夹的 .lite
文件,并且自动转译为同名的目标文件。
Lite
需要使用部分语言库的功能,所以请自行引用编译器对应的库。
下载:
- C# https://github.com/kulics/lite-csharp/releases
- Go https://github.com/kulics/lite-go/releases
- TypeScript https://github.com/kulics/lite-typescript/releases
- Kotlin https://github.com/kulics/lite-kotlin/releases
基础语法
基本语句
在这门语言内,任何表达都必须归属于语句。
语句的基本形式为:
语句内容;
在本语言中,语法规则都是明确的,每一条语句都有明确的范围,必须由 ;
或 换行
结束。
因此在大多数情况下,我们都可以直接使用换行来结束。只有特殊需求的时候,可以选择使用 ;
来维持当前行。
所以我们更愿意这样写:
语句内容 # 自动结束 #
语句内容
导出命名空间
本语言内所有的内容都只能定义在命名空间中,这样可以有效地将内容区分成明确的区块来管理,你可以在独立的命名空间中自由定义而不必过多受命名重复限制。
我们可以使用 "id" {}
语句来定义当前文件的命名空间。
例如:
"Demo" {}
这个语句的意思是将当前代码文件内的内容标记命名空间为 Demo
,这样里面的内容命名就被限定在区域内,不必考虑和区域外的命名冲突。
同时外部区域可以导入 Demo
来使用其中的内容,我们接下来就会了解到如何导入。
导入命名空间
我们可以在导出语句的 {}
中,使用 "id"
语句来导入其它的命名空间、库、框架到某个命名空间中。
例如:
"Demo" {
"System"
}
这样就在 Demo
命名空间中导入了 System
库,然后就可以在程序中使用他们。
你可以编写多个导入语句,他们的顺序不影响导入功能。
主入口
我们需要定义一个主入口来让程序知道从哪里启动。主入口通过一个函数 Main(->) {}
声明。
根据目标平台的不同,主入口的声明方式可能不同,这里默认使用 C# 的主函数。
例如:
"Demo" {
"System"
}
Main(->) {
}
这里的 主入口 函数是一个无参数无返回值的函数,它会被自动识别为主入口,程序启动时即会执行 主入口 函数,因此我们只需将功能写在 主入口 函数中即可。
在以后的示例中,我们默认都在 主入口 函数中执行,因此不再过多展示这部分代码。
特别的,一个命名空间内只能存在一个 主入口 函数,因为入口必须唯一。
关于函数的更多细节将在之后的章节中说明。
显示信息
我们使用程序都是为了获取到一些实际有用的信息,所以我们需要有显示信息给我们浏览的功能,这个功能可以是显示、打印或输出。
如果我们编写的是控制台程序,我们可以使用 print()
函数,它可以将数据或文本信息显示到控制台供我们浏览。
例如:
print("Hello world") # 输出 Hello world #
在往后的例子中,我们都会使用控制台作为演示环境。
注释
注释只用来向使用者提供额外的信息,并不会被真正编译到可执行的程序中。
注释只需要使用 #
包裹住内容:
#
多行
注释
#
定义
我们只需使用 id type
语句就可以创建一个新的变量。
例如:
A int
这会将左边的名称创建一个标识符,并且定义为右边的类型,此时这个标志符是一个空值。
一旦标识符被创建之后,它的数据类型在有效区域内就不会再被改变。
赋值
和常规的编程语言一样,我们需要用 id = value
语句就可以将右边的数据赋值给左边的标识符。
例如:
A = 2
但是和定义不一样,赋值的左边只能是已经被定义过的标识符,否则赋值语句不成立。
自动推导
在大部分情况下,我们可以直接用赋值语句来创建一个新变量,我们无需明确指定数据的类型,编译器会自动为数据推断类型。
例如:
B = 10
这样就定义了新变量B
,它等于10
,并且被自动推导为int
类型。
如果我们不希望自动推导,也可以使用连写语句定义,标记自己需要的类型。
例如:
B int = 10
常量
常量是语言在编译时确定的,不可更改的,只支持基础类型的一种特殊类型,使用id type : value
定义,type
通常可以省略。
例如:
I : 2 # 自动推导 #
J int : 3 # 不使用自动推导 #
标识符
识符就是给变量、函数、结构体、接口等指定的名字。构成标识符的字母均有一定的规范,这门语言中标识符的命名规则如下:
- 区分大小写,Myname与myname是两个不同的标识符;
- 标识符首字符可以以下划线
_
或者字母开始,但不能是数字; - 标识符中其他字符可以是下划线
_
、字母或数字。 - 在同一个
{}
内,不能重复定义相同名称的标识符。 - 在不同
{}
内,可以定义重名的标识符,语言会优先选择当前范围内定义的标识符。 - 在命名空间、结构体和接口中,以下划线
_
开头的属性和方法名会被视为私有,其余会被视为公开。
关键字
无。
是的,你没有看错,我们的确是没有关键字的。所以你可以以任意字符作为你的标识符,无需考虑冲突问题。
基础类型
我们只需要一些简单的基础类型,就可以开展大部分工作。
Integer 整数
由于我们目前的计算机结构比较擅长计算整数,因此一个独立的整数类型有助于提升程序的运行效率。
在本语言中,默认的整数为 int
类型,它是一个 32 位有符号整数类型数据,是 i32
类型的别名,两者等价。
例如:
Integer int = 3987349
如果我们需要其它数值范围的整数,也可以使用其它类型,所有支持的整数类型如下表。
i8 # 8位有符号 -128 到 127 #
u8,byte # 8位无符号 0 到 255 #
i16 # 16位有符号 -32,768 到 32,767 #
u16 # 16位无符号 0 到 65,535 #
i32,int # 32位有符号 -2,147,483,648 到 2,647 #
u32 # 32位无符号 0 到 4,294,967,295 #
i64 # 64位有符号 -9,223,372,036,854,775,808 到 9,807 #
u64 # 64位无符号 0 到 18,446,744,073,709,551,615 #
基础类型转换
既然默认整数都是int
,我们怎么使用其它类型的整数呢?
我们可以用基础类型转换来将数字更改为我们需要的类型,只需要使用 to_type()
方法即可。
例如:
Integer8 = (16).to_i8()
需要注意的是,基础类型转换方法只有基础类型拥有,
如果需要所有类型的强制转换,请使用 to[type]()
方法,此方法对不兼容的类型会造成崩溃,请谨慎使用。
Float 浮点数
整数不能满足我们对数字的需求,我们很多时候还需要处理小数。
在本语言中,默认的小数为 num
类型,它是一个 64 位双精度浮点型数据,是 f64
类型的别名,两者等价。
例如:
Float1 num = 855.544
Float2 num = 0.3141592653
需要注意的是,由于计算机计算浮点数的特殊性,浮点数运算存在一定的精度问题,所以对精度敏感的需求应该考虑特殊处理。
所有支持的浮点类型如下表:
f32 # 32位 ±1.5e−45 到 ±3.4e38 #
f64,num # 64位 ±5.0e−324 到 ±1.7e308 #
Character 字符
计算机通常使用特定数字对字符进行编码显示,因此需要一种类型来表达字符,这个就是 chr
类型。
它只能是单个字符,只代表了某一个字符与数字的对应关系,所以即是字符,也是数字。
你只需要使用 ''
包裹一个字符,它就会被识别为字符值。
例如:
Char chr = 'x'
Char2 chr = '8'
String 字符串
我们在并不是生活在一个只有数字的世界,所以我们也非常需要使用文字来显示我们需要的信息。
在本语言中,默认的文字为 str
类型,它是一个不限长度的字符串数据。
你只需要使用 ""
包裹一段文字内容,它就会被识别为字符串值。
例如:
String str = "Hello World!"
需要注意的是,字符串是由多个字符组成的类型,所以实际上字符串是一个固定顺序的列表,两者存在对应关系。很多时候我们可以像使用列表那样对字符串进行处理。
字符串模版
很多时候我们需要将其它内容插入到字符串中,平常我们会如何做呢?
例如:
Title = "Year:"
Content = 2018
String = "Hello World! " + Title + Content.to_str()
# Hello World! Year:2018 #
这样做当然不影响功能,但是我们可以使用更直观方便的方式,那就是字符串模版。
我们可以在两段字符串中间直接插入元素,然后语言会自动合并为一段字符串。
例如:
String = "Hello World! " Title "" Content ""
# Hello World! Year:2018 #
Boolean 布尔
布尔指逻辑上的值,因为它们只能是真或者假。它经常用以辅助判断逻辑。
在本语言中,默认的布尔为 bool
类型,它是一个只有真值和假值的类型。
例如:
Boolean1 bool = true # 真 #
Boolean2 bool = false # 假 #
任意类型
特别的,有时候会需要一个可以是 任意对象 的 类型 来辅助完成功能,它就是 any
。
例如:
A any = 1 # 任意类型 #
nil 空
我们需要一个可以是 任意类型空值 的 值 ,所以它就是 nil
。
例如:
A = nil # 空值 #
操作符
操作符是一种告诉编译器执行特定的数学或逻辑操作的符号。
我们可以简单地理解成数学中的计算符号,但是编程语言有它不同的地方。
算术操作符
算数操作符主要被使用在数字类型的数据运算上,大部分声明符合数学中的预期。
例如:
A = 4
B = 2
print( A + B ) # + 加 #
print( A - B ) # - 减 #
print( A * B ) # * 乘 #
print( A / B ) # / 除 #
print( A % B ) # % 取余,意思是整除后剩下的余数,这里的结果为 2 #
print( A ** B ) # ** 幂 #
print( A // B ) # // 根 #
print( A %% B ) # %% 对数 #
除了数字之外,也有其它支持算术操作的类型,例如 str
可以使用加运算将两段文字合并起来。
例如:
A = "hello"
B = "world"
C = A + " " + B # C 为 "hello world" #
判断操作符
判断操作符主要被使用在判断语句中,用来计算两个数据的关系,结果符合预期的为true
,不符合的为false
。
例如:
A = 4
B = 2
print( A == B ) # == 等于 #
print( A >< B ) # >< 不等于 #
print( A > B ) # > 大于 #
print( A >= B ) # >= 大于或等于 #
print( A < B ) # < 小于 #
print( A <= B ) # <= 小于或等于 #
逻辑操作符
逻辑操作符主要也被使用在判断语句中,用来进行逻辑运算(与、或、非)。
例如:
A = true
B = false
print( A & B ) # & 与,两者同时为真才为真 #
print( A | B ) # | 或,两者其中一者为真就为真 #
print( ~A ) # ~ 非,布尔值取反 #
赋值操作符
赋值操作符主要用来将右边的数据赋值给左边的标识符,也可以附带一些快捷操作。
例如:
A = 0
A = 1 # = 最简单的赋值 #
A += 1 # += 先相加再赋值 #
A -= 1 # -= 先相减再赋值 #
A *= 1 # *= 先相乘再赋值 #
A /= 1 # /= 先相除再赋值 #
A %= 1 # %= 先取余再赋值 #
位操作
位操作作为底层计算的基础,在本语言中也支持。
例如:
A = 1
A && 1 # 按位与 #
A || 1 # 按位或 #
A ^^ 1 # 按位异或 #
~~A # 按位取反 #
A << 1 # 左移 #
A >> 1 # 右移 #
集合类型
当我们需要将很多相同类型的数据组合在一起的时候,我们就可以使用集合来完成这个任务。
我们内置的集合类型有列表和字典两种。
列表
列表使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个列表的不同位置中。
定义
我们只需要使用 { e1,e2,e3 }
语法将我们需要的数据括起来,并用 ,
分割每一个数据,就可以创建一个列表。
在大部分情况下,数据类型都可以由语言自动推断。
例如:
list = { 1,2,3,4,5 }
这样便会创建一个包含 1
到 5
的 int
类型列表。
列表类型的表示方法是 []type
。
例如我们需要一个字符串列表:
List = []str{} # 空 #
数组
如果我们需要使用原生数组类型,可以使用 [:]type
来表示。
也可以使用 array_of(e1,e3)
直接创建。
例如:
Array [:]int = array_of(1,5)
访问
如果我们需要访问列表中的其中一个元素,我们可以用 标识符.(索引)
来访问。
例如:
print( List.(1) )
需要注意的是,在编程语言里,大多数列表起始索引都是从 0
开始的,标识符.(0)
取得的才是第一个元素,往后的元素以此类推。
更改元素
如果我们需要更改列表中的其中一个元素,我们可以直接访问该元素,使用赋值语句来更改。
例如:
List.(0) = 5
需要注意的是,我们只能访问已经存在数据的索引,如果不存在,则会出现错误。
常用操作
List += 1 # 添加到末尾 #
List.insert(2,3) # 插入元素 3 到索引 2 #
List -= 1 # 删除指定位置元素 #
Length = List.len # 长度 #
字典
字典是用来存储无序的相同类型数据的集合,字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。
和列表中的数据项不同,字典中的数据项并没有具体顺序。我们需要通过标识符(键)访问数据,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。
字典的键只能使用 整数
和 字符串
类型。
定义
和列表类似,字典也使用 {}
定义,不同的是字典的类型是 key
和 value
的联合类型,形式是 key:value
。
例如:
Dictionary = {"a":1,"b":2,"c":3}
这样便会创建一个包含 a,b,c
三个条目 的 str:int
类型字典。
字典类型的表示方法是 []type:type
。
例如:
DictionaryNumNum = []int:int{} # 空 #
访问
和列表类似,我们也可以使用索引直接访问数据。
例如:
print( Dictionary.("a") )
更改元素
和列表类似,我们也可以使用赋值语句来更改元素。
例如:
Dictionary.("b") = 5
和列表不同的是,如果赋值的是不存在的索引,也不会错误,会直接将值赋予给新的键。
常用操作
Dictionary += {"d":11} # 添加元素 #
Dictionary -= "c" # 删除指定索引元素 #
Length = Dictionary.len # 长度 #
集合
集合用来存储相同类型、没有确定顺序、且不重复的值。
也就是说当元素顺序不重要时,或者希望确保每个元素只出现一次时,可以使用集合而不是列表。
定义
和列表类似,集合也使用 {}
定义,形式是 :value
。
例如:
Set = {:"a",:"b",:"c"}
这样便会创建一个包含 a,c
三个条目 的 str
类型集合。
集合类型的表示方法是 []:type
。
例如:
Numbers = []:int{} # 空 #
常用操作
Numbers.add(1) # 添加元素 #
Has = Numbers.contains(1) # 判断是否包含 #
Success = Numbers.remove(1) # 删除元素 #
Length = Numbers.len # 长度 #
判断
判断语句通过设定的一个或多个条件来执行程序,在条件为 true
时执行指定的语句,在条件为 false
时执行另外指定的语句。
我们只需要使用 ? value {}
就可以声明判断语句,根据后面的值进入对应的区域。
例如:
? true {
print("true") # 真 #
}
布尔判断
当判断值只会为 bool
类型时,语句只有当为 true
时才执行。
如果我们同时需要处理其它情况,可以在之后使用 value {}
来继续声明另一个处理语句。
如果只需要 false
的情况,使用 _ {}
来声明。
例如:
B = false
? B {
...... # 因为 B 为 false,所以永不会进入这个分支 #
} _ {
...... # 处理 false #
}
我们也可以在中间插入更多判断,语言会自动将它们实现为连续处理。
例如:
I = 3
? I == 0 {
......
} I == 1 {
......
} I == 2 {
......
}
相对于其它语言来说,这可以认为是 if elseif else
结构。
条件判断
如果我们需要对一个标志符进行判断,可以使用value ? case {}
语句,语句实现多条件匹配,搭配匹配条件来执行相应的逻辑,这样它就只会执行匹配成功的语句。
例如:
I ? 1 {
......
} 2 {
......
}
这种条件判断非常适合对某一个标识符的多条件判断,避免编写过多的判断条件。
是的,就像上面布尔判断一样,这里的每一个条件被执行完成后都会被结束,并不会继续向下执行。
如果有多个条件需要合并一起,可以使用 ,
分开。
例如:
I ? 1,3 {
......
} 4 {
......
}
缺省条件
如果需要一个缺省条件来执行逻辑怎么做?我们可以使用一个匿名标识符_
来完成这个目标。
例如:
I ? 1 {
......
} 2 {
......
} _ {
......
}
这样匹配不到的情况下,就会到缺省处理区域去执行。
相对于其它语言来说,这可以认为是 switch case default
结构。
模式匹配
条件判断还能做的更多,比如我们需要判断标志符的类型,
可以使用value ? id type{}
语法来匹配类型,id
可以放弃。
例如:
X ? _ int { # 是否 int #
print("int")
} content str { # 是否 str #
print(content)
} nil { # 是否为 nil #
print("nil")
}
获取类型
如果我们需要明确获取类型值,可以使用 typeof[type]()
函数来获取。
例如:
typeof[int]() # 通过类型直接获取类型值 #
循环
有的时候,我们可能需要多次执行同一块代码。
一般情况下,语句是按顺序执行的,函数中的第一个语句先执行,接着是第二个语句,以此类推。
集合循环
如果我们刚好有一个集合,可以是数组、字典、或是一段文本,那么我们就可以使用 id @ value {}
语句来遍历这个集合,取出的每一个元素为 id
。
例如:
Arr = {1,5}
item @ Arr {
print(item) # 打印每一个数字 #
}
如果我们需要同时取出索引与值,可以使用 index:value
语法替换 id
,这个方式对列表和字典都有效。
例如:
i:v @ Arr {
print(""i":"v"")
}
相对于其它语言来说,这可以认为是 foreach
结构。
迭代器循环
有些时候,我们未必刚好就有一个集合,但是我们又需要从 0
到 100
去取数。我们有一个迭代器语法可以完成这样的任务。
迭代器可以从起点向终点循环取数,我们使用集合的表达方式,在两个数之间使用 ..<=
符号隔开即可。
例如:
i @ 0 ..<= 100 {
print(i) # 打印每一个数字 #
}
需要注意的是,0 ..<= 100
的意义是从 0
逐次读取到 100
,也就是一共执行了 101
次。迭代器会执行到最后一个数字被执行完毕,而不是提前一次结束。
因此如果我们需要的是执行一百次,可以使用 0 ..< 99
或 1 ..<= 100-1
,切记这个区别。
迭代器默认每次间隔累加 1
,如果我们需要每隔一个数取一次,可以增加一个每步条件,只需要在起点和终点完成后再插入 :
和一个数字即可。
例如:
i @ 0..<=100 : 2 {
......
}
这样每次间隔就不是 1
而是 2
,同理我们可以设置其它数字。
我们也可以让它倒序遍历,只要使用 ..>=
即可。
例如:
i @ 100..>=0 {
...... # 从100到0 #
}
同理,如果不希望到达最后一位,可以使用100 > 0
。
相对于其它语言来说,这可以认为是 for
结构。
条件循环
如果我们需要一个只判断某个条件的循环怎么做呢?
加一个条件就可以了。
例如:
I = 0
@ I < 6 {
I += 1
}
相对于其它语言来说,这可以认为是 while
结构。
跳出
那么要如何跳出循环呢?我们可以使用 @..
语句来跳出。
例如:
@ {
@.. # 什么都没执行就跳出了 #
}
需要注意的是,如果跳出在多层嵌套的循环中,只会跳出最靠近自己的那一层循环。
继续
如果只需要跳出当前循环,使用 ..@
语句即可。
缺省条件
如果希望在循环不成立的时候执行另外一些逻辑,一样使用 _
声明即可。
@ 1 > 2 {
…
} _ {
# 循环不成立,于是执行此处的逻辑 #
}
函数类型
通常我们会将一系列需要重复使用的任务处理封装成为函数,方便在其它地方重复使用。
在实际工程实践中,给与确定的输入,必定会准确返回确定输出的函数被视为是较好的设计。所以建议尽可能地保持函数地独立性。
定义
之前我们已经见过了主入口函数,它只使用了固定语句 Main(->) {}
来定义。
例如:
Function(->) {
......
}
需要注意的是,函数可以在命名空间、结构体、接口中定义,也可以在函数内部中定义。在函数中定义函数时,内部函数不具有公有特性,只属于当前函数的私有函数。
调用
不像主入口函数不能被调用,常规函数都可以使用标识符来调用,我们只需要使用 id()
语句就可以使用封装好的函数。
例如:
Function() # 调用了 Function #
参数
虽然函数可以没有任何参数只执行特定的功能,但是更多时候我们需要的是可以接收某些输入数据、或是可以返回数据、或者两者都有的功能,而这需要参数出场来帮助我们完成任务。
非常简单的,我们只需要使用 id type
就可以声明参数。
例如:
Func(x int -> y int) {
<- x * 2
}
这个函数的意义是,接受输入的一个 int
参数 x
,返回一个 int
参数 y
。
左边是入参,右边是出参,括号内的参数没有数量限制,但是对顺序和类型有严格要求。
返回
到这里,即使不明说,大致你也可以猜到 <-
应该是一个与返回有关的语句了。
是的,我们只需要使用 <-
就可以指定一个明确的返回语句。
例如:
<- 1,"Hello"
这会返回 1,"Hello"
四个值。
如果不需要返回数据,可以省略数据。
例如:
<-
如果是一个不需要返回值的函数,语言会自动在函数末尾添加退出功能,因此我们可以选择性省略部分返回语句。
我们可以在函数内的任何一个地方使用返回语句提前终止函数,这样可以满足我们对逻辑控制的需求。
需要注意的是,与循环的跳出一样,返回语句只会中止距离自己最近的一层函数。
入参
我们把输入进函数的参数称之为入参,入参可以没有或多个,对类型和标识符没有限制。
当我们调用函数时,需要按照定义好的顺序,将数据按顺序填入括号内。顺序或类型不符合时,都会被视为错误使用。
例如:
# 定义一个包含两个入参的函数 #
Sell(price int,name str ->) {}
# 按照定义的要求,填入符合要求的数据 #
Sell(1.99,"cola")
出参
和入参类似,出参也需要在定义时明确带有标识符,这能让调用者更容易获取函数的作用信息。
例如:
TopSell(-> name str,count int) {
......
<- "cola",many
}
返回值的使用
很简单,就像我们做加减乘除运算一样去使用函数即可。
不同的是对于多返回值我们必须要像参数形式一样使用括号包裹每个标识符。
例如:
n,c = TopSell() # 将返回的两个值赋值给 n 和 c #
你可以使用定义或赋值语句去获得函数的返回值来使用,也可以将符合要求的函数嵌套到另一个函数里使用。
例如:
print( TopSell() ) # 打印两个数值 #
函数入参
如果我们希望函数的部分细节由外部定义,内部只执行其余的部分逻辑,比如对某个集合遍历处理一些功能,这时我们可以使用函数入参来完成这个目标。
函数入参没有特别的定义方式,只是将参数的类型替换为函数,不需要定义函数执行内容,并且省略了标识符。
例如:
Each_1_To_10(func (int->) ->) {
i @ 1..<=10 {
func(i)
}
}
我们定义了一个名为 func
的函数入参,它的类型是只有一个入参的函数。
这样我们就能将处理的细节交给外部传入的 func
定义了。
例如:
print(item int ->) {
print(item)
}
Each_1_To_10(print)
如此,我们就在 Each_1_To_10
内部的循环中执行了 print
函数。
Lambda 表达式
如上面那种方式先定义一个函数再传入使用有时候显得比较啰嗦,因为我们仅仅只是希望执行一小段功能而已,未必想定义一个函数提供给其它地方使用。
这时我们可以使用 Lambda 表达式 的语法来简化我们的代码。
因为函数入参在声明时就已经确定了,所以我们可以使用简化的语法 {id,id -> statements}
来表达,它的意思是定义入参标识符,执行函数语句。
例如:
ForEach( {it ->
print(it)
print(it * it)
print(it % 2)
})
Take( {a,b -> a + b} )
FindAll{ it -> it > 7 } # 如果函数只有一个参数,还可以省略括号 #
非常简单,和函数类型的表达的差异在于,只需要声明参数标识符和执行逻辑,类型与返回值都不需要声明。
Lambda 函数
与上面的简化写法不同,我们也可以直接写入一个完整的函数,就像我们定义函数一样。
例如:
Each_1_To_10( (item int ->) {
print(item)
})
结构体类型
如果我们只有那几种基础数据,实际上非常难以描述更具体的东西。
因此我们需要一种,能将不同属性的数据包装起来的功能,才能更好地描述我们需要的东西。
显而易见,这个负责包装数据的功能,就是结构体。
定义
我们可以使用 id -> {}
语句来定义一个什么都没有的结构体。
例如:
Package -> {
}
当然,我们更希望的是能包装几个数据,例如一个具有名称、学号、班级、年级属性的学生。
我们可以像定义普通标识符一样在结构体内去定义这些数据。
例如:
Student -> {
name str = ""
number str = ""
class int = 0
grade int = 0
}
这样我们就得到了具有这几个数据属性的学生结构体。这个结构体现在就像 int,str,bool
一样成为了一个可以使用的类型。
不像我们原始的基础类型只能存储一种数据,这个结构体可以同时存储名称、学号、班级、年级这些数据。
这非常像是我们现实中将不同的零件拼装在一起包装成一个整体的概念。
创建
那么我们怎么创建一个新的结构体呢?老样子,我们所有的类型都可以使用构造函数 type{}
来创建。
例如:
Peter = Student{}
这样便创建了一个 Peter
标识符,这个学生的所有属性都根据定义中设置的那样被初始化为 "","",0
。
让我们回顾一下,我们的基础类型、集合类型都可以使用构造函数来创建,实际上它们都是结构体.
使用属性
现在我们已经有了一个 Peter
,我们要怎么使用里面的属性呢?
很简单,我们只需要使用 .
语法,就能召唤出我们需要的属性。
例如:
print( Peter.name ) # 打印了某个学生的名字 #
要更改属性的值也是一样的,它就相当于是个嵌套的标识符。我们可以直接用赋值语句去更改值。
例如:
Peter.name = "peter"
Peter.number = "060233"
Peter.class = 2
Peter.grade = 6
构建赋值
像上面那样创建一个新的结构体,再逐个装填数据非常麻烦,我们可以使用简化语法来配置。
例如:
Peter = Student{
name = "peter"
number = "060233"
class = 2
grade = 6
}
同样的,集合的创建方式其实就是一种构建语法,所以我们也可以这样创建数组和字典。
例如:
Array = []int{ 1,5 }
Dictionary = []str:int{ "1":1,"2":2,"3":3 }
匿名结构体
如果我们只想直接包裹某些数据使用,而不是先定义结构体再使用,像匿名函数那样可以吗?
当然可以的,我们直接使用 {}
包裹就可以了。
例如:
Peter = {
name = "peter"
number = "060233"
class = 2
grade = 6
}
这样就直接创建了一个 Peter
数据,我们可以直接使用这些数据,但是不可更改这些数据。
由于匿名结构体并不是一个具有明确类型的结构体,所以我们只建议在一些临时场合使用,例如LINQ。
私有属性
任何人都会有些小秘密, Peter
也一样,也许他藏了一个秘密小女友的名字不想让其他人知道。
例如:
Student -> {
......
_girlFriend str # 第一个字符是 _ 的标识符是私有的 #
}
没错,如果你还记得标识符的定义的话,这就是私有标识符的定义方式,私有标识符是不能被外界访问的。
因此我们再定义一个 Peter
的话,也不能通过 Peter._girlFriend
来获取值或修改值。
那这种结构体的私有属性又不能访问,又不能修改,有什么用呢?别急,结构体还有另外一种属性。
结构体函数
如果我们需要让这个结构体自带函数,让它能方便操作,我们可以另外声明一个带结构体参数的函数。
结构体函数的声明方式与结构体类似,只是需要声明指定的标志符。形式为 id type -> {}
。
例如:
me Student -> {
getGirlFriend(->name str) {
<- me._girlFriend
}
}
这里的 me
用来声明结构体自身,这样可以方便地访问自身的属性。这可以认为是其它语言中的 this | self
,它只是一个参数,因此可以自由使用 me
以外的标识符。
通过函数属性,我们就能获取到私有的属性,也可以方便地根据业务需求去处理结构体中的其它数据。
例如:
print( Peter.getGirlFriend() )
# 打印了某个早恋学生的女朋友名字 #
与数据属性一样,函数也可以是私有标识符,使用私有标识符的函数也意味着只有结构体自己能访问。
组合
现在让我们发挥我们的想象力,我们想要一个专门给中国学生定制的结构体该怎么定义呢?
例如:
ChineseStudent -> {
name str = ""
number str = ""
class int = 0
grade int = 0
kungfu bool = false # 不会功夫的学生 #
}
不不不,这样重复定义数据就很不优雅了,我们可以将学生属性复用,加上一个额外的功夫属性就可以了。
我们需要用到组合这个特性,但是没有那么复杂,只是创建了一个学生属性而已。
例如:
ChineseStudent -> {
student = Student{} # 将学生属性包含其中 #
kungfu = false # 不会功夫 #
}
例如:
Chen = ChineseStudent{}
print( Chen.student.name )
# 当然,因为没有赋值,所以什么也没有输出 #
通过组合一层又一层的结构体,你可以自由拼装出任何一个你想要描述的事物。
兼容 .NET
继承
如果我们希望定义一个新的结构体,并且完全继承某个结构体的所有属性,可以使用继承语法,在定义里面声明不带标识符的属性。
如果希望改写原始结构体的属性,在结构体函数的结构体参数前面增加父标识符即可。
例如:
ChineseStudent -> {
Student # 继承 student #
kungfu = false
}
# 重写 #
parent me ChineseStudent -> {
getGirlFriend(-> name str) {
<- parent._girlFriend
}
}
构造
有些时候,我们可能使用 .NET 中的构造方法。
我们可以在使用特殊的构造函数语句 id type() {}
。
例如:
me Student(name str,number str) {
me.name = name
me.number = number
# 计算得出班级 #
me.class = getSubText(number,3)
# 计算得出年级 #
me.grade = getSubText(number,1)
}
这样就得到了一个带构造函数的结构体,我们在创建一个新学生的时候,就会自动产生班级和年级数据。
例如:
Peter = Student("peter","060233")
print(Peter.class) # 打印出 2 #
如果需要使用带继承的构造函数,可在参数语法后面追加 (params)
即可。
例如:
me Parent(a int) {
}
Child -> {
_ Parent
}
me Child(a int)(a) {
}
命名空间
命名空间的设计目的是提供一种让一组名称与其他名称分隔开的方式。在一个命名空间中声明的名称与另一个命名空间中声明的名称不冲突。
导出
为了方便我们管理代码,我们必须将我们的代码写在命名空间内,我们可以通过公有属性暴露给外部使用,也可以使用私有属性只完成自己的业务。
例如:
"Name/Space" {}
GetSomething(-> content str) {
<- "something"
}
导入
我们可以通过导入功能来使用其它命名空间内容,导入后可以直接调用命名空间内容。
例如:
"Run" {
"Name/Space"
}
Main(->) {
# 打印 something #
print( GetSomething() )
}
控制类型
控制类型是对数据操作封装的代码块。
通常我们会将一些数据控制处理封装成为控制类型,这样在使用数据时就无需执行额外的方法。
获取操作
如果我们想设定一个获取的操作,我们可以使用 id() type{ctrl}
定义。
例如:
Number() int {
get { # 表示获取,相当于其它语言中的getter #
<- 7 # 只返回 7 #
}
}
这样Number就具有了一个特殊的获取值方法,在调用Number时会执行内部的逻辑。
需要注意的是,这个控制数据只有一个获取方法,那么它就只支持获取操作,调用者无法给它赋值。
设置操作
有了以上的例子,我们很自然能够想到设置操作改如何处理。只需要再加一个参数标识即可。
例如:
Number() int {
set(value) { # 表示设置,相当于其它语言中的setter #
# ???该把值给谁??? #
}
}
是的,这里引出了一个问题,控制类型是用来控制操作的,实现操作的时候无法使用自身来存储数据。
因此我们需要使用另一个搭配的数据来使用控制类型。
例如:
_Number = 0
Number() int {
set(value) {
_Number = value # value代表输入的值 #
}
}
一个完整的读写例子如下:
_Number = 0
Number() int {
get {
<- _Number
}
set(v) {
_Number = v
}
}
这样做是不是太麻烦了?
是的,我们有更简单的方式,直接指明一个引用数据,控制类型会自动声明所有控制操作。
例如:
_Number = 0
Number(_Number) int # 等价于上面的封装 #
接口类型
我们在现实中常常用协议来规定一些特定的规则,让人或事物可以按照预期的规则来做事情。
我们在程序语言里也常常需要这么做,这个功能就是接口。
接口规定用来实现某一特定功能所必需的方法和属性,让结构体来遵守。
我们的结构体可以像签署接口一样引入我们需要的接口,然后声明接口要求的所有属性,这样我们就认为这个结构体实现了接口。
定义
我们只需要使用 id <- {}
语句就可以定义一个接口。
例如:
Protocol <- {
}
这就是一个空的接口。
接下来,让我们设计一个学生都需要完成的艰巨任务……作业。
例如:
Homework <- {
count() int
do(->)
}
这是一个作业接口,它有两个属性,一个是需要做作业的数量,一个是完成作业的函数。
定义的方式和结构体内定义属性的方式如出一辙。
与结构体不同的是,接口定义时属性不需要具体的数值或函数内容,只需要确定类型。
接下来,我们就要让学生来实现这个接口了。
实现接口
对于需要显式实现的接口,可以在结构体函数中指定接口,否则可以直接实现。
例如:
Student -> {
......
_count int
}
# 显式实现 #
me Student -> Homework {
count(me._count) int
do(->) {
SpendTime(1) # 花费了一个小时 #
me.count -= 1 # 完成了一个 #
}
}
我们的学生写作业真是非常艰苦的……
让我们来解释一下这段代码发生了什么:
- 我们实现了一个接口,现在
Student
也被认为是Homework
类型了,我们可以将一个Student
当作Homework
一样去使用。 - 在接口内我们包含了接口规定的两个属性
count,do
,根据规定,一个也不能少。 - 我们给接口的两个属性都分别编写了真实的值和函数,这样这两个属性就成为了
Student
的有效子属性之一。 - 我们在
do
里面做了一些事情,减少了作业的总量。
使用接口
包含了接口之后,我们就能使用拥有接口的学生了。
例如:
Peter = Student{ count=999999 }
print( Peter.count )
# 打印 999999,好多呀 #
Peter.do()
# 做了一次作业 #
print( Peter.count )
# 打印 999998,还是好多呀 #
如果只是这样使用,那和在结构体里直接定义这两个属性比就没什么优势了。
让我们来回想一下接口的作用,接口是让每个含了接口的结构体都拥有了规定的相同的属性和方法。
这样对于接口的制定者来说,就无需关心结构体是如何遵循接口的,只需要知道它们都遵循了,就能用同样的方法去使用它们。
现在我们可以创建各种各样的学生,它们都遵循了一样的接口,我们可以无差别使用接口里的功能。
例如:
# 创建了三个不同类型的学生结构体 #
StudentA = ChineseStudent{}
StudentB = AmericanStudent{}
StudentC = JapaneseStudent{}
# 让他们分别做作业 #
StudentA.do()
StudentB.do()
StudentC.do()
更有效率的做法是把这个功能写进函数,让函数来帮我们重复调用接口的功能。
例如:
DoHomework(Student Homework ->) {
student.do()
}
# 现在我们就可以更简单地让每个学生做作业了 #
DoHomework(StudentA)
DoHomework(StudentB)
DoHomework(StudentC)
当然,更好的做法是把这些学生都放进数组,这样我们就可以使用循环来处理这些重复的工作了。
例如:
Arr = []Homework{}
Arr.add( StudentA )
...... # 塞进很多很多学生 #
i @ Arr {
DoHomework(i)
}
╮( ̄▽ ̄)╭
完美
类型判断
因为结构体类型可以被转为接口类型使用,所以在使用过程中就不能确定数据的原始类型。
但有时候我们又需要获得数据的原始类型来处理,我们可以使用类型判断来帮助我们完成这个事情。
我们可以使用 is[type]()
来判断数据的类型,使用 value.[type]
来将数据转化为我们的类型。
例如:
Func(hw Homework ->) {
# 判断是否中国学生 #
? hw.is[ChineseStudent]() {
# 转换为中国学生数据 #
Cs = hw.[ChineseStudent]
}
}
枚举类型
枚举是一组具有独立名称整数常量。通常可以用来标记一些业务数据的类型,方便判断处理。
定义
我们只需要使用id -> type:{id id id id}
语句即可。
例如:
Color -> u8:{
Red
Green
Blue
}
枚举会按照顺序给标识符赋值,最终得到 Red=0,Green=1,Blue=2
这样的集合。
这样我们在使用时就无需关心它们的数值,放心标记我们需要处理的业务。
例如:
C = Random color() # 获取一个随机颜色 #
C ? Color.Red {
......
} Color.Green {
......
} Color.Blue {
......
}
需要注意的是,枚举只能在命名空间下定义。
指定值
如果有需要,我们也可以给单独一个标识符赋值,没指定的会依照上一个标识符顺序继续累加1。
例如:
Number -> u8:{
A = 1 # 1 #
B # 2 #
C = 1 # 1 #
D # 2 #
}
检查
程序可能会出现各种各样的异常。
异常无法完全避免,但是我们可以选择一些手段帮助我们检查和报告异常。
报告异常
我们可以在函数中的任何地方,使用 throw()
函数来声明一个异常数据。
例如:
ReadFile(name str ->) {
? name.len == 0 {
throw( Exception("something wrong") )
}
......
}
这样我们就声明了一个异常,异常说明是 something wrong
,一旦外部调用者使用了不合法长度的 name
,这个函数就会被强制中止,将这个异常向上报告,交给调用者处理。
检查异常
我们可以使用 ! {}
语句来检查异常,使用 id type {}
来处理异常。type
可以省略,默认为Exception
。
例如:
! {
f = ReadFile("temp.txt")
} ex IOException {
throw(ex)
} e {
print(e.message)
}
当出现异常时,程序就会进入错误处理区块,e
为异常标识符,我们可以获取异常的信息,或者进行其它操作。
如果没有异常,则不会进入异常处理区块的逻辑。
一般情况下,我们可以在异常处理中进行提早返回或数据处理,如果有处理不了的异常,我们也可以继续向上报告。
例如:
! {
Func()
} ex {
# 可以手动中止 #
# <- #
throw(ex)
}
检查延迟
如果我们有一段功能希望无论程序正常或异常都能处理,例如关键资源的释放问题,我们可以使用检查延迟特性。
很简单,在检查的最后使用 _ {}
就能声明一段检查延迟的语句。
例如:
func(->) {
F File
! {
F = ReadFile("./somecode.lite")
} _ {
? F >< nil {
F.release()
}
}
......
}
这样我们就声明了 F.release()
这条释放文件的语句,这条语句不会被立刻执行,而是会等待检查结束后调用。
有了检查延迟,我们就可以无需关心如何退出,安全地处理某些任务。
需要注意的是,正因为检查延迟是函数退出前执行的,并且无论程序运行状态异常与否都会执行,所以检查延迟中不能使用返回语句。
例如:
......
_ {
F.release()
<- # 错误,不能使用返回语句 #
}
自动释放
对于实现了自动释放接口的接口,我们可以使用声明语法来定义变量,这样在检查执行完毕时就会自动释放。
例如:
! Res = FileResource("/test.lite")
......
异步处理
线程 被定义为程序的执行路径。每个线程都定义了一个独特的控制流。如果您的应用程序涉及到复杂的和耗时的操作,那么设置不同的线程执行路径往往是有益的,每个线程执行特定的工作。
由于计算机处理器是有计算瓶颈的,所以无法让所有的事情都按照单线顺序的方式逐个处理,为了提升处理容量,我们经常会需要使用异步并行的方式来解决计算问题。
.Net 平台拥有自己的线程库 System.Threading
,更多关于线程的使用可以查询相关接口。
这里我们谈一谈如何更简单地处理线程问题,也就是异步处理。
在其它语言里可以认为是异步编程终级解决方案的 async/await
。
异步声明
没错,真的是使用 ~>
就可以了。
例如:
Async(~> out int) {
<- 12
}
一旦一个方法被声明为异步方法,编译器会自动给返回值套上 Task[type]
类型包裹,这个方法就可以被异步执行了。
例如:
Result = Async() # result 是一个 Task 数据 #
接下来我们再看看如何让它异步等待执行。
异步等待
与声明一样,我们只需要使用 <~ function()
就可以声明执行一个异步方法。
例如:
Result = <~ Async()
......
声明异步等待后,程序执行到这里就会暂时停止后面的功能,直到 Async
函数执行完毕后,将 out
的值赋值给 Result
,再继续执行。
异步使用条件
异步等待只能在异步声明的函数里使用。
例如:
# 正确 #
Async(~> out int) {
<~ delay(5000) # 等待一段时间 #
<- 12
}
# 错误 #
Async(-> out int) {
<~ delay(5000) # 不能被声明 #
<- 12
}
空返回值
如果异步方法没有返回值,它也会同样返回一个 Task
数据,外部调用一样可以等待。
例如:
Async(~>) {
<~ delay(5000) # 等待一段时间 #
}
<~ Async() # 正确 #
Task = Async() # 正确,获取了 Task #
Lambda
对于lambda,我们也可以使用异步,同样使用 ~>
即可。
例如:
Arr.filter( {it ~> it > 5} )
泛型
在封装公共组件的时候,很多时候我们的结构体、函数、接口不需要关注调用者传递的实体是"什么",这个时候就可以使用泛型。
比如我们现在需要一个集合,可以支持增加、删除和读取,希望任何类型都可以使用,就可以封装一个泛型的结构体。
我们的列表和字典其实就是使用泛型实现的。
声明与使用
让我们来看看怎么使用泛型来实现一个列表,我们只需在标识符后面使用 [id]
符号来包裹类型的代称即可。
这是一个简化的实现。
例如:
List[T] -> {
items = Storage{T} # 创建存储 #
length = 0
}
me List[T] -> {
get(index int -> item T) { # 获取某个泛型数据 #
<- items.get( index )
}
add(item T ->) { # 将一个泛型数据添加进列表 #
items.insert(length,item)
length += 1
}
}
这样我们便定义一个支持泛型的结构体,T
就是泛型,实际上它可以是任何标识符,只是习惯性我们会使用 T
作为代称。
泛型括号内像参数一样支持多个代称,例如:[T,H,Q]
。
定义了泛型之后,在结构体的区域内,就会将 T
看作是真正的类型,之后我们可以像 int
一样在各种需要类型的地方使用它。
需要注意的是,因为泛型是在运行中确定类型的,所以编译器无法推断泛型的构造方法。我们只能用缺省值创建方法去构造泛型数据。
我们可以用缺省值创建方法 empty[type]()
来指定一个包含了类型的缺省值。
例如:
X = empty[int]()
Y = empty[protocol]()
Z = empty[(->)]()
这样我们就可以在泛型里使用。
例如:
Package[T] -> {
item = empty[T]() # 初始化了一个缺省值的泛型数据 #
}
那么我们如何使用泛型呢?
很简单,就和我们声明一样去使用即可,只不过调用时需传入真正的类型。
例如:
ListNumber = List[int]{} # 传入 integer 类型 #
这样我们便拥有了一个整数类型的列表,是不是很像这个:
ListNumber = []int{}
没错,其实我们的列表和字典语法都是语法糖。
支持的类型
我们可以在 结构体、函数、接口 类型中使用泛型。
例如:
Func[T](data T -> data T) {
<- data
}
Protocol[T] <- {
test[T](in T ->) {}
}
Implement -> {}
me Implement -> Protocol[Implement] {
test[Implement](in Implement ->) {
}
}
泛型约束
如果我们需要对泛型的类型进行约束,只需要使用T id
语法即可。
例如:
Package[T Student] -> {
}
注解
注解是用于在运行时传递程序中各种元素(比如结构体、函数、组件等)的特征信息的声明性标签。
通常我们在很多反射、数据解析的场景种会使用到注解特性。
注解声明
我们只需要使用 ()
包裹住标记内容即可。
如果注解项目有子属性,只需要使用 ()
包裹即可。如果需要注明指定属性,则如结构体的简化构建一样使用 id = data
赋值即可。
注意要在标识符前面使用才有效。
下面我们以数据库数据为参照看看要如何使用注解。
例如:
(Table("test"))
Annotation -> {
(Key,Column("id"))
id str
(Column("name"))
name str
(Column("data"))
data str
}
我们声明了一个 annotation
的结构体,它使用注解标记了表名 test
、主键 id
、字段 name
和字段 data
。
这样在处理数据库时,就可以被数据库接口解析为对应的名称来进行数据操作了。
我们在程序内部直接使用这个结构体,调用数据库功能时程序会自动映射为对应数据库数据。
这样极大节省了我们进行解析转换的工作。
LINQ
在关系型数据库系统中,数据被组织放入规范化很好的表中,并且通过简单且强大的 sql 语言来进行访问。因为数据在表中遵从某些严格的规则,所以 sql 可以和它们很好的配合使用。
然而,在程序中却与数据库相反,保存在类对象或结构中的数据差异很大。因此,没有通用的查询语言来从数据结构中获取数据。从对象获取数据的方法一直都是作为程序的一部分而设计的,然而使用 LINQ 可以很轻松地查询对象集合。
以下是 LINQ 的重要特性。
关于更多 LINQ 的细节说明可以到以下网址阅读。
声明
我们可以像C#一样使用Linq进行查询,只需要使用 ->
声明LINQ语句即可。
例如:
Linq(->) {
Numbers = { 0,1,5,6 }
Linq = from num -> in Numbers ->
where (num % 2) == 0 ->
orderby num -> descending ->
select num
}
可选类型
本语言中所有的类型默认都不可以为空值,这可以极大限度地避免空值问题。
如果定义了一个类型却并未赋值,那么它将不能被使用。
例如:
A int
B = A # 错误,并未给 A 赋值 #
声明与使用
如果某些情况必须要使用带空值地类型,可以使用可空类型。
只需要在任何类型前加入?
,即为可空类型。
例如:
A ?int
B = A # B 赋值为空的I32 #
一旦出现了可选类型,我们就需要严格处理空值,避免程序错误。
例如:
? A >< nil {
A.to_str()
}
这样做很繁琐,特别是我们需要执行连续多个函数的时候。
我们可以在表达式后面加上 ?
来使用它们,这样只有当他们不为空时才会执行。
例如:
Arr?.to_str()
合并操作
如果希望可选类型的值为空时选用另一个默认值,可以使用id.or_else(value)
函数。
例如:
B = A.or_else(128)
引用操作
如果我们需要在参数传递中使用可修改自身的特性,可以使用引用声明 !type
。
这样就可以在函数内部操作操作外部的变量,使用时也需要使用 value!
声明传递引用。
例如:
Swap(x !int,y !int ->) {
x,y = y,x
}
A = 1
B = 2
Swap(A!,B!)
print(A,B)
# A = 2,B = 1 #