基本
Warning: 本文档不完整。它未经校对(样式、正确链接、误译等)。此外,Erg 的语法可能在版本 0.* 期间发生破坏性更改,并且文档可能没有相应更新。请事先了解这一点 如果您在本文档中发现任何错误,请报告至 此处的表单 或 GitHub repo。我们将不胜感激您的建议
本文档描述 Erg 的基本语法 如果您已经有使用 Python 等语言的经验,请参阅 快速浏览 了解概览 还有一个单独的 标准 API 和 Erg 贡献者的内部文档。如果您需要语法或 Erg 本身的详细说明, 请参阅那些文档
你好,世界!
首先,让我们做"Hello World"
print!("Hello, World!")
这与 Python 和同一家族中的其他语言几乎相同。最显着的Trait是!
,后面会解释它的含义
在 Erg 中,括号 ()
可以省略,除非在解释上有一些混淆
括号的省略与 Ruby 类似,但不能省略可以以多种方式解释的括号
print! "Hello, World!" # OK
print! "Hello,", "World!" # OK
print!() # OK
print! # OK, 但这并不意味着调用,只是将 `print!` 作为可调用对象
print! f x # OK, 解释为 `print!(f(x))`
print!(f(x, y)) # OK
print! f(x, y) # OK
print! f(x, g y) # OK
print! f x, y # NG, 可以理解为 `print!(f(x), y)` 或 `print!(f(x, y))`
print!(f x, y) # NG, 可以表示"print!(f(x),y)"或"print!(f(x,y))"
print! f(x, g y, z) # NG, 可以表示"print!(x,g(y),z)"或"print!(x,g(y,z))"
脚本
Erg 代码称为脚本。脚本可以以文件格式 (.er) 保存和执行
REPL/文件执行
要启动 REPL,只需键入:
> erg
>
mark is a prompt, just type erg
.
Then the REPL should start.
> erg
Starting the REPL server...
Connecting to the REPL server...
Erg interpreter 0.2.4 (tags/?:, 2022/08/17 0:55:12.95) on x86_64/windows
>>>
Or you can compile from a file.
> 'print! "hello, world!"' >> hello.er
> erg hello.er
hello, world!
注释
#
之后的代码作为注释被忽略。使用它来解释代码的意图或暂时禁用代码
# Comment
# `#` and after are ignored until a new line is inserted
#[
Multi-line comment
Treated as a comment all the way up to the corresponding `]#`
]#
文档注释
'''...'''
是一个文档注释。注意,与Python不同,它是在任何类或函数之外定义的。
'''
PI is a constant that is the ratio of the circumference of a circle to its diameter.
'''
PI = 3.141592653589793
'''
This function returns twice the given number.
'''
twice x = x * 2
print! twice.__doc__
# This function returns twice the given number.
'''
Documentation comments for the entire class
'''
C = Class {x = Int}
'''
Method documentation comments
'''
.method self = ...
您可以通过在'''
之后立即写入语言代码来指定文档的语言。然后,Erg语言服务器将以Markdown格式显示每种语言版本的文档(默认语言为英语)。
参见这里获取多语言相关文档
'''
Answer to the Ultimate Question of Life, the Universe, and Everything.
cf. https://www.google.co.jp/search?q=answer+to+life+the+universe+and+everything
'''
'''japanese
生命、宇宙、そして全てについての究極の謎への答え
参照: https://www.google.co.jp/search?q=answer+to+life+the+universe+and+everything
'''
ANSWER = 42
表达式,分隔符
脚本是一系列表达式。表达式是可以计算或评估的东西,在 Erg 中几乎所有东西都是表达式
每个表达式由分隔符分隔 - 新行或分号 ;
-
Erg 脚本基本上是从左到右、从上到下进行评估的
n = 1 # 赋值表达式
f(1, 2) # 函数调用表达式
1 + 1 # 运算符调用表达式
f(1, 2); 1 + 1
如下所示,有一种称为 Instant block 的语法,它将块中评估的最后一个表达式作为变量的值
这与没有参数的函数不同,它不添加 ()
。请注意,即时块仅在运行中评估一次
i =
x = 1
x + 1
assert i == 2
这不能用分号 (;
) 完成
i = (x = 1; x + 1) # 语法错误: 不能在括号中使用 `;`
缩进
Erg 和 Python 一样,使用缩进来表示块。有三个运算符(特殊形式)触发块的开始: =
、->
和 =>
(此外,:
和 |
,虽然不是运算符,但也会产生缩进)。每个的含义将在后面描述
f x, y =
x + y
for! 0..9, i =>
print!
for! 0..9, i =>
print! i; print! i
ans = match x:
0 -> "zero"
_: 0..9 -> "1 dight"
_: 10..99 -> "2 dights"
_ -> "unknown"
如果一行太长,可以使用 \
将其断开
# 这不是表示 `x + y + z` 而是表示 `x; +y; +z`
X
+ y
+ z
# 这意味着`x + y + z`
x \
+ y \
+ z
字面量
基本字面量
整数字面量
0, -0, 1, -1, 2, -2, 3, -3, ...
比率文字
0.00, -0.0, 0.1, 400.104, ...
注意,Ratio类型不同于Float类型,虽然API相同,但计算结果的准确性和效率存在差异。
如果"比率"文字的整数或小数部分为0
,则可以省略0
assert 1.0 == 1.
assert 0.5 == .5
注意: 这个函数
assert
用于表明1.0
和1.
相等 后续文档可能会使用assert
来表示结果是相等的
字符串字面量
可以使用任何 Unicode 可表示的字符串
与 Python 不同,引号不能包含在 '
中。如果要在字符串中使用 "
,请使用 \"
"", "a", "abc", "111", "1# 3f2-3*8$", "こんにちは", "السَّلَامُ عَلَيْكُمْ", ...
\{..}
允许您在字符串中嵌入表达式。这称为字符串插值
如果要输出\{..}
本身,请使用\\{..}
assert "1 + 1 is 2" == "\{1} + \{1} is \{1+1}"
文档注释也被视为字符串字面量,因此可以使用字符串插值。 它在编译时展开。如果您嵌入的表达式在编译时无法确定,则会收到警告。
PI = 3.14159265358979323
'''
S(r) = 4 × \{PI} × r^2
'''
sphere_surface r = 4 * PI * r ** 2
指数字面量
这是学术计算中常用的表示指数符号的文字。它是"比率"类型的一个实例 该符号与 Python 中的符号相同
1e-34, 0.4e-10, 2.455+e5, 245e5, 25E5, ...
assert 1e-10 == 0.0000000001
复合字面量
这些文字中的每一个都有自己的文档分别描述它们,因此请参阅该文档以获取详细信息
数组字面量
[], [1], [1, 2, 3], ["1", "2",], [1, "1", True, [1]], ...
元组字面量
(), (1, 2, 3), (1, "hello", True), ...
字典字面量
{:}, {"one": 1}, {"one": 1, "two": 2}, {"1": 1, "2": 2}, {1: "1", 2: True, "three": [1]}, ...
Record 字面量
{=}, {one = 1}, {one = 1; two = 2}, {.name = "John"; .age = 12}, {.name = Str; .age = Nat}, ...
Set 字面量
{}, {1}, {1, 2, 3}, {"1", "2", "1"}, {1, "1", True, [1]} ...
与 Array
字面量不同的是,Set
中删除了重复元素
assert {1, 2, 1} == {1, 2}
看起来像文字但不是
布尔对象
True, False
None 对象
None
Range 对象
assert 0..5 == {1, 2, 3, 4, 5}
assert 0..10 in 5
assert 0..<10 notin 10
assert 0..9 == 0..<10
Float 对象
assert 0.0f64 == 0
assert 0.0f32 == 0.0f64
浮点对象是通过将 Ratio
对象乘以 f64
构造的,后者是 Float 64
单位对象
Complex 对象
1+2Im, 0.4-1.2Im, 0Im, Im
一个"复杂"对象只是一个虚数单位对象Im
的算术组合
*-less 乘法
在 Erg 中,您可以省略 *
来表示乘法,只要解释上没有混淆即可。但是,运算符的组合强度设置为强于 *
# same as `assert (1*m) / (1*s) == 1*(m/s)`
assert 1m / 1s == 1 (m/s)
变量和常量
变量
变量是一种代数; Erg 中的代数 - 如果没有混淆,有时简称为变量 - 指的是命名对象并使它们可从代码的其他地方引用的功能
变量定义如下
n
部分称为变量名(或标识符),=
是赋值运算符,1
部分是赋值
n = 1
以这种方式定义的"n"此后可以用作表示整数对象"1"的变量。该系统称为分配(或绑定)
我们刚刚说过1
是一个对象。稍后我们将讨论对象是什么,但现在我们假设它是可以赋值的,即在赋值运算符的右侧(=
等)
如果要指定变量的"类型",请执行以下操作。类型大致是一个对象所属的集合,后面会解释
这里我们指定n
是自然数(Nat
)类型
n: Nat = 1
请注意,与其他语言不同,不允许多次分配
# NG
l1 = l2 = [1, 2, 3] # 语法错误: 不允许多重赋值
# OK
l1 = [1, 2, 3]
l2 = l1.clone()
也不能重新分配给变量。稍后将描述可用于保存可变状态的语法
i = 1
i = i + 1 # 分配错误: 不能分配两次
您可以在内部范围内定义具有相同名称的变量,但您只是覆盖它,而不是破坏性地重写它的值。如果您返回外部范围,该值也会返回 请注意,这是与 Python "语句"范围不同的行为 这种功能通常称为阴影。但是,与其他语言中的阴影不同,您不能在同一范围内进行阴影
x = 0
# x = 1 # 赋值错误: 不能赋值两次
if x.is_zero(), do:
x = 1 # 与同名的外部 x 不同
assert x == 1
assert x == 0
乍一看,以下内容似乎可行,但仍然不可能。这是一个设计决定,而不是技术限制
x = 0
if x.is_zero(), do:
x = x + 1 # 名称错误: 无法定义变量引用同名变量
assert x == 1
assert x == 0
常量
常数也是一种代数。如果标识符以大写字母开头,则将其视为常量。它们被称为常量,因为一旦定义,它们就不会改变
N
部分称为常量名(或标识符)。否则,它与变量相同
N = 0
if True, do:
N = 1 # 赋值错误: 常量不能被遮蔽
pass()
常量在定义的范围之外是不可变的。他们不能被遮蔽。由于这个属性,常量可以用于模式匹配。模式匹配在后面解释
例如,常量用于数学常量、有关外部资源的信息和其他不可变值
除了 types 之外的对象标识符使用全大写(所有字母大写的样式)是常见的做法
PI = 3.141592653589793
URL = "https://example.com"
CHOICES = ["a", "b", "c"]
PI = 3.141592653589793
match! x:
PI => print! "π"
other => print! "other"
当 x
为 3.141592653589793
时,上面的代码会打印 π
。如果 x
更改为任何其他数字,它会打印 other
有些对象不能绑定为常量。例如,可变对象。可变对象是其状态可以改变的对象,后面会详细介绍 这是因为只有常量表达式才能分配给常量的规则。常量表达式也将在后面讨论
X = 1 # OK
X = !1 # 类型错误: 无法定义 Int! 对象作为常量
删除变量
您可以使用 Del
函数删除变量。依赖于变量的所有其他变量(即直接引用变量值的变量)也将被删除
x = 1
y = 2
z = 3
f a = x + a
assert f(2) == 3
Del x
Del y, z
f(2) # 名称错误: f 未定义(在第 6 行中删除)
注意 Del
只能删除用户自定义模块中定义的变量。无法删除诸如"True"之类的内置常量
Del True # 类型错误: 无法删除内置常量
Del print! # TypeError: 无法删除内置变量
附录: 赋值和等价
请注意,当 x = a
时,x == a
不一定为真。一个例子是Float.NaN
。这是 IEEE 754 定义的浮点数的正式规范
x = Float.NaN
assert x ! = NaN
assert x ! = x
还有其他对象首先没有定义等价关系
f = x -> x**2 + 2x + 1
g = x -> (x + 1)**2
f == g # 类型错误: 无法比较函数对象
C = Class {i: Int}
D = Class {i: Int}
C == D # 类型错误: 无法比较类对象
严格来说,=
不会将右侧的值直接分配给左侧的标识符
在函数和类对象的情况下,执行"修改",例如将变量名称信息赋予对象。但是,结构类型并非如此
f x = x
print! f # <函数 f>
g x = x + 1
print! g # <函数 g>
C = Class {i: Int}
print! C # <类 C>
声明
声明是用于指定要使用的变量类型的语法 可以在代码中的任何地方进行声明,但单独的声明并不引用变量。它们必须被初始化 分配后,可以检查声明以确保类型与分配它的对象兼容
i: Int
# 可以与赋值同时声明,如 i: Int = 2
i = 2
i: Num
i: Nat
i: -2..2
i: {2}
赋值后的声明类似于assert
的类型检查,但具有在编译时检查的特点
在运行时通过assert
进行类型检查可以检查"可能是Foo类型",但是在编译时通过:
进行类型检查是严格的: 如果类型未确定为"类型Foo",则不会通过 检查会出现错误
i = (-1..10).sample!
assert i in Nat # 这可能会通过
i: Int # 这会通过
i: Nat # 这不会通过(-1 不是 Nat 的元素)
函数可以用两种不同的方式声明
f: (x: Int, y: Int) -> Int
f: (Int, Int) -> Int
如果显式声明参数名称,如果在定义时名称不同,则会导致类型错误。如果你想给参数名称任意命名,你可以用第二种方式声明它们。在这种情况下,类型检查只会看到方法名称及其类型
T = Trait {
.f = (x: Int, y: Int): Int
}
C = Class()
C|<: T|.
f(a: Int, b: Int): Int = ... # TypeError: `.f` must be type of `(x: Int, y: Int) -> Int`, not `(a: Int, b: Int) -> Int`
函数
函数是一个块,它接受一个"参数",对其进行处理,并将其作为"返回值"返回。定义如下
add x, y = x + y
# 或者
add(x, y) = x + y
在函数名之后指定的名称称为参数
相反,传递给函数的对象称为参数
函数 add
是一个以 x
和 y
作为参数并返回它们之和的函数,x + y
可以按如下方式调用(应用/调用)定义的函数
add 1, 2
# or
add(1, 2)
冒号应用风格
函数像f x, y, ...
一样被调用,但是如果单行参数太多,可以使用:
(冒号)来应用它们
f some_long_name_variable_1 + some_long_name_variable_2, some_long_name_variable_3 * some_long_name_variable_4
f some_long_name_variable_1 + some_long_name_variable_2:
some_long_name_variable_3 * some_long_name_variable_4
f:
some_long_name_variable_1 + some_long_name_variable_2
some_long_name_variable_3 * some_long_name_variable_4
以上三个代码的含义相同。例如,这种风格在使用 if
函数时也很有用
result = if Bool.sample!():
do:
log "True was chosen"
1
do:
log "False was chosen"
0
在 :
之后,除了注释之外,不得编写任何代码,并且必须始终在新行上
此外,您不能在函数后立即使用 :
。只有 do
和do!
可以做到这一点
# NG
f:
x
y
# Ok
f(
x,
y
)
关键字参数
如果使用大量参数定义函数,则存在以错误顺序传递参数的危险 在这种情况下,使用关键字参数调用函数是安全的
f x, y, z, w, v, u: Int = ...
上面定义的函数有很多参数,并且排列顺序混乱。您不应该创建这样的函数,但是在使用别人编写的代码时可能会遇到这样的代码。因此,我们使用关键字参数。如果使用关键字参数,则值会从名称传递到正确的参数,即使它们的顺序错误
f u := 6, v := 5, w := 4, x := 1, y := 2, z := 3
定义默认参数
当某些参数大部分是固定的并且您希望能够省略它们时,使用默认参数
默认参数由:=
(default-assign运算符)指定。如果未指定 base
,则将 math.E
分配给 base
math_log x: Ratio, base := math.E = ...
assert math_log(100, 10) == 2
assert math_log(100) == math_log(100, math.E)
请注意,不指定参数和指定None
是有区别的
p! x := 0 = print!
p!(2) # 2
p!() # 0
p!(None) # None
也可以与类型规范和模式一起使用
math_log x, base: Ratio := math.E = ...
f [x, y] := [1, 2] = ...
但是,在默认参数中,不能调用过程(稍后描述)或分配可变对象
f x := p! 1 = ... # NG
此外,刚刚定义的参数不能用作传递给默认参数的值
f x := 1, y := x = ... # NG
可变长度参数
输出其参数的日志(记录)的 log
函数可以采用任意数量的参数
log "你好", "世界", "!" # 你好 世界 !
要定义这样的函数,请将 *
添加到参数中。这样,函数将参数作为可变长度数组接收
f *x =
for x, i ->
log i
# x == [1, 2, 3, 4, 5]
f 1, 2, 3, 4, 5
具有多种模式的函数定义
fib n: Nat =
match n:
0 -> 0
1 -> 1
n -> fib(n - 1) + fib(n - 2)
像上面这样的函数,其中 match
直接出现在定义下,可以重写如下
fib 0 = 0
fib 1 = 1
fib(n: Nat): Nat = fib(n - 1) + fib(n - 2)
注意一个函数定义有多个模式不是所谓的重载(multiple definition); 一个函数只有一个定义。在上面的示例中,"n"必须与"0"或"1"属于同一类型。此外,与 match
一样,模式匹配是从上到下完成的
如果不同类的实例混合在一起,最后一个定义必须指定函数参数的类型为Or
f "aa" = ...
f 1 = ...
# `f x = ... ` 无效
f x: Int or Str = ...
此外,像 match
一样,它也必须是详尽的
fib 0 = 0
fib 1 = 1
# 模式错误: fib 参数的模式并不详尽
但是,可以通过使用稍后描述的 refinement type 显式指定类型来使其详尽无遗
fib: 0..1 -> 0..1
fib 0 = 0
fib 1 = 1
# OK
递归函数
递归函数是在其定义中包含自身的函数
作为一个简单的例子,让我们定义一个执行阶乘计算的函数factorial
。阶乘是"将所有小于或等于的正数相乘"的计算
5 的阶乘是 5*4*3*2*1 == 120
factorial 0 = 1
factorial 1 = 1
factorial(n: Nat): Nat = n * factorial(n - 1)
首先,从阶乘的定义来看,0和1的阶乘都是1
反过来,2的阶乘是2*1 == 2
,3的阶乘是3*2*1 == 6
,4的阶乘是4*3*2*1 == 24
如果我们仔细观察,我们可以看到一个数 n 的阶乘是前一个数 n-1 乘以 n 的阶乘
将其放入代码中,我们得到 n * factorial(n - 1)
由于 factorial
的定义包含自身,factorial
是一个递归函数
提醒一下,如果您不添加类型规范,则会这样推断
factorial: |T <: Sub(Int, T) and Mul(Int, Int) and Eq(Int)| T -> Int
factorial 0 = 1
factorial 1 = 1
factorial n = n * factorial(n - 1)
但是,即使您可以推理,您也应该明确指定递归函数的类型。在上面的例子中,像"factorial(-1)"这样的代码可以工作,但是
factorial(-1) == -1 * factorial(-2) == -1 * -2 * factorial(-3) == ...
并且这种计算不会停止。递归函数必须仔细定义值的范围,否则您可能会陷入无限循环 所以类型规范也有助于避免接受意外的值
High-order functions
高阶函数是将函数作为参数或返回值的函数 例如,一个以函数为参数的高阶函数可以写成如下
arg_f = i -> log i
higher_f(x: (Int -> NoneType)) = x 10
higher_f arg_f # 10
当然,也可以将返回值作为一个函数。
add(x): (Int -> Int) = y -> x + y
add_ten = add(10) # y -> 10 + y
add_hundred = add(100) # y -> 100 + y
assert add_ten(1) == 11
assert add_hundred(1) == 101
通过这种方式将函数作为参数和返回值,可以用函数定义更灵活的表达式
编译时函数
函数名以大写字母开头,表示编译时函数。用户定义的编译时函数必须将所有参数作为常量,并且必须指定它们的类型 编译时函数的功能有限。在编译时函数中只能使用常量表达式,即只有一些运算符(例如求积、比较和类型构造操作)和编译时函数。要传递的参数也必须是常量表达式 作为回报,优点是计算可以在编译时完成
Add(X, Y: Nat): Nat = X + Y
assert Add(1, 2) == 3
Factorial 0 = 1
Factorial(X: Nat): Nat = X * Factorial(X - 1)
assert Factorial(10) == 3628800
math = import "math"
Sin X = math.sin X # 常量错误: 此函数在编译时不可计算
编译时函数也用于多态类型定义
Option T: Type = T or NoneType
Option: Type -> Type
附录: 功能对比
Erg 没有为函数定义 ==
。这是因为通常没有函数的结构等价算法
f = x: Int -> (x + 1)**2
g = x: Int -> x**2 + 2x + 1
assert f == g # 类型错误: 无法比较函数
尽管 f
和 g
总是返回相同的结果,但要做出这样的决定是极其困难的。我们必须向编译器教授代数
所以 Erg 完全放弃了函数比较,并且 (x -> x) == (x -> x)
也会导致编译错误。这是与 Python 不同的规范,应该注意
# Python,奇怪的例子
f = lambda x: x
assert f == f
assert (lambda x: x) ! = (lambda x: x)
Appendix2: ()-completion
f x: Object = ...
# 将完成到
f(x: Object) = ...
f a
# 将完成到
f(a)
f a, b # 类型错误: f() 接受 1 个位置参数,但给出了 2 个
f(a, b) # # 类型错误: f() 接受 1 个位置参数,但给出了 2 个
f((a, b)) # OK
函数类型T -> U
实际上是(T,) -> U
的语法糖
内置函数
如果
if
是一个根据条件改变处理的函数
result: Option Int = if! Bool.sample!(), do:
log "True was chosen"
1
print! result # None (or 1)
.sample!()
返回一组随机值。如果返回值为真,print! "真"
被执行
如果条件为假,您还可以指定要执行的操作; 第二个 do 块称为 else 块
result: Nat = if Bool.sample!():
do:
log "True was chosen"
1
do:
log "False was chosen"
0
print! result # 1 (or 0)
如果进程是单行,则可以省略缩进
result = if Bool.sample!():
do 1
do 0
for
你可以使用 for
来编写一个重复的过程
match_s(ss: Iterator(Str), pat: Pattern): Option Str =
for ss, s ->
if pat.match(s).is_some():
break s
运算符
运算符是表示操作的符号。操作数是运算符(左)右侧的东西
运算符是一种函数,因此它们本身就是可以绑定到变量的一流对象。绑定时,需要用```括起来
对于+
(和-
),有一元和二元运算符,所以必须指定_+_
(二元运算)/+_
(一元运算)
add = `+` # 语法错误: 指定 `_+_` 或 `+_`
add=`_+_`
assert f(1, 2) == 3
assert f("a", "b") == "ab"
mul = `*` # OK, 这只是二进制
assert mul(1, 2) == 2
一些称为特殊形式的基本运算符不能被绑定
def = `=` # 语法错误: 无法绑定 `=` 运算符,这是一种特殊形式
# NG: def x, 1
function = `->` # 语法错误: 无法绑定 `->` 运算符,这是一种特殊形式
# NG: function x, x + 1
副作用
我们一直忽略了解释"!"的含义,但现在它的含义终于要揭晓了。这个 !
表示这个对象是一个带有"副作用"的"过程"。过程是具有副作用的函数
f x = print! x # EffectError: 不能为函数分配有副作用的对象
# 提示: 将名称更改为 'f!'
上面的代码会导致编译错误。这是因为您在函数中使用了过程。在这种情况下,您必须将其定义为过程
p! x = print! x
p!
, q!
, ... 是过程的典型变量名
以这种方式定义的过程也不能在函数中使用,因此副作用是完全隔离的
方法
函数和过程中的每一个都可以是方法。函数式方法只能对self
进行不可变引用,而程序性方法可以对self
进行可变引用
self
是一个特殊的参数,在方法的上下文中是指调用对象本身。引用 self
不能分配给任何其他变量
C!.
method ref self =
x = self # 所有权错误: 无法移出`self`
x
程序方法也可以采取 self
的 ownership。从方法定义中删除 ref
或 ref!
n = 1
s = n.into(Str) # '1'
n # 值错误: n 被 .into 移动(第 2 行)
在任何给定时间,只有一种程序方法可以具有可变引用。此外,在获取可变引用时,不能从原始对象获取更多可变引用。从这个意义上说,ref!
会对self
产生副作用
但是请注意,可以从可变引用创建(不可变/可变)引用。这允许在程序方法中递归和 print!
的self
T -> T # OK (move)
T -> Ref T # OK (move)
T => Ref! T # OK (only once)
Ref T -> T # NG
Ref T -> Ref T # OK
Ref T => Ref!
T -> Ref T # NG
T -> Ref T # OK
T => Ref!
附录: 副作用的严格定义
代码是否具有副作用的规则无法立即理解
直到你能理解它们,我们建议你暂时把它们定义为函数,如果出现错误,添加!
将它们视为过程
但是,对于那些想了解该语言的确切规范的人,以下是对副作用的更详细说明
首先,必须声明返回值的等价与 Erg 中的副作用无关
有些过程对于任何给定的 x
都会导致 p!(x) == p!(x)
(例如,总是返回 None
),并且有些函数会导致 f(x) ! = f(x)
前者的一个例子是print!
,后者的一个例子是下面的函数
nan _ = Float.NaN
assert nan(1) ! = nan(1)
还有一些对象,例如类,等价确定本身是不可能的
T = Structural {i = Int}
U = Structural {i = Int}
assert T == U
C = Class {i = Int}
D = Class {i = Int}
assert C == D # 类型错误: 无法比较类
言归正传: Erg 中"副作用"的准确定义是
- 访问可变的外部信息
"外部"一般是指外部范围; Erg 无法触及的计算机资源和执行前/执行后的信息不包含在"外部"中。"访问"包括阅读和写作
例如,考虑 print!
过程。乍一看,print!
似乎没有重写任何变量。但如果它是一个函数,它可以重写外部变量,例如,使用如下代码:
camera = import "some_camera_module"
ocr = import "some_ocr_module"
n = 0
_ =
f x = print x # 假设我们可以使用 print 作为函数
f(3.141592)
cam = camera.new() # 摄像头面向 PC 显示器
image = cam.shot!()
n = ocr.read_num(image) # n = 3.141592
将"camera"模块视为为特定相机产品提供 API 的外部库,将"ocr"视为用于 OCR(光学字符识别)的库
直接的副作用是由 cam.shot!()
引起的,但显然这些信息是从 f
泄露的。因此,print!
本质上不可能是一个函数
然而,在某些情况下,您可能希望临时检查函数中的值,而不想为此目的在相关函数中添加 !
。在这种情况下,可以使用 log
函数
log
打印整个代码执行后的值。这样,副作用就不会传播
log "this will be printed after execution"
print! "this will be printed immediately"
# 这将立即打印
# 这将在执行后打印
如果没有反馈给程序,或者换句话说,如果没有外部对象可以使用内部信息,那么信息的"泄漏"是可以允许的。只需要不"传播"信息
程序
程序是指允许副作用的函数
基本用法或定义请参考Function
添加 !
到函数名来定义它
proc!(x: Int!, y: Int!) =
for! 0..x, i =>
for 0..y, j =>
print! i, j
在处理可变对象时过程是必需的,但是将可变对象作为参数并不一定使它成为过程 这是一个函数接受一个可变对象(不是过程)
peek_str s: Str! = log s
make_proc(x!: (Int => Int)): (Int => Int) = y => x! y
p! = make_proc(x => x)
print! p! 1 # 1
此外,过程和函数通过proc :> func
关联
因此,可以在过程中定义函数
但是,请注意,反过来是不可能的
proc!(x: Int!) = y -> log x, y # OK
func(x: Int) = y => print! x, y # NG
绑定
过程可以操作范围外的变量。
x = ! 0
proc! () =
x.inc! ()
proc! ()
assert x == 1
此时,“proc!” 具有以下类型:
proc!: {| x: Int! |} () => ()
`{| x: Int! |} '部分称为绑定列,表示过程操作的变量及其类型。 绑定列是自动派生的,因此无需显式编写。 请注意,常规过程只能操作预先确定的外部变量。 这意味着不能重写传递给参数的变量。 如果要执行此操作,则必须使用过程方法。 过程方法可以重写“自”。
C! N = Class {arr = [Int; N]!}
C!.
new() = Self! (0)::__new__ {arr = ![]}
C! (N).
# push!: {|self: C!( N) ~> C! (N+1)|} (self: RefMut(C!( N)), x: Int) => NoneType
push! ref! self, x = self.arr.push! (x)
# pop!: {|self: C!( N) ~> C! (N-1)|} (self: RefMut(C!( N))) => Int
pop! ref! self = self.arr.pop! ()
c = C!. new()
c.push! (1)
assert c.pop! () == 1
内置程序
id!
返回对象的唯一标识号
尽管在纯 Erg 语义中,结构相同的对象之间没有区别,但实际上对象在内存中具有不同的位置
id!
返回一个代表这个位置的数字
数组
数组是最基本的__collection(聚合)__ 集合是一个可以在其中包含多个对象的对象
a = [1, 2, 3]
a: [Int; 3] # 类型说明: 分号后的数字为元素个数
# 如果元素个数未知,可以省略
a: [Int]
mut_a = [!1, !2, !3]
mut_a[0].inc!()
assert mut_a == [2, 2, 3]
通常,数组不能包含不同类型的对象
[1, "a"] # 类型错误: 第一个元素是 Int,但第二个元素是 Str
但是,您可以通过像这样显式指定类型来绕过限制
[1: Int or Str, "a"]
切片
一个数组也可以同时取出多个值。这称为切片
l = [1, 2, 3, 4]
# 与 Python 中的 l[1:3] 相同
assert l[1.. <3] == [2, 3]
assert l[1..2] == [2, 3]
# 与 l[1] 相同
assert l[1..1] == [2]
# 与 Python 中的 l[::2] 相同
assert l[..].step(2) == [2, 4]
通过切片获得的对象是数组的(不可变的)副本
print! Typeof l[1..2] # [Int; 4]
字典
Dict 是键/值对的集合
ids = {"Alice": 145, "Bob": 214, "Charlie": 301}
assert ids["Alice"] == 145
如果键是"哈希"对象,则键不必是字符串
# 不推荐使用范围对象作为键(与切片混淆)
r = {1..3: "1~3", 4..6: "4~6", 7..9: "7~9"}
assert r[1..3] == "1~3"
l = {[]: "empty", [1]: "1"}
assert l[[]] == "empty"
对于字典来说,顺序无关紧要。它也不能有重复的元素。在这方面,Dict 与 Set 类似 您可以说 Dict 是具有值的 Set
{"Alice": 145, "Bob": 214, "Charlie": 301} == {"Alice": 145, "Charlie": 301, "Bob": 214}
从 dict 文字生成 dict 时,会检查重复键 任何重复都会导致编译错误
{"Alice": 145, "Alice": 1} # Key错误: 重复键`Alice`
空字典是用 {:}
创建的。请注意,{}
表示一个空集
mut_dict = !{:}
mut_dict.insert! "Alice", 145
mut_dict.insert! "Bob", 214
assert mut_dict["Alice"] == 145
异构字典
不需要有单一的键/值类型。这样的字典称为 _heterogenous dict
d: {Str: Int, Int: Str} = {"a": 1, 1: "a"}
assert d["a"] == 1
assert d[1] == "a"
但是,不能将相同类型的值分配给不同类型的键,或者将不同类型的值分配给相同类型的键 在这种情况下,请改用 Or 类型
invalid1 = {1: "a", "a": "b"}
invalid2 = {1: "a", 2: 2}
# Erg 类型推断不推断 Or 类型,因此需要类型说明
valid1: {Int or Str: Str} = {1: "a", "a": "b"}
valid2: {Int: Int or Str} = {1: "a", 2: 2}
下标
[]
不同于普通的方法
a = [!1, !2]
a[0].inc!()
assert a == [2, 2]
回想一下,子例程的返回值不能是引用
这里的 a[0]
的类型显然应该是 Ref!(Int!)
(a[0]
的类型取决于上下文)
所以 []
实际上是特殊语法的一部分,就像 .
一样。与 Python 不同,它不能被重载
也无法在方法中重现 []
的行为
C = Class {i = Int!}
C.steal(self) =
self::i
C. get(ref self) =
self::i # 类型错误:`self::i`是`Int!`(需要所有权)但`get`不拥有`self`
# OK (分配)
c = C.new({i = 1})
i = c.steal()
i.inc!()
assert i == 2
# or (own_do!)
own_do! C.new({i = 1}).steal(), i => i.inc!()
# NG
C.new({i = 1}).steal().inc!() # OwnershipWarning: `C.new({i = 1}).steal()` is not owned by anyone
# hint: assign to a variable or use `uwn_do!`
此外,[]
可以不承认,但元素不会移动
a = [!1, !2]
i = a[0]
i.inc!()
assert a[1] == 2
a[0] # 所有权错误:`a[0]`被移动到`i`
元组
元组类似于数组,但可以保存不同类型的对象 这样的集合称为不等集合。相比之下,同构集合包括数组、集合等
t = (1, True, "a")
(i, b, s) = t
assert(i == 1 and b == True and s == "a")
元组t
可以以t.n
的形式检索第n个元素; 请注意,与 Python 不同,它不是 t[n]
这是因为访问元组元素更像是一个属性(在编译时检查元素的存在,并且类型可以根据 n
改变)而不是方法(数组的 []
是一种方法)
assert t.0 == 1
assert t.1 == True
assert t.2 == "a"
括号 ()
在不嵌套时是可选的
t = 1, True, "a"
i, b, s = t
元组可以保存不同类型的对象,因此它们不能像数组一样被迭代
t: ({1}, {2}, {3}) = (1, 2, 3)
(1, 2, 3).iter().map(x -> x + 1) # 类型错误: 类型 ({1}, {2}, {3}) 没有方法 `.iter()`
# 如果所有类型都相同,则可以像数组一样用`(T; n)`表示,但这仍然不允许迭代
t: (Int; 3) = (1, 2, 3)
assert (Int; 3) == (Int, Int, Int)
但是,非同质集合(如元组)可以通过向上转换、相交等方式转换为同质集合(如数组) 这称为均衡
(Int, Bool, Str) can be [T; 3] where T :> Int, T :> Bool, T :> Str
t: (Int, Bool, Str) = (1, True, "a") # 非同质
a: [Int or Bool or Str; 3] = [1, True, "a"] # 同质的
_a: [Show; 3] = [1, True, "a"] # 同质的
_a.iter().map(x -> log x) # OK
t.try_into([Show; 3])? .iter().map(x -> log x) # OK
单元
零元素的元组称为 unit。一个单元是一个值,但也指它自己的类型
unit = ()
(): ()
Unit是所有图元的超级类
() > (Int; 0)
() > (Str; 0)
() :> (Int, Str)
...
该对象的用途是用于没有参数和没有返回值的过程等。Erg 子例程必须有参数和返回值。但是,在某些情况下,例如过程,可能没有有意义的参数或返回值,只有副作用。在这种情况下,我们将单位用作"无意义的正式值"
p!() =.
# `print!` does not return a meaningful value
print! "Hello, world!"
p!: () => () # The parameter part is part of the syntax, not a tuple
但是,在这种情况下,Python 倾向于使用"无"而不是单位
在 Erg 中,当您从一开始就确定操作不会返回有意义的值(例如在过程中)时,您应该使用 ()
,并且当操作可能失败并且您可能会返回 None
将一无所获,例如在检索元素时
记录(Record)
记录是一个集合,它结合了通过键访问的 Dict 和在编译时检查其访问的元组的属性 如果您了解 JavaScript,请将其视为一种(更增强的)对象字面量表示法
john = {.name = "John"; .age = 21}
assert john.name == "John"
assert john.age == 21
assert john in {.name = Str; .age = Nat}
john["name"] # 错误: john 不可订阅
.name
和 .age
部分称为属性,而 "John"
和 21
部分称为属性值
与 JavaScript 对象字面量的区别在于它们不能作为字符串访问。也就是说,属性不仅仅是字符串
这是因为对值的访问是在编译时确定的,而且字典和记录是不同的东西。换句话说,{"name": "John"}
是一个字典,{name = "John"}
是一个记录
那么我们应该如何使用字典和记录呢?
一般来说,我们建议使用记录。记录具有在编译时检查元素是否存在以及能够指定 _visibility 的优点
指定可见性等同于在 Java 和其他语言中指定公共/私有。有关详细信息,请参阅 可见性 了解详细信息
a = {x = 1; .y = x + 1}
a.x # 属性错误: x 是私有的
# 提示: 声明为 `.x`
assert a.y == 2
对于熟悉 JavaScript 的人来说,上面的示例可能看起来很奇怪,但简单地声明 x
会使其无法从外部访问
您还可以显式指定属性的类型
anonymous = {
.name: Option! Str = "Jane Doe"
.age = 20
}
anonymous.name.set! "John Doe"
一个记录也可以有方法
o = {
.i = !0
.inc! ref! self = self.i.inc!()
}
assert o.i == 0
o.inc!()
assert o.i == 1
关于记录有一个值得注意的语法。当记录的所有属性值都是类(不是结构类型)时,记录本身表现为一个类型,其自身的属性作为必需属性 这种类型称为记录类型。有关详细信息,请参阅 [记录] 部分
# 记录
john = {.name = "John"}
# 记录 type
john: {.name = Str}
Named = {.name = Str}
john: Named
greet! n: Named =
print! "Hello, I am \{n.name}"
john # "你好,我是约翰 print!
Named.name # Str
解构记录
记录可以按如下方式解构
record = {x = 1; y = 2}
{x = a; y = b} = record
assert a == 1
assert b == 2
point = {x = 2; y = 3; z = 4}
match point:
{x = 0; y = 0; z = 0} -> "origin"
{x = _; y = 0; z = 0} -> "on the x axis"
{x = 0; *} -> "x = 0"
{x = x; y = y; z = z} -> "(\{x}, \{y}, \{z})"
当存在与属性同名的变量时,x = ...
也可以缩写为x
,例如x = x
或x = .x
到x
,和 .x = .x
或 .x = x
到 .x
但是,当只有一个属性时,必须在其后加上;
以与集合区分开来
x = 1
y = 2
xy = {x; y}
a = 1
b = 2
ab = {.a; .b}
assert ab.a == 1
assert ab.b == 2
record = {x;}
tuple = {x}
assert tuple.1 == 1
此语法可用于解构记录并将其分配给变量
# 一样 `{x = x; y = y} = xy`
{x; y} = xy
assert x == 1
assert y == 2
# 一样 `{.a = a; .b = b} = ab`
{a; b} = ab
assert a == 1
assert b == 2
空记录
空记录由{=}
表示。空记录也是它自己的类,如 Unit
empty_record = {=}
empty_record: {=}
# Object: Type = {=}
empty_record: Object
empty_record: Structural {=}
{x = 3; y = 5}: Structural {=}
空记录不同于空 Dict {:}
或空集 {}
。特别要注意的是,它与 {}
的含义相反(在 Python 中,{}
是一个空字典,而在 Erg 中它是 Erg 中的 !{:}
)
作为枚举类型,{}
是一个空类型,其元素中不包含任何内容。Never
类型是这种类型的一个分类
相反,记录类 {=}
没有必需的实例属性,因此所有对象都是它的元素。Object
是 this 的别名
一个Object
(Object
的一个补丁)是的一个元素。__sizeof__
和其他非常基本的提供方法
AnyPatch = Patch Structural {=}
. __sizeof__ self = ...
.clone self = ...
...
Never = Class {}
请注意,没有其他类型或类在结构上与 {}
、Never
类型等效,如果用户在右侧使用 {}
、Class {}
定义类型,则会出错
这意味着,例如,1..10 或 -10。-1
,但 1..10 和 -10... -1
。例如,当它应该是 1..10 或 -10...-1 时是 -1
此外,如果您定义的类型(例如 Int 和 Str
)会导致组合 Object
,则会警告您只需将其设置为 Object
即时封锁
Erg 有另一种语法 Instant 块,它只返回最后评估的值。不能保留属性
x =
x = 1
y = x + 1
y ** 3
assert x == 8
y =
.x = 1 # 语法错误: 无法在实体块中定义属性
数据类
如果您尝试自己实现方法,则必须直接在实例中定义裸记录(由记录文字生成的记录) 这是低效的,并且随着属性数量的增加,错误消息等变得难以查看和使用
john = {
name = "John Smith"
age = !20
.greet! ref self = print! "Hello, my name is \{self::name} and I am \{self::age} years old."
.inc_age! ref! self = self::age.update! x -> x + 1
}
print! john + 1
# 类型错误: {name = Str; 没有实现 + 年龄=诠释; 。迎接! =参考(自我)。() => 无; inc_age! =参考! () => 无}, 整数
因此,在这种情况下,您可以继承一个记录类。这样的类称为数据类 这在 class 中有描述
Person = Inherit {name = Str; age = Nat}
Person.
greet! ref self = print! "Hello, my name is \{self::name} and I am \{self::age} years old."
inc_age! ref! self = self::age.update! x -> x + 1
john = Person.new {name = "John Smith"; age = 20}
print! john + 1
# 类型错误: Person、Int 没有实现 +
Set
一个Set代表一个集合,它在结构上是一个重复的无序数组
assert Set.from([1, 2, 3, 2, 1]) == {1, 2, 3}
assert {1, 2} == {1, 1, 2} # 重复的被自动删除
assert {1, 2} == {2, 1}
它也可以用类型和长度来声明
a: {Int; 3} = {0, 1, 2} # OK
b: {Int; 3} = {0, 0, 0} # NG,重复的内容被删除,长度也会改变
# TypeError: the type of b is mismatched
# expected: Set(Int, 3)
# but found: Set({0, }, 1)
此外,只有实现Eq
跟踪的对象才能成为集合的元素
因此,不可能使用Floats等作为集合元素
d = {0.0, 1.0} # NG
#
# 1│ d = {0.0, 1.0}
# ^^^^^^^^
# TypeError: the type of _ is mismatched:
# expected: Eq(Float)
# but found: {0.0, 1.0, }
Set可以执行集合操作
assert 1 in {1, 2, 3}
assert not 1 in {}
assert {1} or {2} == {1, 2}
assert {1, 2} and {2, 3} == {2}
assert {1, 2} not {2} == {1}
Set是同质集合。为了使不同类的对象共存,它们必须同质化
s: {Int or Str} = {"a", 1, "b", -1}
Sets为类型
Sets也可以被视为类型。这种类型称为 枚举类型
i: {1, 2, 3} = 1
assert i in {1, 2, 3}
Set的元素直接是类型的元素 请注意,这些Set本身是不同的
mut_set = {1, 2, 3}.into {Int; !3}
mut_set.insert!(4)
类型
类型是 Erg 中一个非常重要的特性,所以我们有一个 dedicated section。请看那里
Erg 的类型系统
下面简单介绍一下 Erg 的类型系统。详细信息在其他部分进行说明
如何定义
Erg 的独特功能之一是(普通)变量、函数(子例程)和类型(Kind)定义之间的语法没有太大区别。所有都是根据普通变量和函数定义的语法定义的
f i: Int = i + 1
f # <函数 f>
f(1) # 2
f.method self = ... # 语法错误: 无法为子例程定义方法
T I: Int = {...}
T # <kind 'T'>
T(1) # 类型 T(1)
T.method self = ...
D = Class {private = Int; .public = Int}
D # <类 'D'>
o1 = {private = 1; .public = 2} # o1 是一个不属于任何类的对象
o2 = D.new {private = 1; .public = 2} # o2 是 D 的一个实例
o2 = D.new {.public = 2} # 初始化错误: 类 'D' 需要属性 'private'(: Int) 但未定义
Classification
Erg 中的所有对象都是强类型的
顶层类型是{=}
,实现了__repr__
、__hash__
、clone
等(不是必须的方法,这些属性不能被覆盖)
Erg 的类型系统包含结构子类型 (SST)。该系统类型化的类型称为结构类型
结构类型主要分为三种: Attributive(属性类型)、Refinement(细化类型)和Algebraic(代数类型)
Record | Enum | Interval | Union | Intersection | Diff | |
---|---|---|---|---|---|---|
kind | Attributive | Refinement | Refinement | Algebraic | Algebraic | Algebraic |
generator | record | set | range operator | or operator | and operator | not operator |
也可以使用名义子类型(NST),将 SST 类型转换为 NST 类型称为类型的名义化。结果类型称为名义类型 在 Erg 中,名义类型是类和Trait。当我们简单地说类/Trait时,我们通常指的是记录类/Trait
Type | Abstraction | Subtyping procedure | |
---|---|---|---|
NST | NominalType | Trait | Inheritance |
SST | StructuralType | Structural Trait | (Implicit) |
整个名义类型的类型(NominalType
)和整个结构类型的类型(StructuralType
)是整个类型(Type
)的类型的子类型
Erg 可以将参数(类型参数)传递给类型定义。带有类型参数的 Option
、Array
等称为多项式类型。这些本身不是类型,但它们通过应用参数成为类型。诸如 Int
、Str
等没有参数的类型称为简单类型(标量类型)
一个类型可以看成一个集合,并且存在包含关系。例如,"Num"包含"Add"、"Sub"等,"Int"包含"Nat"
所有类的上类是Object == Class {:}
,所有类型的下类是Never == Class {}
。这在下面描述
类型
像 Array T
这样的类型可以看作是 Type -> Type
类型的函数,它以 T
类型为参数并返回 Array T
类型(在类型论中也称为 Kind)。像 Array T
这样的类型专门称为多态类型,而 Array
本身称为一元 Kind
已知参数和返回类型的函数的类型表示为(T, U) -> V
。如果要指定同一类型的整个双参数函数,可以使用 |T| (T, T) -> T
,如果要指定整个 N 参数函数,可以使用 Func N
。但是,Func N
类型没有关于参数数量或其类型的信息,因此所有返回值在调用时都是Obj
类型
Proc
类型表示为 () => Int
等等。此外,Proc
类型实例的名称必须以 !
结尾
Method
类型是一个函数/过程,其第一个参数是它所属的对象 self
(通过引用)。对于依赖类型,也可以在应用方法后指定自己的类型。这是 T!(!N)
类型和 T!(N ~> N-1)。() => Int
等等
Erg 的数组(Array)就是 Python 所说的列表。[诠释; 3]
是一个数组类,包含三个Int
类型的对象
Note:
(Type; N)
既是类型又是值,所以可以这样使用Types = (Int, Str, Bool) for! Types, T => print! T # Int Str Bool a: Types = (1, "aaa", True)
pop|T, N|(l: [T; N]): ([T; N-1], T) =
[*l, last] = l
(l, last)
lpop|T, N|(l: [T; N]): (T, [T; N-1]) =
[first, *l] = l
(first, l)
以 !
结尾的类型可以重写内部结构。例如,[T; !N]
类是一个动态数组
要从"T"类型的对象创建"T!"类型的对象,请使用一元运算符"!"
i: Int! = !1
i.update! i -> i + 1
assert i == 2
arr = [1, 2, 3]
arr.push! 4 # 导入错误
mut_arr = [1, 2, 3].into [Int; !3]
mut_arr.push4
assert mut_arr == [1, 2, 3, 4].
类型定义
类型定义如下
Point2D = {.x = Int; .y = Int}
请注意,如果从变量中省略 .
,它将成为类型中使用的私有变量。但是,这也是必需的属性
由于类型也是对象,因此类型本身也有属性。这样的属性称为类型属性。在类的情况下,它们也称为类属性
数据类型
如前所述,Erg 中的"类型"大致表示一组对象
下面是 Add
类型的定义,需要 +
(中间运算符)。R, O
是所谓的类型参数,可以是真正的类型(类),例如 Int
或 Str
。在其他语言中,类型参数被赋予特殊的符号(泛型、模板等),但在 Erg 中,它们可以像普通参数一样定义
类型参数也可以用于类型对象以外的类型。例如数组类型[Int; 3]
是 Array Int, 3
的语法糖。如果类型实现重叠,用户必须明确选择一个
Add R = Trait {
.AddO = Type
. `_+_` = Self.(R) -> Self.AddO
}
._+_
是Add._+_
的缩写。前缀运算符 .+_
是 Num
类型的方法
Num = Add and Sub and Mul and Eq
NumImpl = Patch Num
NumImpl.
`+_`(self): Self = self
...
多态类型可以像函数一样对待。通过将它们指定为 Mul Int、Str
等,它们可以是单态的(在许多情况下,它们是用实际参数推断出来的,而没有指定它们)
1 + 1
`_+_` 1, 1
Nat.`_+_` 1, 1
Int.`_+_` 1, 1
前四行返回相同的结果(准确地说,底部的返回 Int
),但通常使用顶部的
Ratio.
+(1, 1)
将返回 2.0
而不会出错
这是因为 Int <: Ratio
,所以 1
向下转换为 Ratio
但这不是演员
i = 1
if i: # 类型错误: i: Int 不能转换为 Bool,请改用 Int.is_zero()
log "a"
log "b"
这是因为 Bool <: Int
(True == 1
, False == 0
)。转换为子类型通常需要验证
类型推理系统
Erg 使用静态鸭子类型,因此几乎不需要显式指定类型
f x, y = x + y
在上面的代码中,带有 +
的类型,即 Add
是自动推断的; Erg 首先推断出最小的类型。如果f 0, 1
,它将推断f x: {0},y: {1}
,如果n: Nat; f n, 1
,它会推断f x: Nat, y: {1}
。最小化之后,增加类型直到找到实现。在 {0}, {1}
的情况下,Nat
与 Nat
是单态的,因为 Nat
是具有 +
实现的最小类型
如果是 {0}, {-1}
,它与 Int
是单态的,因为它不匹配 Nat
。如果子类型和父类型之间没有关系,则首先尝试具有最低浓度(实例数)(或者在多态类型的情况下参数更少)的那个
{0}
和 {1}
是枚举类型,它们是部分类型,例如 Int
和 Nat
例如,可以为枚举类型指定名称和请求/实现方法。在有权访问该类型的命名空间中,满足请求的对象可以使用实现方法
Binary = Patch {0, 1}
Binary.
# self 包含一个实例。在此示例中,为 0 或 1
# 如果你想重写self,你必须追加! 必须添加到类型名称和方法名称
is_zero(self) = match self:
0 -> True
1 -> False # 你也可以使用 _ -> False
is_one(self) = not self.is_zero()
to_bool(self) = match self:
0 -> False
1 -> True
此后,代码"0.to_bool()"是可能的(尽管"0 as Bool == False"是内置定义的)
这是一个实际上可以重写 self
的类型的示例,如代码所示
Binary! = Patch {0, 1}!
Binary!
switch! ref! self = match! self:
0 => self = 1
1 => self = 0
b = !1
b.switch!()
print! b # => 0
结构类型(匿名类型)
Binary = {0, 1}
上面代码中的 Binary
是一个类型,其元素是 0
和 1
。它也是 Int
类型的子类型,它同时具有 0
和 1
像 {}
这样的对象本身就是一种类型,可以在分配或不分配给上述变量的情况下使用
这样的类型称为结构类型。当我们想强调它作为后者而不是类(命名类型)的用途时,它也被称为未命名类型。{0, 1}
这样的结构类型称为枚举类型,还有区间类型、记录类型等
类型标识
无法指定以下内容。例如,您不能指定 Int
和 Int
和 Int
和 Int
和 Int
和 Int
例如,Int
和Str
都是Add
,但是Int
和Str
不能相加
add l: Add, r: Add =
l + r # 类型错误: `_+_` 没有实现: |T, U <: Add| (T, U) -> <失败>
此外,下面的类型 A
和 B
不被认为是同一类型。但是,类型"O"被认为是匹配的
... |R1, R2, O, A <: Add(R1, O); B <: Add(R2, O)|
基本语法
类型规范
在 Erg 中,可以在 :
之后指定变量的类型,如下所示。这可以与作业同时完成
i: Int # 将变量 i 声明为 Int 类型
i: Int = 1
j = 1 # 类型说明可以省略
您还可以指定普通表达式的类型
i = 1: Int
f([1, "a"]: [Int or Str])
对于简单的变量赋值,大多数类型说明可以省略 在定义子例程和类型时,类型规范更有用
# 参数的类型规范
f x, y: Array Int = ...
T X, Y: Array Int = ...
请注意,在上述情况下,x, y
都是 Array Int
# 大写变量的值必须是常量表达式
f X: Int = X
或者,如果你不需要关于类型参数的完整信息,你可以用 _
省略它
g v: [T; _] = ...
但是请注意,类型规范中的 _
意味着 Object
f x: _, y: Int = x + y # 类型错误: Object 和 Int 之间没有实现 +
子类型规范
除了 :
(类型声明运算符),Erg 还允许您使用 <:
(部分类型声明运算符)来指定类型之间的关系
<:
的左边只能指定一个类。使用 Subtypeof
或类似的运算符来比较结构类型
这也经常在定义子例程或类型时使用,而不是简单地指定变量
# 参数的子类型规范
f X <: T = ...
# 所需属性的子类型规范(.Iterator 属性必须是 Iterator 类型的子类型)
Iterable T = Trait {
.Iterator = {Iterator} # {Iterator} == {I: Type | I <: Iterator}
.iter = Self.() -> Self.Iterator T
...
}
也可以在定义类时使用子类型规范来静态检查该类是否是指定类型的子类型
# C 类是 Show 的子类型
C = Class Object, Impl := Show
C.show self = ... # 显示所需的属性
您也可以仅在特定情况下指定子类型
K T: Eq
K Int <: Show and Eq
K T = Class Object
K(T).
`==` self, other = ...
K(Int).
show self = ...
实现结构类型时建议使用子类型规范 这是因为,由于结构子类型的性质,拼写错误或类型规范错误在实现所需属性时不会导致错误
C = Class Object
C.shoe self = ... # Show 由于 Typo 没有实现(它被认为只是一种独特的方法)
属性定义
只能在模块中为Trait和类定义属性
C = Class()
C.pub_attr = "this is public"
C::private_attr = "this is private"
c = C.new()
assert c.pub_attr == "this is public"
定义批处理定义的语法称为批处理定义,其中在 C.
或 C::
之后添加换行符,并且定义在缩进下方组合在一起
C = Class()
C.pub1 = ...
C.pub2 = ...
C::priv1 = ...
C::priv2 = ...
# 相当于
C = Class()
C.
pub1 = ...
C. pub2 = ...
C::
priv1 = ...
priv2 = ...
别名
类型可以有别名。这允许缩短长类型,例如记录类型
Id = Int
Point3D = {x = Int; y = Int; z = Int}
IorS = Int or Str
Vector = Array Int
此外,当显示错误时,如果定义了复合类型(在上面的示例中,右侧类型不是第一个类型),编译器将为它们使用别名
但是,每个模块只允许一个相同类型的别名,多个别名将导致警告 这意味着应将具有不同用途的类型定义为单独的类型 目的还在于防止在已经具有别名的类型之上添加别名
Id = Int
UserId = Int # 类型警告: 重复别名: Id 和 UserId
Ids = Array Id
Ints = Array Int # 类型警告: 重复别名: Isd 和 Ints
IorS = Int or Str
IorSorB = IorS or Bool
IorSorB_ = Int or Str or Bool # 类型警告: 重复别名: IorSorB 和 IorSorB_
Point2D = {x = Int; y = Int}
Point3D = {.... Point2D; z = Int}
Point = {x = Int; y = Int; z = Int} # 类型警告: 重复别名: Point3D 和 Point
Trait
Trait 是一种名义类型,它将类型属性要求添加到记录类型 它类似于 Python 中的抽象基类 (ABC),但区别在于能够执行代数运算
Norm = Trait {.x = Int; .y = Int; .norm = Self.() -> Int}
trait不区分属性和方法
注意,trait 只能声明,不能实现(实现是通过一个叫做 patching 的特性来实现的,后面会讨论) 可以通过指定部分类型来检查Trait在类中的实现
Point2D <: Norm
Point2D = Class {.x = Int; .y = Int}
Point2D.norm self = self.x**2 + self.y**2
Error if the required attributes are not implemented.
Point2D <: Norm # 类型错误: Point2D 不是 Norm 的子类型
Point2D = Class {.x = Int; .y = Int}
Trait与结构类型一样,可以应用组合、替换和消除等操作(例如"T 和 U")。由此产生的Trait称为即时Trait
T = Trait {.x = Int}
U = Trait {.y = Int}
V = Trait {.x = Int; y: Int}
assert Structural(T and U) == Structural V
assert Structural(V not U) == Structural T
W = Trait {.x = Ratio}
assert Structural(W) ! = Structural(T)
assert Structural(W) == Structural(T.replace {.x = Ratio})
Trait 也是一种类型,因此可以用于普通类型规范
points: [Norm; 2] = [Point2D::new(1, 2), Point2D::new(3, 4)]
assert points.iter().map(x -> x.norm()).collect(Array) == [5, 25].
Trait包含
Subsume
允许您将包含某个Trait的Trait定义为父类型。这称为Trait的 subsumption
在下面的示例中,BinAddSub
包含 BinAdd
和 BinSub
这对应于类中的继承,但与继承不同的是,可以使用"和"组合多个基类型。也允许被 not
部分排除的Trait
Add R = Trait {
.AddO = Type
. `_+_` = Self.(R) -> Self.AddO
}
Sub R = Trait {
.SubO = Type
. `_-_` = Self.(R) -> Self.SubO
}
BinAddSub = Subsume Add(Self) and Sub(Self)
结构Trait
Trait可以结构化
SAdd = Structural Trait {
. `_+_` = Self.(Self) -> Self
}
# |A <: SAdd| 不能省略
add|A <: SAdd| x, y: A = x.`_+_` y
C = Class {i = Int}
C.
new i = Self.__new__ {i;}
`_+_` self, other: Self = Self.new {i = self::i + other::i}
assert add(C.new(1), C.new(2)) == C.new(3)
名义Trait不能简单地通过实现请求方法来使用,而必须明确声明已实现
在以下示例中,add
不能与C
类型的参数一起使用,因为没有明确的实现声明。它必须是C = Class {i = Int}, Impl := Add
Add = Trait {
.`_+_` = Self.(Self) -> Self
}
# |A <: 添加| 可以省略
add|A <: Add| x, y: A = x.`_+_` y
C = Class {i = Int}
C.
new i = Self.__new__ {i;}
`_+_` self, other: Self = Self.new {i = self::i + other::i}
add C.new(1), C.new(2) # 类型错误: C 不是 Add 的子类
# 提示: 继承或修补"添加"
不需要为此实现声明结构Trait,但类型推断不起作用。使用时需要指定类型
多态Trait
Trait可以带参数。这与多态类型相同
Mapper T: Type = Trait {
.mapIter = {Iterator}
.map = (self: Self, T -> U) -> Self.MapIter U
}
# ArrayIterator <: Mapper
# ArrayIterator.MapIter == ArrayMapper
# [1, 2, 3].iter(): ArrayIterator Int
# [1, 2, 3].iter().map(x -> "\{x}"): ArrayMapper Str
assert [1, 2, 3].iter().map(x -> "\{x}").collect(Array) == ["1", "2", "3"].
OverrideTrait
派生Trait可以Override基本Trait的类型定义 在这种情况下,Override方法的类型必须是基方法类型的子类型
# `Self.(R) -> O` is a subtype of ``Self.(R) -> O or Panic
Div R, O: Type = Trait {
. `/` = Self.(R) -> O or Panic
}
SafeDiv R, O = Subsume Div, {
@Override
. `/` = Self.(R) -> O
}
在 API 中实现和解决重复的Trait
Add
、Sub
和 Mul
的实际定义如下所示
Add R = Trait {
.Output = Type
. `_+_` = Self.(R) -> .Output
}
Sub R = Trait {
.Output = Type
. `_-_` = Self.(R) -> .Output
}
Mul R = Trait {
.Output = Type
. `*` = Self.(R) -> .Output
}
.Output
重复。如果要同时实现这些多个Trait,请指定以下内容
P = Class {.x = Int; .y = Int}
# P|Self <: Add(P)|可简写为 P|<: Add(P)|
P|Self <: Add(P)|.
Output = P
`_+_` self, other = P.new {.x = self.x + other.x; .y = self.y + other.y}
P|Self <: Mul(Int)|.
Output = P
`*` self, other = P.new {.x = self.x * other; .y = self.y * other}
以这种方式实现的重复 API 在使用时几乎总是类型推断,但也可以通过使用 ||
显式指定类型来解决
print! P.Output # 类型错误: 不明确的类型
print! P|<: Mul(Int)|.Output # <class 'P'>
附录: 与 Rust Trait的区别
Erg 的Trait忠实于 [Schärli 等人] (https://www.ptidej.net/courses/ift6251/fall06/presentations/061122/061122.doc.pdf) 提出的Trait 为了允许代数运算,Trait被设计为不能有方法实现目录,但可以在必要时进行修补
Class
Erg 中的类大致是一种可以创建自己的元素(实例)的类型 这是一个简单类的示例
Person = Class {.name = Str; .age = Nat}
# 如果 `.new` 没有定义,那么 Erg 将创建 `Person.new = Person::__new__`
Person.
new name, age = Self::__new__ {.name = name; .age = age}
john = Person.new "John Smith", 25
print! john # <Person object>
print! classof(john) # Person
赋予"Class"的类型(通常是记录类型)称为需求类型(在本例中为"{.name = Str; .age = Nat}")
可以使用 <Class name>::__new__ {<attribute name> = <value>; 创建实例 ...}
可以创建
{.name = "约翰·史密斯"; .age = 25}
只是一条记录,但它通过传递 Person.new
转换为 Person
实例
创建此类实例的子例程称为构造函数
在上面的类中,.new
方法被定义为可以省略字段名等
请注意,以下不带换行符的定义将导致语法错误
Person.new name, age = ... # 语法错误: 不能直接在对象上定义属性
新类型符号
对于非记录类型T
',可以通过
' C = class T
定义类C
。这是一个简写符号,相当于C = Class {base = T}
。
这是为了简化所谓“新型模式”的定义。
同样,构造函数 __new__
/ new
可以直接传递给T
类型对象,而无需将其包装在记录中
Id = Class {base = Int}
i = Id.new {base = 1}
# ↓
Id = Class Int
i = Id.new 1
实例和类属性
在 Python 和其他语言中,实例属性通常在块侧定义如下,但请注意,这样的写法在 Erg 中具有不同的含义
# Python
class Person:
name: str
age: int
# 在Erg中,这个符号意味着类属性的声明(不是实例属性)
Person = Class()
Person.
name: Str
age: Int
# 以上 Python 代码的 Erg 代码
Person = Class {
.name = Str
.age = Nat
}
元素属性(在记录中定义的属性)和类型属性(也称为实例/类属性,尤其是在类的情况下)是完全不同的东西。类型属性是类型本身的属性。当一个类型的元素本身没有所需的属性时,它指的是一个类型属性。元素属性是元素直接拥有的唯一属性 为什么要进行这种区分? 如果所有属性都是元素属性,那么在创建对象时复制和初始化所有属性将是低效的 此外,以这种方式划分属性明确了诸如"该属性是共享的"和"该属性是分开持有的"之类的角色
下面的例子说明了这一点。species
属性对所有实例都是通用的,因此将其用作类属性更自然。但是,属性 name
应该是实例属性,因为每个实例都应该单独拥有它
Person = Class {name = Str}
Person::
species = "human"
Person.
describe() =
log "species: \{Person::species}"
greet self =
log "Hello, My name is \{self::name}."
Person.describe() # 类型: Person
Person.greet() # 类型错误: 未绑定的方法 Person.greet 需要一个参数
john = Person.new {name = "John"}
john.describe() # 类型: human
john.greet() # 你好,我是约翰
alice = Person.new {name = "Alice"}
alice.describe() # 类型: human
alice.greet() # 你好,我是爱丽丝
顺便说一下,如果实例属性和类型属性具有相同的名称和相同的类型,则会发生编译错误。这是为了避免混淆
C = Class {.i = Int}
C.i = 1 # 属性错误: `.i` 已在实例字段中定义
类(Class), 类型(Type)
请注意,1
的类和类型是不同的
只有一个类 Int
是 1
的生成器。可以通过classof(obj)
或obj.__class__
获取对象所属的类
相比之下,1
有无数种。例如,{1}, {0, 1}, 0..12, Nat, Int, Num
但是,可以将最小类型定义为单一类型,在本例中为"{1}"。可以通过Typeof(obj)
获取对象所属的类型。这是一个编译时函数
对象可以使用补丁方法以及类方法
Erg 不允许您添加类方法,但您可以使用 patch 来扩展类
您还可以从现有类(Inheritable 类)继承
您可以使用 Inherit
创建一个继承类。左侧的类型称为派生类,右侧的"继承"的参数类型称为基类(继承类)
MyStr = Inherit Str
# other: 如果你设置 ``other: Str'',你可以使用 MyStr
MyStr.
`-` self, other: Str = self.replace other, ""
abc = MyStr.new("abc")
# 这里的比较是向上的
assert abc - "b" == "ac"
与 Python 不同,默认情况下,定义的 Erg 类是 final
(不可继承的)
要使类可继承,必须将 Inheritable
装饰器附加到该类
Str` 是可继承的类之一
MyStr = Inherit Str # OK
MyStr2 = Inherit MyStr # NG
@Inheritable
InheritableMyStr = Inherit Str
MyStr3 = Inherit InheritableMyStr # OK
Inherit Obj
和 Class()
在实践中几乎是等价的。一般使用后者
类具有与类型不同的等价检查机制 类型基于其结构进行等效性测试
Person = {.name = Str; .age = Nat}
Human = {.name = Str; .age = Nat}
assert Person == Human
class has no equivalence relation defined.
Person = Class {.name = Str; .age = Nat}
Human = Class {.name = Str; .age = Nat}
Person == Human # 类型错误: 无法比较类
与结构类型的区别
我们说过类是一种可以生成自己的元素的类型,但这并不是严格的描述。事实上,一个记录类型+补丁可以做同样的事情
Person = {.name = Str; .age = Nat}
PersonImpl = Patch Person
PersonImpl.
new name, age = {.name; .age}
john = Person.new("John Smith", 25)
使用类有四个优点 第一个是构造函数经过有效性检查,第二个是它的性能更高,第三个是您可以使用符号子类型(NST),第四个是您可以继承和覆盖
我们之前看到记录类型 + 补丁也可以定义一个构造函数(某种意义上),但这当然不是一个合法的构造函数。这当然不是一个合法的构造函数,因为它可以返回一个完全不相关的对象,即使它调用自己.new
。在类的情况下,.new
被静态检查以查看它是否生成满足要求的对象
~
类的类型检查只是检查对象的。__class__
对象的属性。因此可以快速检查一个对象是否属于一个类型
~
Erg 在课堂上启用 NST; NST 的优点包括健壮性 在编写大型程序时,经常会出现对象的结构巧合匹配的情况
Dog = {.name = Str; .age = Nat}
DogImpl = Patch Dog
DogImpl.
bark = log "Yelp!"
...
Person = {.name = Str; .age = Nat}
PersonImpl = Patch Person
PersonImpl.
greet self = log "Hello, my name is \{self.name}."
john = {.name = "John Smith"; .age = 20}
john.bark() # "Yelp!"
Dog
和 Person
的结构完全一样,但让动物打招呼,让人类吠叫显然是无稽之谈
前者是不可能的,所以让它不适用更安全。在这种情况下,最好使用类
Dog = Class {.name = Str; .age = Nat}
Dog.bark = log "Yelp!"
...
Person = Class {.name = Str; .age = Nat}
Person.greet self = log "Hello, my name is \{self.name}."
john = Person.new {.name = "John Smith"; .age = 20}
john.bark() # 类型错误: `Person` 对象没有方法 `.bark`
另一个特点是补丁添加的类型属性是虚拟的,实现类不作为实体保存
也就是说,T.x
、T.bar
是可以通过与 {i = Int}
兼容的类型访问(编译时绑定)的对象,并且未在 {i = Int}
或 C
相反,类属性由类本身持有。因此,它们不能被不处于继承关系的类访问,即使它们具有相同的结构
C = Class {i = Int}
C.
foo self = ...
print! dir(C) # ["foo", ...].
T = Patch {i = Int}
T.
x = 1
bar self = ...
print! dir(T) # ["bar", "x", ...].
assert T.x == 1
assert {i = 1}.x == 1
print! T.bar # <函数 bar>
{i = Int}.bar # 类型错误: Record({i = Int}) 没有方法 `.bar`
C.bar # 类型错误: C 没有方法 `.bar` 打印!
print! {i = 1}.bar # <方法 bar>
C.new({i = 1}).bar # <方法 bar>
与数据类的区别
有两种类型的类: 常规类,通过Class
成为记录类,以及从记录类继承(Inherit
)的数据类
数据类继承了记录类的功能,具有分解赋值、默认实现的==
和hash
等特性。另一方面,数据类有自己的等价关系和格式展示
另一方面,如果要定义自己的等价关系或格式显示,则应使用普通类
C = Class {i = Int}
c = C.new {i = 1}
d = C.new {i = 2}
print! c # <C object>
c == d # 类型错误: `==` 没有为 `C` 实现
D = Inherit {i = Int}
e = D::{i = 1} # 与`e = D.new {i = 1}`相同
f = D::{i = 2}
print! e # D(i=1)
assert e ! = f
枚举类
为了便于定义"Or"类型的类,提供了一个"Enum"
X = Class()
Y = Class()
XorY = Enum X, Y
每种类型都可以通过XorY.X
、XorY.Y
来访问,构造函数可以通过X.new |> XorY.new
获得
x1 = XorY.new X.new()
x2 = (X.new |> XorY.new())()
x3 = (Y.new |> XorY.new())()
assert x1 == x2
assert x1 != x3
类关系
类是需求类型的子类型。类中可以使用需求类型的方法(包括补丁方法)
T = Trait {.foo = Foo}
C = Class(... , impl: T)
C.
foo = foo
bar x = ...
assert C < T
assert C.foo == foo
assert not T < C
assert T.foo == Foo
继承
继承允许您定义一个新类,为现有类添加功能或专业化 继承类似于包含在Trait中。继承的类成为原始类的子类型
NewInt = Inherit Int
NewInt.
plus1 self = self + 1
assert NewInt.new(1).plus1() == 2
assert NewInt.new(1) + NewInt.new(1) == 2
如果你希望新定义的类是可继承的,你必须给它一个 Inheritable
装饰器
您可以指定一个可选参数 additional
以允许该类具有其他实例属性,但前提是该类是一个值类。但是,如果类是值类,则不能添加实例属性
@Inheritable
Person = Class {name = Str}
Student = Inherit Person, additional: {id = Int}
john = Person.new {name = "John"}
alice = Student.new {name = "Alice", id = 123}
MailAddress = Inherit Str, additional: {owner = Str} # 类型错误: 实例变量不能添加到值类中
Erg 的特殊设计不允许继承"Never"类型。Erg 的特殊设计不允许继承 Never
类型,因为 Never
是一个永远无法实例化的独特类
枚举类的继承
Or 类型 也可以被继承。在这种情况下,您可以通过指定可选参数 Excluding
来删除任何选项(可以使用 or
进行多项选择)
不能添加其他选项。添加选项的类不是原始类的子类型
Number = Class Int or Float or Complex
Number.abs(self): Float =
match self:
i: Int -> i.abs().into Float
f: Float -> f.abs()
c: Complex -> c.abs().into Float
# c: 复杂不能出现在匹配选项中
RealNumber = Inherit Number, Excluding: Complex
同样,也可以指定细化类型
Months = Class 0..12
MonthsNot31Days = Inherit Months, Excluding: {1, 3, 5, 7, 8, 10, 12}
StrMoreThan3 = Class StrWithLen N | N >= 3
StrMoreThan4 = Inherit StrMoreThan3, Excluding: StrWithLen N | N == 3
覆盖
该类与补丁相同,可以将新方法添加到原始类型,但可以进一步"覆盖"该类
这种覆盖称为覆盖。要覆盖,必须满足三个条件
首先,覆盖必须有一个 Override
装饰器,因为默认情况下它会导致错误
另外,覆盖不能改变方法的类型。它必须是原始类型的子类型
如果你重写了一个被另一个方法引用的方法,你也必须重写所有被引用的方法
为什么这个条件是必要的?这是因为重写不仅会改变一种方法的行为,而且可能会影响另一种方法的行为
让我们从第一个条件开始。此条件是为了防止"意外覆盖"
换句话说,必须使用 Override
装饰器来防止派生类中新定义的方法的名称与基类的名称冲突
接下来,考虑第二个条件。这是为了类型一致性。由于派生类是基类的子类型,因此它的行为也必须与基类的行为兼容
最后,考虑第三个条件。这种情况是 Erg 独有的,在其他面向对象语言中并不常见,同样是为了安全。让我们看看如果不是这种情况会出现什么问题
# 反面示例
@Inheritable
Base! = Class {x = Int!}
Base!
f! ref! self =
print! self::x
self.g!()
g! ref! self = self::x.update! x -> x + 1
Inherited! = Inherit Base!
Inherited!
@Override
g! ref! self = self.f!() # 无限递归警告: 此代码陷入无限循环
# 覆盖错误: 方法 `.g` 被 `.f` 引用但未被覆盖
在继承类 Inherited!
中,.g!
方法被重写以将处理转移到 .f!
。但是,基类中的 .f!
方法会将其处理转移到 .g!
,从而导致无限循环。.f
是 Base!
类中的一个没有问题的方法,但它被覆盖以一种意想不到的方式使用,并且被破坏了
Erg 已将此规则构建到规范中
# OK.
@Inheritable
Base! = Class {x = Int!}
Base!
f! ref! self =
print! self::x
self.g!()
g! ref! self = self::x.update! x -> x + 1
Inherited! = Inherit Base!
Inherited!
@Override
f! ref! self =
print! self::x
self::x.update! x -> x + 1
@Override
g! ref! self = self.f!()
然而,这个规范并没有完全解决覆盖问题。然而,这个规范并没有完全解决覆盖问题,因为编译器无法检测覆盖是否解决了问题 创建派生类的程序员有责任纠正覆盖的影响。只要有可能,尝试定义一个别名方法
替换Trait(或看起来像什么)
尽管无法在继承时替换Trait,但有一些示例似乎可以这样做
例如,Int
,Real
的子类型(实现了 Add()
),似乎重新实现了 Add()
Int = Class ... , Impl := Add() and ...
但实际上 Real
中的 Add()
代表 Add(Real, Real)
,而在 Int
中它只是被 Add(Int, Int)
覆盖
它们是两个不同的Trait(Add
是一个 covariate,所以Add(Real, Real) :> Add(Int, Int)
)
多重继承
Erg 不允许普通类之间的交集、差异和互补
Int and Str # 类型错误: 无法合并类
该规则防止从多个类继承,即多重继承
IntAndStr = Inherit Int and Str # 语法错误: 不允许类的多重继承
但是,可以使用多个继承的 Python 类
多层(多级)继承
Erg 继承也禁止多层继承。也就是说,您不能定义从另一个类继承的类 从"Object"继承的可继承类可能会异常继承
同样在这种情况下,可以使用 Python 的多层继承类
重写继承的属性
Erg 不允许重写从基类继承的属性。这有两个含义
第一个是对继承的源类属性的更新操作。例如,它不能重新分配,也不能通过 .update!
方法更新
覆盖与重写不同,因为它是一种用更专业的方法覆盖的操作。覆盖也必须替换为兼容的类型
@Inheritable
Base! = Class {.pub = !Int; pri = !Int}
Base!
var = !1
inc_pub! ref! self = self.pub.update! p -> p + 1
Inherited! = Inherit Base!
Inherited!
var.update! v -> v + 1
# 类型错误: 不能更新基类变量
@Override
inc_pub! ref! self = self.pub + 1
# 覆盖错误: `.inc_pub!` 必须是 `Self! 的子类型! () => ()`
第二个是对继承源的(变量)实例属性的更新操作。这也是被禁止的。基类的实例属性只能从基类提供的方法中更新 无论属性的可见性如何,都无法直接更新。但是,它们可以被读取
@Inheritable
Base! = Class {.pub = !Int; pri = !Int}
Base!
inc_pub! ref! self = self.pub.update! p -> p + 1
inc_pri! ref! self = self::pri.update! p -> p + 1
self = self.pub.update!
Inherited!
# OK
add2_pub! ref! self =
self.inc_pub!()
self.inc_pub!()
# NG, `Child` 不能触摸 `self.pub` 和 `self::pri`
add2_pub! ref! self =
self.pub.update! p -> p + 2
毕竟 Erg 继承只能添加新的属性和覆盖基类的方法
使用继承
虽然继承在正确使用时是一项强大的功能,但它也有一个缺点,即它往往会使类依赖关系复杂化,尤其是在使用多层或多层继承时。复杂的依赖关系会降低代码的可维护性 Erg 禁止多重和多层继承的原因是为了降低这种风险,并且引入了类补丁功能以降低依赖关系的复杂性,同时保留继承的"添加功能"方面
那么,反过来说,应该在哪里使用继承呢?一个指标是何时需要"基类的语义子类型"
Erg 允许类型系统自动进行部分子类型确定(例如,Nat,其中 Int 大于或等于 0)
但是,例如,仅依靠 Erg 的类型系统很难创建"表示有效电子邮件地址的字符串类型"。您可能应该对普通字符串执行验证。然后,我们想为已通过验证的字符串对象添加某种"保证"。这相当于向下转换为继承的类。将 Str object
向下转换为 ValidMailAddressStr
与验证字符串是否采用正确的电子邮件地址格式是一一对应的
ValidMailAddressStr = Inherit Str
ValidMailAddressStr.
init s: Str =
validate s # 邮件地址验证
Self.new s
s1 = "invalid mail address"
s2 = "foo@gmail.com"
_ = ValidMailAddressStr.init s1 # 恐慌: 无效的邮件地址
valid = ValidMailAddressStr.init s2
valid: ValidMailAddressStr # 确保电子邮件地址格式正确
另一个指标是您何时想要实现名义多态性
例如,下面定义的 greet!
过程将接受任何类型为 Named
的对象
但显然应用 Dog
类型的对象是错误的。所以我们将使用 Person
类作为参数类型
这样,只有 Person
对象、从它们继承的类和 Student
对象将被接受为参数
这是比较保守的,避免不必要地承担过多的责任
Named = {name = Str; ...}
Dog = Class {name = Str; breed = Str}
Person = Class {name = Str}
Student = Inherit Person, additional: {id = Int}
structural_greet! person: Named =
print! "Hello, my name is \{person::name}."
greet! person: Person =
print! "Hello, my name is \{person::name}."
max = Dog.new {name = "Max", breed = "Labrador"}
john = Person.new {name = "John"}
alice = Student.new {name = "Alice", id = 123}
structural_greet! max # 你好,我是马克斯
structural_greet! john # 你好,我是约翰
greet! alice # 你好,我是爱丽丝
greet! max # 类型错误:
NST 与 SST
Months = 0..12
# NST
MonthsClass = Class Months
MonthsClass.
name self =
match self:
1 -> "january"
2 -> "february"
3 -> "march"
...
# SST
MonthsImpl = Patch Months
MonthsImpl.
name self =
match self:
1 -> "January"
2 -> "February"
3 -> "March"
...
assert 12 in Months
assert 2.name() == "February"
assert not 12 in MonthsClass
assert MonthsClass.new(12) in MonthsClass
# 它可以使用结构类型,即使包装在一个类中
assert MonthsClass.new(12) in Months
# 如果两者都存在,则类方法优先
assert MonthsClass.new(2).name() == "february"
最后,我应该使用哪个,NST 还是 SST?
如果您无法决定使用哪一个,我们的建议是 NST SST 需要抽象技能来编写在任何用例中都不会崩溃的代码。好的抽象可以带来高生产力,但错误的抽象(外观上的共性)会导致适得其反的结果。(NST 可以通过故意将抽象保持在最低限度来降低这种风险。如果您不是库实现者,那么仅使用 NST 进行编码并不是一个坏主意
补丁
Erg 不允许修改现有类型和类 这意味着,不可能在类中定义额外的方法,也不能执行特化(一种语言特性,单态化多态声明的类型并定义专用方法,如在 C++ 中) 但是,在许多情况下,您可能希望向现有类型或类添加功能,并且有一个称为"修补"的功能允许您执行此操作
StrReverse = Patch Str
StrReverse.
reverse self = self.iter().rev().collect(Str)
assert "abc".reverse() == "cba"
补丁的名称应该是要添加的主要功能的简单描述
这样,被修补类型的对象(Str
)可以使用修补程序的方法(StrReverse
)
实际上,内置方法.reverse
并不是Str
的方法,而是StrRReverse
中添加的方法
但是,补丁方法的优先级低于名义类型(类/trait)的方法,并且不能覆盖现有类型
StrangeInt = Patch Int
StrangeInt.
`_+_` = Int.`_-_` # 赋值错误: . `_+_` 已在 Int 中定义
如果要覆盖,则必须从类继承 但是,基本上建议不要覆盖并定义具有不同名称的方法 由于一些安全限制,覆盖不是很容易做到
StrangeInt = Inherit Int
StrangeInt.
# 覆盖方法必须被赋予覆盖装饰器
# 另外,你需要覆盖所有依赖于 Int.`_+_` 的 Int 方法
@Override
`_+_` = Super.`_-_` # OverrideError: Int.`_+_` 被 ... 引用,所以这些方法也必须被覆盖
选择修补程序
可以为单一类型定义修复程序,并且可以组合在一起
# foo.er
StrReverse = Patch(Str)
StrReverse.
reverse self = ...
StrMultiReplace = Patch(Str)
StrMultiReverse.
multi_replace self, pattern_and_targets: [(Pattern, Str)] = ...
StrToCamelCase = Patch(Str)
StrToCamelCase.
to_camel_case self = ...
StrToKebabCase = Patch(Str)
StrToKebabCase.
to_kebab_case self = ...
StrBoosterPack = StrReverse and StrMultiReplace and StrToCamelCase and StrToKebabCase
StrBoosterPack = StrReverse and StrMultiReplace and StrToCamelCase and StrToKebabCase
{StrBoosterPack;} = import "foo"
assert "abc".reverse() == "cba"
assert "abc".multi_replace([("a", "A"), ("b", "B")]) == "ABc"
assert "to camel case".to_camel_case() == "toCamelCase"
assert "to kebab case".to_kebab_case() == "to-kebab-case"
如果定义了多个修复程序,其中一些可能会导致重复实施
# foo.er
StrReverse = Patch(Str)
StrReverse.
reverse self = ...
# 更高效的实现
StrReverseMk2 = Patch(Str)
StrReverseMk2.
reverse self = ...
"hello".reverse() # 补丁选择错误: `.reverse` 的多个选择: StrReverse, StrReverseMk2
在这种情况下,您可以使用 related function 形式而不是方法形式使其唯一
assert StrReverseMk2.reverse("hello") == "olleh"
You can also make it unique by selectively importing.
{StrReverseMk2;} = import "foo"
assert "hello".reverse() == "olleh"
胶水补丁
维修程序也可以将类型相互关联。StrReverse
补丁涉及 Str
和 Reverse
这样的补丁称为 glue patch
因为 Str
是内置类型,所以用户需要使用胶水补丁来改造Trait
Reverse = Trait {
.reverse = Self.() -> Self
}
StrReverse = Patch Str, Impl := Reverse
StrReverse.
reverse self =
self.iter().rev().collect(Str)
每个类型/Trait对只能定义一个胶水补丁 这是因为如果多个胶水修复程序同时"可见",就不可能唯一确定选择哪个实现 但是,当移动到另一个范围(模块)时,您可以交换维修程序
NumericStr = Inherit Str
NumericStr.
...
NumStrRev = Patch NumericStr, Impl := Reverse
NumStrRev.
...
# 重复修补程序错误: 数值Str已与"反向"关联`
# 提示: 'Str'(NumericStr'的父类)通过'StrReverse'与'Reverse'关联
附录: 与 Rust Trait的关系
Erg 修复程序相当于 Rust 的(改造的)impl
块
#![allow(unused)] fn main() { // Rust trait Reverse { fn reverse(self) -> Self; } impl Reverse for String { fn reverse(self) -> Self { self.chars().rev().collect() } } }
可以说,Rust 的Trait是 Erg 的Trait和修复程序的Trait。这使得 Rust 的Trait听起来更方便,但事实并非如此
# Erg
Reverse = Trait {
.reverse = Self.() -> Self
}
StrReverse = Patch(Str, Impl := Reverse)
StrReverse.
reverse self =
self.iter().rev().collect(Str)
因为 impl 块在 Erg 中被对象化为补丁,所以在从其他模块导入时可以选择性地包含。作为副作用,它还允许将外部Trait实现到外部结构
此外,结构类型不再需要诸如 dyn trait
和 impl trait
之类的语法
# Erg
reversible: [Reverse; 2] = [[1, 2, 3], "hello"]
iter|T|(i: Iterable T): Iterator T = i.iter()
#![allow(unused)] fn main() { // Rust let reversible: [Box<dyn Reverse>; 2] = [Box::new([1, 2, 3]), Box::new("hello")]; fn iter<I>(i: I) -> impl Iterator<Item = I::Item> where I: IntoIterator { i.into_iter() } }
通用补丁
补丁不仅可以为一种特定类型定义,还可以为"一般功能类型"等定义
在这种情况下,要给出自由度的项作为参数给出(在下面的情况下,T: Type
)。以这种方式定义的补丁称为全对称补丁
如您所见,全对称补丁正是一个返回补丁的函数,但它本身也可以被视为补丁
FnType T: Type = Patch(T -> T)
FnType(T).
type = T
assert (Int -> Int).type == Int
结构补丁
此外,可以为满足特定结构的任何类型定义修复程序 但是,这比名义上的维修程序和类方法具有较低的优先级
在定义结构修复程序时应使用仔细的设计,因为某些属性会因扩展而丢失,例如以下内容
# 这不应该是 `Structural`
Norm = Structural Patch {x = Int; y = Int}
Norm.
norm self = self::x**2 + self::y**2
Point2D = Class {x = Int; y = Int}
assert Point2D.new({x = 1; y = 2}).norm() == 5
Point3D = Class {x = Int; y = Int; z = Int}
assert Point3D.new({x = 1; y = 2; z = 3}).norm() == 14 # AssertionError:
值类型
值类型是可以在编译时评估的 Erg 内置类型,具体来说:
Value = (
Int
or Nat
or Ratio
or Float
or Complex
or Bool
or Str
or NoneType
or Array Const
or Tuple Const
or Set Const
or ConstFunc(Const, _)
or ConstProc(Const, _)
or ConstMethod(Const, _)
)
应用于它们的值类型对象、常量和编译时子例程称为 constant 表达式
1, 1.0, 1+2im, True, None, "aaa", [1, 2, 3], Fib(12)
小心子程序。子例程可能是也可能不是值类型 由于子程序的实质只是一个指针,因此可以将其视为一个值1,但是在编译不是子程序的东西时不能使用 在恒定的上下文中。不是值类型,因为它没有多大意义
将来可能会添加归类为值类型的类型
1 Erg 中的术语"值类型"与其他语言中的定义不同。纯 Erg 语义中没有内存的概念,并且因为它被放置在堆栈上而说它是值类型,或者因为它实际上是一个指针而说它不是值类型是不正确的。值类型仅表示它是"值"类型或其子类型。↩
属性类型
属性类型是包含 Record 和 Dataclass、Patch、Module 等的类型 属于属性类型的类型不是值类型
记录类型复合
可以展平复合的记录类型
例如,{... {.name = Str; .age = Nat}; ... {.name = Str; .id = Nat}}
变成 {.name = Str; .age = 自然; .id = Nat}
区间类型
Range
对象最基本的用途是作为迭代器
for! 0..9, i =>
print! i
请注意,与 Python 不同,它包含一个结束编号
然而,这不仅仅用于 Range
对象。也可以使用类型。这种类型称为Interval类型
i: 0..10 = 2
Nat
类型等价于 0..<Inf
并且,Int
和 Ratio
等价于 -Inf<..<Inf
,
0..<Inf
也可以写成 0.._
。_
表示任何 Int
类型的实例
由于它也可以用作迭代器,所以可以倒序指定,例如10..0
,但是<..
、..<
和<..<
不能倒序
a = 0..10 # OK
b = 0..<10 # OK
c = 10..0 # OK
d = 10<..0 # 语法错误
e = 10..<0 # 语法错误
f = 10<..<0 # 语法错误
Range 运算符可用于非数字类型,只要它们是"Ord"不可变类型
Alphabet = "A".."z"
枚举类型
Set 生成的枚举类型 枚举类型可以与类型规范一起使用,但可以通过将它们分类为类或定义修复程序来定义进一步的方法
具有枚举类型的部分类型系统称为枚举部分类型
Bool = {True, False}
Status = {"ok", "error"}
由于 1..7
可以重写为 {1, 2, 3, 4, 5, 6, 7}
,所以当元素是有限的时,Enum 类型本质上等同于 Range 类型
Binary! = Class {0, 1}!.
invert! ref! self =
if! self == 0:
do!:
self.set! 1
do!:
self.set! 0
b = Binary!.new !0
b.invert!()
顺便说一下,Erg 的 Enum 类型是一个包含其他语言中常见的枚举类型的概念
#![allow(unused)] fn main() { // Rust enum Status { Ok, Error } }
# Erg
Status = {"Ok", "Error"}
Rust 的不同之处在于它使用了结构子类型(SST)
#![allow(unused)] fn main() { // Status 和 ExtraStatus 之间没有关系 enum Status { Ok, Error } enum ExtraStatus { Ok, Error, Unknown } // 可实施的方法 impl Status { // ... } impl ExtraStatus { // ... } }
# Status > ExtraStatus,Status的元素可以使用ExtraStatus的方法
Status = Trait {"Ok", "Error"}
# ...
ExtraStatus = Trait {"Ok", "Error", "Unknown"}
# ...
方法也可以通过补丁添加
使用"或"运算符明确指示包含或向现有 Enum 类型添加选项
ExtraStatus = Status or {"Unknown"}
一个元素所属的所有类都相同的枚举类型称为同质枚举类型
默认情况下,可以将需求类型为同类枚举类型的类视为元素所属类的子类
如果您不想这样做,可以将其设为包装类
Abc = Class {"A", "B", "C"}
Abc.new("A").is_uppercase()
OpaqueAbc = Class {inner = {"A", "B", "C"}}.
new inner: {"A", "B", "C"} = Self.new {inner;}
OpaqueAbc.new("A").is_uppercase() # 类型错误
细化类型
细化类型是受谓词表达式约束的类型。枚举类型和区间类型是细化类型的语法糖
细化类型的标准形式是{Elem: Type | (Pred)*}
。这意味着该类型是其元素为满足 Pred
的 Elem
的类型
可用于筛选类型的类型仅为 数值类型
Nat = 0.. _
Odd = {N: Int | N % 2 == 1}
Char = StrWithLen 1
# StrWithLen 1 == {_: StrWithLen N | N == 1}
[Int; 3] == {_: Array Int, N | N == 3}
Array3OrMore == {A: Array _, N | N >= 3}
当有多个 pred 时,可以用 ;
或 and
或 or
分隔。;
和 and
的意思是一样的
Odd
的元素是 1, 3, 5, 7, 9, ...
它被称为细化类型,因为它的元素是现有类型的一部分,就好像它是细化一样
Pred
被称为(左侧)谓词表达式。和赋值表达式一样,它不返回有意义的值,左侧只能放置一个模式
也就是说,诸如X**2 - 5X + 6 == 0
之类的表达式不能用作细化类型的谓词表达式。在这方面,它不同于右侧的谓词表达式
{X: Int | X**2 - 5X + 6 == 0} # 语法错误: 谓词形式无效。只有名字可以在左边
如果你知道如何解二次方程,你会期望上面的细化形式等价于{2, 3}
但是,Erg 编译器对代数的了解很少,因此无法解决右边的谓词
智能投射
很高兴您定义了 Odd
,但事实上,它看起来不能在文字之外使用太多。要将普通 Int
对象中的奇数提升为 Odd
,即将 Int
向下转换为 Odd
,您需要传递 Odd
的构造函数
对于细化类型,普通构造函数 .new
可能会出现恐慌,并且有一个名为 .try_new
的辅助构造函数返回一个 Result
类型
i = Odd.new (0..10).sample!()
i: Odd # or Panic
它也可以用作 match
中的类型说明
# i: 0..10
i = (0..10).sample!
match i:
o: Odd ->
log "i: Odd"
n: Nat -> # 0..10 < Nat
log "i: Nat"
但是,Erg 目前无法做出诸如"偶数"之类的子决策,因为它不是"奇数"等
枚举、区间和筛选类型
前面介绍的枚举/区间类型是细化类型的语法糖
{a, b, ...}
是 {I: Typeof(a) | I == a 或 I == b 或 ... }
,并且 a..b
被去糖化为 {I: Typeof(a) | 我 >= a 和我 <= b}
{1, 2} == {I: Int | I == 1 or I == 2}
1..10 == {I: Int | I >= 1 and I <= 10}
1... <10 == {I: Int | I >= 1 and I < 10}
细化模式
正如 _: {X}
可以重写为 X
(常量模式),_: {X: T | Pred}
可以重写为X: T | Pred
# 方法 `.m` 是为长度为 3 或更大的数组定义的
Array(T, N | N >= 3)
.m(&self) = ...
代数类型
代数类型是通过将类型视为代数来操作类型而生成的类型 它们处理的操作包括Union、Intersection、Diff、Complement等 普通类只能进行Union,其他操作会导致类型错误
联合(Union)
联合类型可以为类型提供多种可能性。顾名思义,它们是由"或"运算符生成的
一个典型的 Union 是 Option
类型。Option
类型是 T 或 NoneType
补丁类型,主要表示可能失败的值
IntOrStr = Int or Str
assert dict.get("some key") in (Int or NoneType)
Option T = T or NoneType
请注意,联合类型是可交换的,但不是关联的。也就是说,X or Y or Z
是(X or Y) or Z
,而不是X or (Y or Z)
允许这样做会导致,例如,Int 或 Option(Str)
、Option(Int) 或 Str
和Option(Int or Str)
属于同一类型
路口
交集类型是通过将类型与 and
操作组合得到的
Num = Add and Sub and Mul and Eq
如上所述,普通类不能与"and"操作结合使用。这是因为实例只属于一个类
差异
Diff 类型是通过 not
操作获得的
最好使用 and not
作为更接近英文文本的符号,但建议只使用 not
,因为它更适合与 and
和 or
一起使用
CompleteNum = Add and Sub and Mul and Div and Eq and Ord
Num = CompleteNum not Div not Ord
True = Bool not {False}
OneTwoThree = {1, 2, 3, 4, 5, 6} - {4, 5, 6, 7, 8, 9, 10}
补充
补码类型是通过 not
操作得到的,这是一个一元操作。not T
类型是 {=} not T
的简写
类型为"非 T"的交集等价于 Diff,类型为"非 T"的 Diff 等价于交集
但是,不推荐这种写法
# 非零数类型的最简单定义
NonZero = Not {0}
# 不推荐使用的样式
{True} == Bool and not {False} # 1 == 2 + - 1
Bool == {True} not not {False} # 2 == 1 - -1
真代数类型
有两种代数类型: 可以简化的表观代数类型和不能进一步简化的真实代数类型
"表观代数类型"包括 Enum、Interval 和 Record 类型的 or
和 and
这些不是真正的代数类型,因为它们被简化了,并且将它们用作类型说明符将导致警告; 要消除警告,您必须简化它们或定义它们的类型
assert {1, 2, 3} or {2, 3} == {1, 2, 3}
assert {1, 2, 3} and {2, 3} == {2, 3}
assert -2..-1 or 1..2 == {-2, -1, 1, 2}
i: {1, 2} or {3, 4} = 1 # 类型警告: {1, 2} 或 {3, 4} 可以简化为 {1, 2, 3, 4}
p: {x = Int, ...} and {y = Int; ...} = {x = 1; y = 2; z = 3}
# 类型警告: {x = Int, ...} 和 {y = Int; ...} 可以简化为 {x = Int; y = 整数; ...}
Point1D = {x = Int; ...}
Point2D = Point1D and {y = Int; ...} # == {x = Int; y = Int; ...}
q: Point2D = {x = 1; y = 2; z = 3}
真正的代数类型包括类型"或"和"与"。类之间的"或"等类属于"或"类型
assert Int or Str == Or(Int, Str)
assert Int and Marker == And(Int, Marker)
Diff, Complement 类型不是真正的代数类型,因为它们总是可以被简化
依赖类型
依赖类型是一个特性,可以说是 Erg 的最大特性 依赖类型是将值作为参数的类型。普通的多态类型只能将类型作为参数,但依赖类型放宽了这个限制
依赖类型等价于[T; N]
(数组(T,N)
)
这种类型不仅取决于内容类型"T",还取决于内容数量"N"。N
包含一个Nat
类型的对象
a1 = [1, 2, 3]
assert a1 in [Nat; 3]
a2 = [4, 5, 6, 7]
assert a1 in [Nat; 4]
assert a1 + a2 in [Nat; 7]
如果函数参数中传递的类型对象与返回类型有关,则写:
narray: |N: Nat| {N} -> [{N}; N]
narray(N: Nat): [N; N] = [N; N]
assert array(3) == [3, 3, 3]
定义依赖类型时,所有类型参数都必须是常量
依赖类型本身存在于现有语言中,但 Erg 具有在依赖类型上定义过程方法的特性
x=1
f x =
print! f::x, module::x
# Phantom 类型有一个名为 Phantom 的属性,其值与类型参数相同
T X: Int = Class Impl := Phantom X
T(X).
x self = self::Phantom
T(1).x() # 1
可变依赖类型的类型参数可以通过方法应用程序进行转换
转换规范是用 ~>
完成的
# 注意 `Id` 是不可变类型,不能转换
VM!(State: {"stopped", "running"}! := _, Id: Nat := _) = Class(..., Impl := Phantom! State)
VM!().
# 不改变的变量可以通过传递`_`省略
start! ref! self("stopped" ~> "running") =
self.initialize_something!()
self::set_phantom!("running")
# 你也可以按类型参数切出(仅在定义它的模块中)
VM!.new() = VM!(!"stopped", 1).new()
VM!("running" ~> "running").stop!ref!self =
self.close_something!()
self::set_phantom!("stopped")
vm = VM!.new()
vm.start!()
vm.stop!()
vm.stop!() # 类型错误: VM!(!"stopped", 1) 没有 .stop!()
# 提示: VM!(!"running", 1) 有 .stop!()
您还可以嵌入或继承现有类型以创建依赖类型
MyArray(T, N) = Inherit[T; N]
# self 的类型: Self(T, N) 与 .array 一起变化
MyStruct!(T, N: Nat!) = Class {.array: [T; !N]}
类型变量
类型变量是用于例如指定子程序参数类型的变量,它的类型是任意的(不是单态的)
首先,作为引入类型变量的动机,考虑 id
函数,它按原样返回输入
id x: Int = x
返回输入的"id"函数是为"Int"类型定义的,但这个函数显然可以为任何类型定义
让我们使用 Object
来表示最大的类
id x: Object = x
i = id 1
s = id "foo"
b = id True
当然,它现在接受任意类型,但有一个问题: 返回类型被扩展为 Object
。返回类型扩展为 Object
如果输入是"Int"类型,我想查看返回类型"Int",如果输入是"Str"类型,我想查看"Str"
print! id 1 # <Object object>
id(1) + 1 # 类型错误: 无法添加 `Object` 和 `Int
要确保输入的类型与返回值的类型相同,请使用 type 变量
类型变量在||
(类型变量列表)中声明
id|T: Type| x: T = x
assert id(1) == 1
assert id("foo") == "foo"
assert id(True) == True
这称为函数的 universal quantification(泛化)。有细微的差别,但它对应于其他语言中称为泛型的函数。泛化函数称为__多态函数__ 定义一个多态函数就像为所有类型定义一个相同形式的函数(Erg 禁止重载,所以下面的代码真的不能写)
id|T: Type| x: T = x
# 伪代码
id x: Int = x
id x: Str = x
id x: Bool = x
id x: Ratio = x
id x: NoneType = x
...
此外,类型变量"T"可以推断为"Type"类型,因为它在类型规范中使用。所以 |T: Type|
可以简单地缩写为 |T|
你也可以省略|T, N| 脚; N]
如果可以推断它不是类型对象(T: Type, N: Nat
)
如果类型对于任意类型来说太大,您也可以提供约束 约束也有优势,例如,子类型规范允许使用某些方法
# T <: Add
# => T 是 Add 的子类
# => 可以做加法
add|T <: Add| l: T, r: T = l + r
在本例中,T
必须是Add
类型的子类,并且要分配的l
和r
的实际类型必须相同
在这种情况下,"T"由"Int"、"Ratio"等满足。因此,例如,"Int"和"Str"的添加没有定义,因此被拒绝
您也可以像这样键入它
f|
Y, Z: Type
X <: Add Y, O1
O1 <: Add Z, O2
O2 <: Add X, _
| x: X, y: Y, z: Z =
x + y + z + x
如果注释列表很长,您可能需要预先声明它
f: |Y, Z: Type, X <: Add(Y, O1), O1 <: Add(Z, O2), O2 <: Add(X, O3)| (X, Y, Z) -> O3
f|X, Y, Z| x: X, y: Y, z: Z =
x + y + z + x
与许多具有泛型的语言不同,所有声明的类型变量都必须在临时参数列表(x: X, y: Y, z: Z
部分)或其他类型变量的参数中使用
这是 Erg 语言设计的一个要求,即所有类型变量都可以从真实参数中推断出来
因此,无法推断的信息,例如返回类型,是从真实参数传递的; Erg 允许从实参传递类型
Iterator T = Trait {
# 从参数传递返回类型
# .collect: |K: Type -> Type| Self(T). ({K}) -> K(T)
.collect(self, K: Type -> Type): K(T) = ...
...
}
it = [1, 2, 3].iter().map i -> i + 1
it.collect(Array) # [2, 3, 4].
类型变量只能在 ||
期间声明。但是,一旦声明,它们就可以在任何地方使用,直到它们退出作用域
f|X|(x: X): () =
y: X = x.clone()
log X.__name__
log X
f 1
# Int
# <class Int>
您也可以在使用时明确单相如下
f: Int -> Int = id|Int|
在这种情况下,指定的类型优先于实际参数的类型(匹配失败将导致类型错误,即实际参数的类型错误) 即如果传递的实际对象可以转换为指定的类型,则进行转换; 否则会导致编译错误
assert id(1) == 1
assert id|Int|(1) in Int
assert id|Ratio|(1) in Ratio
# 你也可以使用关键字参数
assert id|T: Int|(1) == 1
id|Int|("str") # 类型错误: id|Int| is type `Int -> Int`但得到了 Str
当此语法与理解相冲突时,您需要将其括在 ()
中
# {id|Int| x | x <- 1..10} 将被解释为 {id | ...}
{(id|Int| x) | x <- 1..10}
不能使用与已存在的类型相同的名称来声明类型变量。这是因为所有类型变量都是常量
I: Type
# ↓ 无效类型变量,已经存在
f|I: Type| ... = ...
在方法定义中输入参数
默认情况下,左侧的类型参数被视为绑定变量
K(T: Type, N: Nat) = ...
K(T, N).
foo(x) = ...
使用另一个类型变量名称将导致警告
K(T: Type, N: Nat) = ...
K(U, M). # 警告: K 的类型变量名是 'T' 和 'N'
foo(x) = ...
自定义以来,所有命名空间中的常量都是相同的,因此它们当然不能用于类型变量名称
N = 1
K(N: Nat) = ... # 名称错误: N 已定义
L(M: Nat) = ...
# 仅当 M == N == 1 时才定义
L(N).
foo(self, x) = ...
# 为任何定义 M: Nat
L(M).
.bar(self, x) = ...
每个类型参数不能有多个定义,但可以定义具有相同名称的方法,因为未分配类型参数的依赖类型(非原始类型)和分配的依赖类型(原始类型)之间没有关系 )
K(I: Int) = ...
K.
# K 不是真正的类型(atomic Kind),所以我们不能定义方法
# 这不是方法(更像是静态方法)
foo(x) = ...
K(0).
foo(self, x): Nat = ...
For-all类型
上一节中定义的 id
函数是一个可以是任何类型的函数。那么 id
函数本身的类型是什么?
print! classof(id) # |T: Type| T -> T
我们得到一个类型|T: Type| T -> T
。这称为一个 封闭的全称量化类型/全称类型,即['a. ...]'
在 ML 和 forall t. ...
在 Haskell 中。为什么使用形容词"关闭"将在下面讨论
封闭的全称量化类型有一个限制: 只有子程序类型可以被通用量化,即只有子程序类型可以放在左子句中。但这已经足够了,因为子程序是 Erg 中最基本的控制结构,所以当我们说"我要处理任意 X"时,即我想要一个可以处理任意 X 的子程序。所以,量化类型具有相同的含义 作为多态函数类型。从现在开始,这种类型基本上被称为多态函数类型
与匿名函数一样,多态类型具有任意类型变量名称,但它们都具有相同的值
assert (|T: Type| T -> T) == (|U: Type| U -> U)
当存在 alpha 等价时,等式得到满足,就像在 lambda 演算中一样。由于对类型的操作有一些限制,所以总是可以确定等价的(如果我们不考虑 stoppage 属性)
多态函数类型的子类型化
多态函数类型可以是任何函数类型。这意味着与任何函数类型都存在子类型关系。让我们详细看看这种关系
类型变量在左侧定义并在右侧使用的类型,例如 OpenFn T: Type = T -> T
,称为 open 通用类型
相反,在右侧定义和使用类型变量的类型,例如 ClosedFn = |T: Type| T -> T
,被称为 封闭的通用类型
开放通用类型是所有同构"真"类型的父类型。相反,封闭的通用类型是所有同构真类型的子类型
(|T: Type| T -> T) < (Int -> Int) < (T -> T)
您可能还记得封闭的较小/开放的较大 但为什么会这样呢? 为了更好地理解,让我们考虑每个实例
# id: |T: Type| T -> T
id|T|(x: T): T = x
# iid: Int -> Int
iid(x: Int): Int = x
# 按原样返回任意函数
id_arbitrary_fn|T|(f1: T -> T): (T -> T) = f
# id_arbitrary_fn(id) == id
# id_arbitrary_fn(iid) == iid
# return the poly correlation number as it is
id_poly_fn(f2: (|T| T -> T)): (|T| T -> T) = f
# id_poly_fn(id) == id
id_poly_fn(iid) # 类型错误
# 按原样返回 Int 类型函数
id_int_fn(f3: Int -> Int): (Int -> Int) = f
# id_int_fn(id) == id|Int|
# id_int_fn(iid) == iid
由于 id
是 |T: Type| 类型T -> T
,可以赋值给Int-> Int
类型的参数f3
,我们可以考虑(|T| T -> T) < (Int -> Int)
反之,Int -> Int
类型的iid
不能赋值给(|T| T -> T)
类型的参数f2
,但可以赋值给(|T| T -> T)
的参数f1
输入 T -> T
,所以 (Int -> Int) < (T -> T)
因此,确实是(|T| T -> T) < (Int -> Int) < (T -> T)
量化类型和依赖类型
依赖类型和量化类型(多态函数类型)之间有什么关系,它们之间有什么区别? 我们可以说依赖类型是一种接受参数的类型,而量化类型是一种赋予参数任意性的类型
重要的一点是封闭的多态类型本身没有类型参数。例如,多态函数类型|T| T -> T
是一个接受多态函数 only 的类型,它的定义是封闭的。您不能使用其类型参数T
来定义方法等
在 Erg 中,类型本身也是一个值,因此带参数的类型(例如函数类型)可能是依赖类型。换句话说,多态函数类型既是量化类型又是依赖类型
PolyFn = Patch(|T| T -> T)
PolyFn.
type self = T # 名称错误: 找不到"T"
DepFn T = Patch(T -> T)
DepFn.
type self =
log "by DepFn"
T
assert (Int -> Int).type() == Int # 由 DepFn
assert DepFn(Int).type() == Int # 由 DepFn
子类型
在 Erg 中,可以使用比较运算符 <
、>
确定类包含
Nat < Int
Int < Object
1... _ < Nat
{1, 2} > {1}
{=} > {x = Int}
{I: Int | I >= 1} < {I: Int | I >= 0}
请注意,这与 <:
运算符的含义不同。它声明左侧的类是右侧类型的子类型,并且仅在编译时才有意义
C <: T # T: 结构类型
f|D <: E| ...
assert F < G
您还可以为多态子类型规范指定 Self <: Add
,例如 Self(R, O) <: Add(R, O)
结构类型和类类型关系
结构类型是结构类型的类型,如果它们具有相同的结构,则被认为是相同的对象
T = Structural {i = Int}
U = Structural {i = Int}
assert T == U
t: T = {i = 1}
assert t in T
assert t in U
相反,类是符号类型的类型,不能在结构上与类型和实例进行比较
C = Class {i = Int}
D = Class {i = Int}
assert C == D # 类型错误: 无法比较类
c = C.new {i = 1}
assert c in C
assert not c in D
子程序的子类型化
子例程的参数和返回值只采用一个类 换句话说,您不能直接将结构类型或Trait指定为函数的类型 必须使用部分类型规范将其指定为"作为该类型子类型的单个类"
# OK
f1 x, y: Int = x + y
# NG
f2 x, y: Add = x + y
# OK
# A 是一些具体的类
f3<A <: Add> x, y: A = x + y
子程序中的类型推断也遵循此规则。当子例程中的变量具有未指定的类型时,编译器首先检查它是否是其中一个类的实例,如果不是,则在Trait范围内查找匹配项。如果仍然找不到,则会发生编译错误。此错误可以通过使用结构类型来解决,但由于推断匿名类型可能会给程序员带来意想不到的后果,因此它被设计为由程序员使用 Structural
显式指定
类向上转换
i: Int
i as (Int or Str)
i as (1..10)
i as {I: Int | I >= 0}
类型转换
向上转换
因为 Python 是一种使用鸭子类型的语言,所以没有强制转换的概念。没有必要向上转换,本质上也没有向下转换
但是,Erg 是静态类型的,因此有时必须进行强制转换
一个简单的例子是 1 + 2.0
: +
(Int, Ratio) 或 Int(<: Add(Ratio, Ratio)) 操作在 Erg 语言规范中没有定义。这是因为 Int <: Ratio
,所以 1 向上转换为 1.0,即 Ratio 的一个实例
~~ Erg扩展字节码在BINARY_ADD中增加了类型信息,此时类型信息为Ratio-Ratio。在这种情况下,BINARY_ADD 指令执行 Int 的转换,因此没有插入指定转换的特殊指令。因此,例如,即使您在子类中重写了某个方法,如果您将父类指定为类型,则会执行类型强制,并在父类的方法中执行该方法(在编译时执行名称修改以引用父母的方法)。编译器只执行类型强制验证和名称修改。运行时不强制转换对象(当前。可以实现强制转换指令以优化执行)。~~
@Inheritable
Parent = Class()
Parent.
greet!() = print! "Hello from Parent"
Child = Inherit Parent
Child.
# Override 需要 Override 装饰器
@Override
greet!() = print! "Hello from Child"
greet! p: Parent = p.greet!()
parent = Parent.new()
child = Child.new()
parent # 来自Parent的问候!
child # 来自child的问候!
此行为不会造成与 Python 的不兼容。首先,Python 没有指定变量的类型,所以可以这么说,所有的变量都是类型变量。由于类型变量会选择它们可以适应的最小类型,因此如果您没有在 Erg 中指定类型,则可以实现与 Python 中相同的行为
@Inheritable
Parent = Class()
Parent.
greet!() = print! "Hello from Parent"
Child = Inherit Parent
Child.
greet!() = print! "Hello from Child" Child.
greet! some = some.greet!()
parent = Parent.new()
child = Child.new()
parent # 来自Parent的问候!
child # 来自child的问候!
您还可以使用 .from
和 .into
,它们会为相互继承的类型自动实现
assert 1 == 1.0
assert Ratio.from(1) == 1.0
assert 1.into<Ratio>() == 1.0
向下转换
由于向下转换通常是不安全的并且转换方法很重要,我们改为实现TryFrom.try_from
IntTryFromFloat = Patch Int
IntTryFromFloat.
try_from r: Float =
if r.ceil() == r:
then: r.ceil()
else: Error "conversion failed".
可变类型
Warning: 本节中的信息是旧的并且包含一些错误
默认情况下,Erg 中的所有类型都是不可变的,即它们的内部状态无法更新
但是你当然也可以定义可变类型。变量类型用 !
声明
Person! = Class({name = Str; age = Nat!})
Person!.
greet! ref! self = print! "Hello, my name is \{self::name}. I am \{self::age}."
inc_age!ref!self = self::name.update!old -> old + 1
准确地说,基类型是可变类型或包含可变类型的复合类型的类型必须在类型名称的末尾有一个"!"。没有 !
的类型可以存在于同一个命名空间中,并被视为单独的类型
在上面的例子中,.age
属性是可变的,.name
属性是不可变的。如果即使一个属性是可变的,那么整个属性也是可变的
可变类型可以定义重写实例的过程方法,但具有过程方法并不一定使它们可变。例如数组类型[T; N]
实现了一个 sample!
随机选择一个元素的方法,但当然不会破坏性地修改数组
对可变对象的破坏性操作主要是通过 .update! 方法完成的。.update!
方法是一个高阶过程,它通过应用函数 f
来更新 self
i = !1
i.update! old -> old + 1
assert i == 2
.set!
方法只是丢弃旧内容并用新值替换它。.set!x = .update!_ -> x
i = !1
i.set! 2
assert i == 2
.freeze_map
方法对不变的值进行操作
a = [1, 2, 3].into [Nat; !3]
x = a.freeze_map a: [Nat; 3] -> a.iter().map(i -> i + 1).filter(i -> i % 2 == 0).collect(Array)
在多态不可变类型中,该类型的类型参数"T"被隐式假定为不可变
# ImmutType < Type
KT: ImmutType = Class ...
K!T: Type = Class ...
在标准库中,变量 (...)!
类型通常基于不可变 (...)
类型。但是,T!
和 T
类型没有特殊的语言关系,并且不能这样构造 1
请注意,有几种类型的对象可变性 下面我们将回顾内置集合类型的不可变/可变语义
# 数组类型
## 不可变类型
[T; N] # 不能执行可变操作
## 可变类型
[T; N] # 可以一一改变内容
[T; !N] # 可变长度,内容不可变但可以通过添加/删除元素来修改
[!T; N] # 内容是不可变的对象,但是可以替换成不同的类型(实际上可以通过不改变类型来替换)
[!T; !N] # 类型和长度可以改变
[T; !N] # 内容和长度可以改变
[!T!; N] # 内容和类型可以改变
[!T!; !N] # 可以执行各种可变操作
当然,您不必全部记住和使用它们
对于可变数组类型,只需将 !
添加到您想要可变的部分,实际上是 [T; N]
, [T!; N]
,[T; !N]
, [T!; !N]
可以涵盖大多数情况
这些数组类型是语法糖,实际类型是:
# actually 4 types
[T; N] = Array(T, N)
[T; !N] = Array!(T, !N)
[!T; N] = ArrayWithMutType!(!T, N)
[!T; !N] = ArrayWithMutTypeAndLength!(!T, !N)
[T!; !N] = Array!(T!, !N)
[!T!; N] = ArrayWithMutType!(!T!, N)
[!T!; !N] = ArrayWithMutTypeAndLength!(!T!, !N)
这就是能够改变类型的意思
a = [1, 2, 3].into [!Nat; 3]
a.map!(_ -> "a")
a: [!Str; 3]
其他集合类型也是如此
# 元组类型
## 不可变类型
(T, U) # 元素个数不变,内容不能变
## 可变类型
(T!, U) # 元素个数不变,第一个元素可以改变
(T,U)! # 元素个数不变,内容可以替换
...
# 设置类型
## 不可变类型
{T; N} # 不可变元素个数,内容不能改变
## 可变类型
{T!; N} # 不可变元素个数,内容可以改变(一个一个)
{T; N}! # 可变元素个数,内容不能改变
{T!; N}! # 可变元素个数,内容可以改变
...
# 字典类型
## 不可变类型
{K: V} # 长度不可变,内容不能改变
## 可变类型
{K:V!} # 恒定长度,值可以改变(一一)
{K: V}! # 可变长度,内容不能改变,但可以通过添加或删除元素来增加或删除,内容类型也可以改变
...
# 记录类型
## 不可变类型
{x = Int; y = Str} # 内容不能改变
## 可变类型
{x = Int!; y = Str} # 可以改变x的值
{x = Int; y = Str}! # 替换 {x = Int; 的任何实例 y = Str}
...
一个类型 (...)
简单地变成了 T! = (...)!
当 T = (...)
被称为简单结构化类型。简单的结构化类型也可以(语义上)说是没有内部结构的类型
数组、元组、集合、字典和记录类型都是非简单的结构化类型,但 Int 和 Refinement 类型是
# 筛子类型
## 枚举
{1, 2, 3} # 1, 2, 3 之一,不可更改
{1、2、3}! # 1、2、3,可以改
## 区间类型
1..12 # 1到12,不能改
1..12! # 1-12中的任意一个,你可以改变
## 筛型(普通型)
{I: Int | I % 2 == 0} # 偶数类型,不可变
{I: Int | I % 2 == 0} # 偶数类型,可以改变
{I: Int | I % 2 == 0}! # 与上面完全相同的类型,但上面的表示法是首选
从上面的解释来看,可变类型不仅包括自身可变的,还包括内部类型可变的
诸如 {x: Int!}
和 [Int!; 之类的类型3]
是内部可变类型,其中内部的对象是可变的,而实例本身是不可变的
对于具有内部结构并在类型构造函数本身上具有 !
的类型 K!(T, U)
,*self
可以更改整个对象。也可以进行局部更改
但是,希望尽可能保持本地更改权限,因此如果只能更改 T
,最好使用 K(T!, U)
而对于没有内部结构的类型‘T!’,这个实例只是一个可以交换的‘T’盒子。方法不能更改类型
1 T!
和 T
类型没有特殊的语言关系是有意的。这是一个设计。如果存在关系,例如命名空间中存在T
/T!
类型,则无法从其他模块引入T!
/T
类型。此外,可变类型不是为不可变类型唯一定义的。给定定义 T = (U, V)
,T!
的可能变量子类型是 (U!, V)
和 (U, V!)
。↩
类型绑定
类型边界为类型规范添加条件。实现这一点的函数是守卫(守卫子句) 此功能可用于函数签名、匿名函数签名以及筛选类型 守卫写在返回类型之后
谓词
您可以使用返回 Bool
的表达式(谓词表达式)指定变量满足的条件
只能使用 值对象 和运算符。未来版本可能会支持编译时函数
f a: [T; N] | T, N, N > 5 = ...
g a: [T; N | N > 5] | T, N = ...
Odd = {I: Int | I % 2 == 1}
R2Plus = {(L, R) | L, R: Ratio; L > 0 and R > 0}
GeneralizedOdd = {I | U; I <: Div(Nat, U); I % 2 == 0}
复合型
元组类型
(), (X,), (X, Y), (X, Y, Z), ...
元组具有长度和内部类型的子类型化规则
对于任何元组T
,U
,以下成立
* T <: () (单位规则)
* forall N in 0..<Len(T) (Len(T) <= Len(U)), U.N == T.N => U <: T (遗忘规则)
例如,(Int, Str, Bool) <: (Int, Str)
但是,这些规则不适用于函数类型的(可见)元组部分。这是因为这部分实际上不是元组
(Int, Int) -> Int !<: (Int,) -> Int
还有单位类型的返回值可以忽略,但是其他tuple类型的返回值不能忽略
配列型
[], [X; 0], [X; 1], [X; 2], ..., [X; _] == [X]
数组和元组存在类似的子类型化规则
* T <: [] (单位规则)
* forall N in 0..<Len(T) (Len(T) <= Len(U)), U[N] == T[N] => U <: T (遗忘规则)
像下面这样的数组不是有效类型。 这是一个刻意的设计,强调阵列元素是同质化的
[Int, Str]
因此,每个元素的详细信息都会丢失。 使用筛模来保存它
a = [1, "a"]: {A: [Int or Str; 2] | A[0] == Int}
a[0]: Int
设置类型
{}, {X}, ...
集合类型本身不携带长度信息。这是因为元素的重复项在集合中被消除,但重复项通常无法在编译时确定。首先,长度信息在集合中没有多大意义
{}
是一个空集合,是拥有类型的子类型
词典类型
{:}, {X: Y}, {X: Y, Z: W}, ...
记录类型
{=}, {i = Int}, {i = Int; j = Int}, {.i = Int; .j = Int}, ...
具有私有属性的类型和具有公共属性的类型之间没有子类型关系,但它们可以通过.Into
相互转换
r = {i = 1}.Into {.i = Int}
assert r.i == 1
函数类型
() -> ()
Int -> Int
(Int, Str) -> Bool
(x: Int, y: Int) -> Int
(x := Int, y := Int) -> Int
(...objs: Obj) -> Str
(Int, Ref Str!) -> Int
|T: Type|(x: T) -> T
|T: Type|(x: T := NoneType) -> T # |T: Type|(x: T := X, y: T := Y) -> T (X != Y) is invalid
高级类型
下面,我们将讨论更高级的类型系统。初学者不必阅读所有部分。
广义代数数据类型 (GADT)
Erg 可以通过对 Or 类型进行分类来创建广义代数数据类型 (GADT)
Nil T = Class(Impl := Phantom T)
Cons T = Class {head = T; rest = List T}, Impl := Unpack
List T: Type = Class(Nil T or Cons T)
List.
nil|T|() = Self(T).new Nil(T).new()
cons head, rest | T = Self(T).new Cons(T).new(head, rest)
head self = match self:
{head; ...}: Cons_ -> head
_: Nil -> panic "empty list"
{nil; cons} = List
print! cons(1, cons(2, nil())).head() # 1
print! nil.head() # 运行时错误: "空list"
我们说 List.nil|T|() = ...
而不是 List(T).nil() = ...
的原因是我们在使用它时不需要指定类型
i = List.nil()
_: List Int = cons 1, i
这里定义的 List T
是 GADTs,但它是一个幼稚的实现,并没有显示 GADTs 的真正价值
例如,上面的 .head 方法会在 body 为空时抛出运行时错误,但是这个检查可以在编译时进行
List: (Type, {"Empty", "Nonempty"}) -> Type
List T, "Empty" = Class(Impl := Phantom T)
List T, "Nonempty" = Class {head = T; rest = List(T, _)}, Impl := Unpack
List.
nil|T|() = Self(T, "Empty").new Nil(T).new()
cons head, rest | T = Self(T, "Nonempty").new {head; rest}
List(T, "Nonempty").
head {head; ...} = head
{nil; cons} = List
print! cons(1, cons(2, nil())).head() # 1
print! nil().head() # 类型错误
街上经常解释的 GADT 的一个例子是一个列表,可以像上面那样通过类型来判断内容是否为空 Erg 可以进一步细化以定义一个有长度的列表
List: (Type, Nat) -> Type
List T, 0 = Class(Impl := Phantom T)
List T, N = Class {head = T; rest = List(T, N-1)}, Impl := Unpack
List.
nil|T|() = Self(T, 0).new Nil(T).new()
cons head, rest | T, N = Self(T, N).new {head; rest}
List(_, N | N >= 1).
head {head; ...} = head
List(_, N | N >= 2).
pair {head = first; rest = {head = second; ...}} = [first, second]
{nil; cons} = List
print! cons(1, cons(2, nil)).pair() # [1, 2]
print! cons(1, nil).pair() # 类型错误
print! cons(1, nil).head() # 1
print! nil. head() # 类型错误
默认参数
首先,让我们看一个使用默认参数的示例
f: (Int, Int, z := Int) -> Int
f(x, y, z := 0) = x + y + z
g: (Int, Int, z := Int, w := Int) -> Int
g(x, y, z := 0, w := 1) = x + y + z + w
fold: ((Int, Int) -> Int, [Int], acc := Int) -> Int
fold(f, [], acc) = acc
fold(f, arr, acc := 0) = fold(f, arr[1..], f(acc, arr[0]))
assert fold(f, [1, 2, 3]) == 6
assert fold(g, [1, 2, 3]) == 8
:=
之后的参数是默认参数
子类型规则如下:
((X, y := Y) -> Z) <: (X -> Z)
((X, y := Y, ...) -> Z) <: ((X, ...) -> Z)
第一个意味着可以用没有默认参数的函数来识别具有默认参数的函数 第二个意味着可以省略任何默认参数。
类型擦除
类型擦除是将类型参数设置为 _
并故意丢弃其信息的过程。类型擦除是许多多态语言的特性,但在 Erg 的语法上下文中,将其称为类型参数擦除更为准确
类型擦除的最常见示例是 [T, _]
。数组在编译时并不总是知道它们的长度。例如,引用命令行参数的 sys.argv
的类型为 [Str, _]
。由于 Erg 的编译器无法知道命令行参数的长度,因此必须放弃有关其长度的信息
然而,一个已经被类型擦除的类型变成了一个未被擦除的类型的父类型(例如[T; N] <: [T; _]
),所以它可以接受更多的对象
类型的对象[T; N]
当然可以使用 [T; _]
,但使用后会删除N
信息。如果长度没有改变,那么可以使用[T; N]
在签名中。如果长度保持不变,则必须由签名指示
# 保证不改变数组长度的函数(例如,排序)
f: [T; N] -> [T; N] # 没有的函数 (f: [T; N])
# 没有的功能(例如过滤器)
g: [T; n] -> [T; _]
如果您在类型规范本身中使用 _
,则类型将向上转换为 Object
对于非类型类型参数(Int、Bool 等),带有 _
的参数将是未定义的
i: _ # i: Object
[_; _] == [Object; _] == Array
类型擦除与省略类型说明不同。一旦类型参数信息被删除,除非您再次声明它,否则它不会被返回
implicit = (1..5).iter().map(i -> i * 2).to_arr()
explicit = (1..5).iter().map(i -> i * 2).into(Array(Nat))
在 Rust 中,这对应于以下代码:
#![allow(unused)] fn main() { let partial = (1..6).iter().map(|i| i * 2).collect::<Vec<_>>(); }
Erg 不允许部分省略类型,而是使用高阶种类多态性
# collect 是采用 Kind 的高阶 Kind 方法
hk = (1..5).iter().map(i -> i * 2).collect(Array)
hk: Array(Int)
存在类型
如果存在对应于∀的for-all类型,那么很自然地假设存在对应于∃的存在类型 存在类型并不难。你已经知道存在类型,只是没有意识到它本身
T: Trait
f x: T = ...
上面的 trait T
被用作存在类型
相比之下,小写的T
只是一个Trait,X
是一个for-all类型
f|X <: T| x: X = ...
事实上,existential 类型被 for-all 类型所取代。那么为什么会有存在类型这样的东西呢? 首先,正如我们在上面看到的,存在类型不涉及类型变量,这简化了类型规范 此外,由于可以删除类型变量,因此如果它是一个全推定类型,则可以构造一个等级为 2 或更高的类型
show_map f: (|T| T -> T), arr: [Show; _] =
arr.map x ->
y = f x
log y
y
但是,如您所见,existential 类型忘记或扩展了原始类型,因此如果您不想扩展返回类型,则必须使用 for-all 类型 相反,仅作为参数且与返回值无关的类型可以写为存在类型
# id(1): 我希望它是 Int
id|T|(x: T): T = x
# |S <: Show|(s: S) -> () 是多余的
show(s: Show): () = log s
顺便说一句,类不称为存在类型。一个类不被称为存在类型,因为它的元素对象是预定义的 存在类型是指满足某种Trait的任何类型,它不是知道实际分配了什么类型的地方。
关键字参数
h(f) = f(y: 1, x: 2)
h: |T: type|((y: Int, x: Int) -> T) -> T
带有关键字参数的函数的子类型化规则如下
((x: T, y: U) -> V) <: ((T, U) -> V) # x, y 为任意关键字参数
((y: U, x: T) -> V) <: ((x: T, y: U) -> V)
((x: T, y: U) -> V) <: ((y: U, x: T) -> V)
这意味着可以删除或替换关键字参数
但是你不能同时做这两件事
也就是说,您不能将 (x: T, y: U) -> V
转换为 (U, T) -> V
请注意,关键字参数仅附加到顶级元组,而不附加到数组或嵌套元组
Valid: [T, U] -> V
Invalid: [x: T, y: U] -> V
Valid: (x: T, ys: (U,)) -> V
Invalid: (x: T, ys: (y: U,)) -> V
Kind
一切都在 Erg 中输入。类型本身也不例外。kind 表示"类型的类型"。例如,Int
属于 Type
,就像 1
属于 Int
。Type
是最简单的一种,atomic kind。在类型论符号中,Type
对应于 *
在Kind的概念中,实际上重要的是一种或多种Kind(多项式Kind)。单项类型,例如Option
,属于它。一元Kind表示为 Type -> Type
1。诸如 Array
或 Option
之类的 container 特别是一种以类型作为参数的多项式类型
正如符号 Type -> Type
所表明的,Option
实际上是一个接收类型 T
并返回类型 Option T
的函数。但是,由于这个函数不是通常意义上的函数,所以通常称为一元类
注意->
本身,它是一个匿名函数操作符,当它接收一个类型并返回一个类型时,也可以看作是一Kind型
另请注意,不是原子Kind的Kind不是类型。正如 -1
是一个数字但 -
不是,Option Int
是一个类型但 Option
不是。Option
等有时被称为类型构造函数
assert not Option in Type
assert Option in Type -> Type
所以像下面这样的代码会报错:
在 Erg 中,方法只能在原子类型中定义,并且名称 self
不能在方法的第一个参数以外的任何地方使用
# K 是一元类型
K: Type -> Type
K T = Class ...
K.
foo x = ... # OK,这就像是所谓的静态方法
bar self, x = ... # 类型错误: 无法为非类型对象定义方法
K(T).
baz self, x = ... # OK
二进制或更高类型的示例是 {T: U}
(: (Type, Type) -> Type
), (T, U, V)
(: (Type, Type, Type) - > Type
), ... 等等
还有一个零项类型() -> Type
。这有时等同于类型论中的原子类型,但在 Erg 中有所区别。一个例子是类
Nil = Class()
收容类
多项类型之间也存在部分类型关系,或者更确切地说是部分类型关系
K T = ...
L = Inherit K
L<: K
也就是说,对于任何 T
,如果 L T <: K T
,则 L <: K
,反之亦然
∀T. L T <: K T <=> L <: K
高阶Kind
还有一种高阶Kind。这是一种与高阶函数相同的概念,一种自身接收一种类型。(Type -> Type) -> Type
是一种更高的Kind。让我们定义一个属于更高Kind的对象
IntContainerOf K: Type -> Type = K Int
assert IntContainerOf Option == Option Int
assert IntContainerOf Result == Result Int
assert IntContainerOf in (Type -> Type) -> Type
多项式类型的有界变量通常表示为 K, L, ...,其中 K 是 Kind 的 K
设置Kind
在类型论中,有记录的概念。这与 Erg 记录 2 几乎相同
# 这是一条记录,对应于类型论中所谓的记录
{x = 1; y = 2}
当所有的记录值都是类型时,它是一种类型,称为记录类型
assert {x = 1; y = 2} in {x = Int; y = Int}
记录类型键入记录。一个好的猜测者可能认为应该有一个"记录类型"来键入记录类型。实际上它是存在的
log Typeof {x = Int; y = Int} # {{x = Int; y = Int}}
像 {{x = Int; 这样的类型 y = Int}}
是一种记录类型。这不是一个特殊的符号。它只是一个枚举类型,只有 {x = Int; y = Int}
作为一个元素
Point = {x = Int; y = Int}
Pointy = {Point}
记录类型的一个重要属性是,如果 T: |T|
和 U <: T
则 U: |T|
从枚举实际上是筛子类型的语法糖这一事实也可以看出这一点
# {c} == {X: T | X == c} 对于普通对象,但是不能为类型定义相等性,所以 |T| == {X | X <: T}
{Point} == {P | P <: Point}
类型约束中的 U <: T
实际上是 U: |T|
的语法糖
作为此类类型的集合的种类通常称为集合种类。Setkind 也出现在迭代器模式中
Iterable T = Trait {
.Iterator = {Iterator}
.iter = (self: Self) -> Self.Iterator T
}
多项式类型的类型推断
Container K: Type -> Type, T: Type = Patch K(T, T)
Container (K).
f self = ...
Option T: Type = Patch T or NoneType
Option(T).
f self = ...
Fn T: Type = Patch T -> T
Fn(T).
f self = ...
Fn2 T, U: Type = Patch T -> U
Fn2(T, U).
f self = ...
(Int -> Int).f() # 选择了哪一个?
在上面的示例中,方法 f
会选择哪个补丁?
天真,似乎选择了Fn T
,但是Fn2 T,U
也是可以的,Option T
原样包含T
,所以任何类型都适用,Container K,T
也匹配->(Int, Int)
,即 Container(
->, Int)
为 Int -> Int
。因此,上述所有四个修复程序都是可能的选择
在这种情况下,根据以下优先标准选择修复程序
- 任何
K(T)
(例如T or NoneType
)优先匹配Type -> Type
而不是Type
- 任何
K(T, U)
(例如T -> U
)优先匹配(Type, Type) -> Type
而不是Type
- 类似的标准适用于种类 3 或更多
- 选择需要较少类型变量来替换的那个。例如,
Int -> Int
是T -> T
而不是K(T, T)
(替换类型变量: K, T)或T -> U
(替换类型变量: T, U )。(替换类型变量: T)优先匹配 - 如果更换的次数也相同,则报错为不可选择
1 在类型理论符号中,*=>*
↩
2 可见性等细微差别。↩
标记Trait
标记Trait是没有必需属性的Trait。也就是说,您可以在不实现任何方法的情况下实现 Impl 没有 required 属性似乎没有意义,但由于注册了它属于 trait 的信息,因此可以使用 patch 方法或由编译器进行特殊处理
所有标记Trait都包含在"标记"Trait中 作为标准提供的"光"是一种标记Trait
Light = Subsume Marker
Person = Class {.name = Str; .age = Nat} and Light
M = Subsume Marker
MarkedInt = Inherit Int, Impl := M
i = MarkedInt.new(2)
assert i + 1 == 2
assert i in M
标记类也可以使用 Excluding
参数排除
NInt = Inherit MarkedInt, Impl := N, Excluding: M
可变结构类型
T!
类型被描述为可以被任何 T
类型对象替换的盒子类型
Particle!State: {"base", "excited"}! = Class(... Impl := Phantom State)
Particle!
# 此方法将状态从"base"移动到"excited"
apply_electric_field!(ref! self("base" ~> "excited"), field: Vector) = ...
T!
类型可以替换数据,但不能改变其结构
更像是一个真实程序的行为,它不能改变它的大小(在堆上)。这样的类型称为不可变结构(mutable)类型
事实上,有些数据结构不能用不变的结构类型来表示
例如,可变长度数组。[T; N]!
类型可以包含任何[T; N]
,但不能被[T; N+1]
等等
换句话说,长度不能改变。要改变长度,必须改变类型本身的结构
这是通过可变结构(可变)类型实现的
v = [Str; !0].new()
v.push! "Hello"
v: [Str; !1].
对于可变结构类型,可变类型参数用 !
标记。在上述情况下,类型 [Str; !0]
可以更改为 [Str; !1]
等等。即,可以改变长度
顺便说一句,[T; !N]
类型是 ArrayWithLength!(T, !N)
类型的糖衣语法
可变结构类型当然可以是用户定义的。但是请注意,在构造方法方面与不变结构类型存在一些差异
Nil T = Class(Impl := Phantom T)
List T, !0 = Inherit Nil T
List T, N: Nat! = Class {head = T; rest = List(T, !N-1)}
List(T, !N).
push! ref! self(N ~> N+1, ...), head: T =
self.update! old -> Self.new {head; old}
新类型模式
这是 Rust 中常用的 newtype 模式的 Erg 版本
Erg 允许定义类型别名如下,但它们只引用相同的类型
UserID = Int
因此,例如,如果你有一个规范,类型为 UserId
的数字必须是一个正的 8 位数字,你可以输入 10
或 -1
,因为它与类型 Int
相同 . 如果设置为 Nat
,则可以拒绝 -1
,但 8 位数字的性质不能仅用 Erg 的类型系统来表达
此外,例如,在设计数据库系统时,假设有几种类型的 ID: 用户 ID、产品 ID、产品 ID 和用户 ID。如果 ID 类型的数量增加,例如用户 ID、产品 ID、订单 ID 等,可能会出现将不同类型的 ID 传递给不同函数的 bug。即使用户 ID 和产品 ID 在结构上相同,但它们在语义上是不同的
对于这种情况,newtype 模式是一个很好的设计模式
UserId = Class {id = Nat}
UserId.
new id: Nat =
assert id.dights().len() == 8, else: "UserId 必须是长度为 8 的正数"
UserId::__new__ {id;}
i = UserId.new(10000000)
print! i # <__main__.UserId object>
i + UserId.new(10000001) # TypeError: + is not implemented between `UserId` and `UserId
构造函数保证 8 位数字的前置条件
UserId
失去了 Nat
拥有的所有方法,所以每次都必须重新定义必要的操作
如果重新定义的成本不值得,最好使用继承。另一方面,在某些情况下,方法丢失是可取的,因此请根据情况选择适当的方法
重载
Erg 不支持 ad hoc 多态性。也就是说,函数和种类(重载)的多重定义是不可能的。但是,您可以通过使用Trait和补丁的组合来重现重载行为
您可以使用Trait而不是Trait类,但随后将涵盖所有实现 .add1
的类型
Add1 = Trait {
.add1: Self.() -> Self
}
IntAdd1 = Patch Int, Impl := Add1
IntAdd1.
add1 self = self + 1
RatioAdd1 = Patch Ratio, Impl := Add1
RatioAdd1.
add1 self = self + 1.0
add1|X <: Add1| x: X = x.add1()
assert add1(1) == 2
assert add1(1.0) == 2.0
这种接受一个类型的所有子类型的多态称为__subtyping polymorphism__
如果每种类型的过程完全相同,则可以编写如下。当行为从类到类(但返回类型相同)时,使用上述内容 使用类型参数的多态称为 parametric polymorphism。参数多态性通常与子类型结合使用,如下所示,在这种情况下,它是参数和子类型多态性的组合
add1|T <: Int or Str| x: T = x + 1
assert add1(1) == 2
assert add1(1.0) == 2.0
此外,可以使用默认参数重现具有不同数量参数的类型的重载
C = Class {.x = Int; .y = Int}
C.
new(x, y := 0) = Self::__new__ {.x; .y}
assert C.new(0, 0) == C.new(0)
Erg 的立场是,您不能定义行为完全不同的函数,例如根据参数的数量具有不同的类型,但如果行为不同,则应该以不同的方式命名
综上所述,Erg 禁止重载,采用子类型加参数多态,原因如下
首先,重载函数分布在它们的定义中。这使得在发生错误时很难报告错误的原因 此外,导入子程序可能会改变已定义子程序的行为
{id;} = import "foo"
...
id x: Int = x
...
id x: Ratio = x
...
id "str" # 类型错误: 没有为 Str 实现 id
# 但是……但是……这个错误是从哪里来的?
其次,它与默认参数不兼容。当具有默认参数的函数被重载时,会出现一个优先级的问题
f x: Int = ...
f(x: Int, y := 0) = ...
f(1) # 选择哪个?
此外,它与声明不兼容
声明 f: Num -> Num
不能指定它引用的定义。这是因为 Int -> Ratio
和 Ratio -> Int
不包含在内
f: Num -> Num
f(x: Int): Ratio = ...
f(x: Ratio): Int = ...
并且语法不一致: Erg禁止变量重新赋值,但是重载的语法看起来像重新赋值 也不能用匿名函数代替
# 同 `f = x -> body`
f x = body
# 一样……什么?
f x: Int = x
f x: Ratio = x
幻影类
幻像类型是标记Trait,其存在仅用于向编译器提供注释 作为幻像类型的一种用法,让我们看一下列表的结构
Nil = Class()
List T, 0 = Inherit Nil
List T, N: Nat = Class {head = T; rest = List(T, N-1)}
此代码导致错误
3 | List T, 0 = Inherit Nil
^^^
类型构造错误: 由于Nil没有参数T,所以无法用Nil构造List(T, 0)
提示: 使用 'Phantom' trait消耗 T
此错误是在使用 List(_, 0).new Nil.new()
时无法推断 T
的抱怨
在这种情况下,无论 T
类型是什么,它都必须在右侧使用。大小为零的类型(例如长度为零的元组)很方便,因为它没有运行时开销
Nil T = Class((T; 0))
List T, 0 = Inherit Nil T
List T, N: Nat = Class {head = T; rest = List(T, N-1)}
此代码通过编译。但是理解意图有点棘手,除非类型参数是类型,否则不能使用它
在这种情况下,幻影类型正是您所需要的。幻像类型是大小为 0 的广义类型
Nil T = Class(Impl := Phantom T)
List T, 0 = Inherit Nil T
List T, N: Nat = Class {head = T; rest = List(T, N-1)}
nil = Nil(Int).new()
assert nil.__size__ == 0
Phantom
拥有T
类型。但实际上 Phantom T
类型的大小是 0 并且不包含 T
类型的对象
此外,Phantom
可以使用除其类型之外的任意类型参数。在下面的示例中,Phantom
包含一个名为 State
的类型参数,它是 Str
的子类型对象
同样,State
是一个假的类型变量,不会出现在对象的实体中
VM! State: {"stopped", "running"}! = Class(... State)
VM!("stopped").
start ref! self("stopped" ~> "running") =
self.do_something!()
self::set_phantom!("running"))
state
是通过 update_phantom!
或 set_phantom!
方法更新的
这是标准补丁为Phantom!
(Phantom
的变量版本)提供的方法,其用法与变量update!
和set!
相同。
投影类型
投影类型表示如下代码中的"Self.AddO"等类型
Add R = Trait {
. `_+_` = Self, R -> Self.AddO
.AddO = Type
}
AddForInt = Patch(Int, Impl := Add Int)
AddForInt.
AddO = Int
类型"Add(R)"可以说是定义了与某个对象的加法的类型。由于方法应该是一个类型属性,+
类型声明应该写在缩进下面
Add
类型的场景是声明 .AddO = Type
,而 .AddO
类型的实体是一个投影类型,由一个作为 子类型的类型持有 添加
。例如,Int.AddO = Int
、Odd.AddO = Even
assert Int < Add
assert Int.AddO == Int
assert Odd < Add
assert Odd.AddO == Even
量化依赖类型
Erg 有量化和依赖类型。那么很自然地,就可以创建一个将两者结合起来的类型。那是量化的依赖类型
NonNullStr = |N: Nat| StrWithLen N | N ! = 0 # 同 {S | N: Nat; S: StrWithLen N; N ! = 0}
NonEmptyArray = |N: Nat| [_; N | N > 0] # 同 {A | N: Nat; A: Array(_, N); N > 0}
量化依赖类型的标准形式是"K(A, ... | Pred)"。K
是类型构造函数,A, B
是类型参数,Pred
是条件表达式
作为左值的量化依赖类型只能在与原始类型相同的模块中定义方法
K A: Nat = Class ...
K(A).
...
K(A | A >= 1).
method ref! self(A ~> A+1) = ...
作为右值的量化依赖类型需要在类型变量列表 (||
) 中声明要使用的类型变量
# T 是具体类型
a: |N: Nat| [T; N | N > 1]
共享引用
共享引用是必须小心处理的语言特性之一 例如,在 TypeScript 中,以下代码将通过类型检查
class NormalMember {}
class VIPMember extends NormalMember {}
let vip_area: VIPMember[] = []
let normal_area: NormalMember[] = vip_area
normal_area.push(new NormalMember())
console.log(vip_area) # [NormalMember]
一个 NormalMember 已进入 vip_area。这是一个明显的错误,但是出了什么问题?
原因是共享引用 denatured。normal_area
是通过复制 vip_area
来创建的,但是这样做的时候类型已经改变了
但是 VIPMember
继承自 NormalMember
,所以 VIPMember[] <: NormalMember[]
,这不是问题
关系 VIPMember[] <: NormalMember[]
适用于不可变对象。但是,如果您执行上述破坏性操作,则会出现故障
在 Erg 中,由于所有权系统,此类代码会被回放
NormalMember = Class()
VIPMember = Class()
vip_area = [].into [VIPMember; !_]
normal_area: [NormalMember; !_] = vip_area
normal_area.push!(NormalMember.new())
log vip_area # 所有权错误: `vip_room` 已移至 `normal_room`
然而,一个对象只属于一个地方可能会很不方便
出于这个原因,Erg 有一个类型 SharedCell!T!
,它代表一个共享状态
$p1 = SharedCell!.new(!1)
$p2 = $p1.mirror!()
$p3 = SharedCell!.new(!1)
# 如果$p1 == $p2,比较内容类型Int!
assert $p1 == $p2
assert $p1 == $p3
# 检查 $p1 和 $p2 是否用 `.addr!` 指向同一个东西
assert $p1.addr!() == $p2.addr!()
assert $p1.addr!() != $p3.addr!()
$p1.add! 1
assert $p1 == 2
assert $p2 == 2
assert $p3 == 1
SharedCell!
类型的对象必须以$
为前缀。此外,就其性质而言,它们不可能是常数
SharedCell! T!
类型也是 T!
的子类型,可以调用 T!
类型的方法。SharedCell!T!
类型特有的唯一方法是 .addr!
、.mirror!
和 .try_take
一个重要的事实是SharedCell! T!
是非变体的,即没有为不同类型的参数定义包含
$vip_area = SharedCell!.new([].into [VIPMember; !_])
$normal_area: SharedCell!([NormalMember; !_]) = $vip_area.mirror!() # 类型错误: 预期 SharedCell!([NormalMember;!_]),但得到 SharedCell!([VIPMember;!_])
# 提示: SharedCell!(T) 是非变体的,这意味着它不能有父类型或子类型
但是,下面的代码没有问题。在最后一行,它是 VIPMember
参数已被类型转换
$normal_area = SharedCell!.new([].into [NormalMember; !_])
$normal_area.push!(NormalMember.new()) # OK
$normal_area.push!(VIPMember.new()) # OK
特殊类型(Self、Super)
Self
代表它自己的类型。您可以将其用作别名,但请注意派生类型的含义会发生变化(指的是自己的类型)
@Inheritable
C = Class()
C.
new_self() = Self. new()
new_c() = C.new()
D = Inherit C
classof D. new_self() # D
classof D. new_c() # C
Super
表示基类的类型。方法本身引用基类,但实例使用自己的类型
@Inheritable
C = Class()
D = Inherit(C)
D.
new_super() = Super.new()
new_c() = C.new()
classof D. new_super() # D
classof D. new_c() # C
特殊类型变量
Self
和 Super
可以用作结构化类型和Trait中的类型变量。这指的是作为该类型子类型的类。也就是说,T
类型中的Self
表示Self <: T
Add R = Trait {
.AddO = Type
.`_+_`: Self, R -> Self.AddO
}
ClosedAdd = Subsume Add(Self)
ClosedAddForInt = Patch(Int, Impl := ClosedAdd)
ClosedAddForInt.
AddO = Int
assert 1 in Add(Int, Int)
assert 1 in ClosedAdd
assert Int < Add(Int, Int)
assert Int < ClosedAdd
Typeof
Typeof
是一个可以窥探 Erg 类型推断系统的函数,它的行为很复杂
assert Typeof(1) == {I: Int | I == 1}
i: 1..3 or 5..10 = ...
assert Typeof(i) == {I: Int | (I >= 1 and I <= 3) or (I >= 5 and I <= 10)}
C = Class {i = Int}
I = C. new {i = 1}
assert Typeof(I) == {X: C | X == I}
J: C = ...
assert Typeof(J) == {i = Int}
assert {X: C | X == I} < C and C <= {i = Int}
Typeof
函数返回派生类型,而不是对象的类
因此,例如 C = Class T
类的I: C
,Typeof(I) == T
值类没有对应的记录类型。为了解决这个问题,值类应该是具有 __valueclass_tag__
属性的记录类型
请注意,您不能访问此属性,也不能在用户定义的类型上定义 __valueclass_tag__
属性
i: Int = ...
assert Typeof(i) == {__valueclass_tag__ = Phantom Int}
s: Str = ...
assert Typeof(s) == {__valueclass_tag__ = Phantom Str}
Typeof
仅输出结构化类型。我解释说结构化类型包括属性类型、筛类型和(真正的)代数类型
这些是独立的类型(存在推理优先级),不会发生推理冲突
属性类型和代数类型可以跨越多个类,而筛类型是单个类的子类型
Erg 尽可能将对象类型推断为筛类型,如果不可能,则将筛基类扩展为结构化类型(见下文)
结构化的
所有类都可以转换为派生类型。这称为 结构化。类的结构化类型可以通过 Structure
函数获得
如果一个类是用C = Class T
定义的(所有类都以这种形式定义),那么Structure(C) == T
C = Class {i = Int}
assert Structure(C) == {i = Int}
D = Inherit C
assert Structure(D) == {i = Int}
Nat = Class {I: Int | I >= 0}
assert Structure(Nat) == {I: Int | I >= 0}
Option T = Class (T or NoneType)
assert Structure(Option Int) == Or(Int, NoneType)
assert Structure(Option) # 类型错误: 只能构造单态类型
# 你实际上不能用 __valueclass_tag__ 定义一条记录,但在概念上
assert Structure(Int) == {__valueclass_tag__ = Phantom Int}
assert Structure(Str) == {__valueclass_tag__ = Phantom Str}
assert Structure((Nat, Nat)) == {__valueclass_tag__ = Phantom(Tuple(Nat, Nat))}
assert Structure(Nat -> Nat) == {__valueclass_tag__ = Phantom(Func(Nat, Nat))}
# 标记类也是带有 __valueclass_tag__ 的记录类型
M = Inherit Marker
assert Structure(M) == {__valueclass_tag__ = Phantom M}
D = Inherit(C and M)
assert Structure(D) == {i = Int; __valueclass_tag__ = Phantom M}
E = Inherit(Int and M)
assert Structure(E) == {__valueclass_tag__ = Phantom(And(Int, M))}
F = Inherit(E not M)
assert Structure(F) == {__valueclass_tag__ = Phantom Int}
变性(逆变与协变)
Erg 可以对多态类型进行子类型化,但有一些注意事项
首先,考虑普通多态类型的包含关系。一般来说,有一个容器K
和它分配的类型A,B
,当A < B
时,K A < K B
例如,Option Int < Option Object
。因此,在Option Object
中定义的方法也可以在Option Int
中使用
考虑典型的多态类型 Array!(T)
请注意,这一次不是 Array!(T, N)
因为我们不关心元素的数量
现在,Array!(T)
类型具有称为 .push!
和 .pop!
的方法,分别表示添加和删除元素。这是类型:
Array.push!: Self(T).(T) => NoneType
Array.pop!: Self(T).() => T
可以直观地理解:
Array!(Object).push!(s)
is OK whens: Str
(just upcastStr
toObject
)- When
o: Object
,Array!(Str).push!(o)
is NG Array!(Object).pop!().into(Str)
is NGArray!(Str).pop!().into(Object)
is OK
就类型系统而言,这是
(Self(Object).(Object) => NoneType) < (Self(Str).(Str) => NoneType)
(Self(Str).() => Str) < (Self(Object).() => Object)
方法
前者可能看起来很奇怪。即使是 Str < Object
,包含关系在将其作为参数的函数中也是相反的
在类型论中,这种关系(.push!
的类型关系)称为逆变,反之,.pop!
的类型关系称为协变
换句话说,函数类型就其参数类型而言是逆变的,而就其返回类型而言是协变的
这听起来很复杂,但正如我们之前看到的,如果将其应用于实际示例,这是一个合理的规则
如果您仍然不明白,请考虑以下内容
Erg 的设计原则之一是"大输入类型,小输出类型"。这正是函数可变性的情况 看上面的规则,输入类型越大,整体类型越小 这是因为通用函数明显比专用函数少 而且输出类型越小,整体越小
这样一来,上面的策略就相当于说"尽量减少函数的类型"
不变性
Erg 有另一个修改。它是不变的
这是对 SharedCell! T!
等内置类型的修改。这意味着对于两种类型 T!, U!
其中 T! != U!
,在 SharedCell! T!
和 SharedCell!意思是 这是因为
SharedCell! T!` 是共享参考。有关详细信息,请参阅 共享参考
变异的泛型类型
通用类型变量可以指定其上限和下限
|A <: T| K(A)
|B :> T| K(B)
在类型变量列表中,执行类型变量的__variant说明__。在上述变体规范中,类型变量"A"被声明为"T"类型的任何子类,"B"类型被声明为"T"类型的任何父类
在这种情况下,T
也称为 A
的上部类型和 B
的下部类型
突变规范也可以重叠
# U<A<T
|A<: T, A :> U| ...
这是使用变量规范的代码示例:
show|S <: Show| s: S = log s
Nil T = Class(Impl = Phantom T)
Cons T = Class(Nil T or List T)
List T = Class {head = T; rest = Cons T}
List(T).
push|U <: T|(self, x: U): List T = Self. new {head = x; rest = self}
upcast(self, U :> T): List U = self
更改规范
List T
的例子很棘手,所以让我们更详细一点
要理解上面的代码,你需要了解多态类型退化。this section 中详细讨论了方差,但现在我们需要三个事实:
- 普通的多态类型,例如
List T
,与T
是协变的(List U > List T
whenU > T
) - 函数
T -> U
对于参数类型T
是逆变的((S -> U) < (T -> U)
whenS > T
) - 函数
T -> U
与返回类型U
是协变的((T -> U) > (T -> S)
当U > S
时)
例如,List Int
可以向上转换为 List Object
,而 Obj -> Obj
可以向上转换为 Int -> Obj
现在让我们考虑如果我们省略方法的变量说明会发生什么
...
List T = Class {head = T; rest = Cons T}
List(T).
# 如果 T > U,列表 T 可以被推入 U
push|U|(self, x: U): List T = Self. new {head = x; rest = self}
# List T 可以是 List U 如果 T < U
upcast(self, U): List U = self
即使在这种情况下,Erg 编译器也能很好地推断 U
的上下类型
但是请注意,Erg 编译器不理解方法的语义。编译器只是根据变量和类型变量的使用方式机械地推断和派生类型关系
正如评论中所写,放在List T
的head
中的U
类型是T
的子类(T: Int
,例如Nat
)。也就是说,它被推断为 U <: T
。此约束将 .push{U}
upcast (List(T), U) -> List(T) 的参数类型更改为 (List(T), T) -> List(T)
(例如 disallow 列表(整数).push{对象}
)。但是请注意,U <: T
约束不会改变函数的类型包含。(List(Int), Object) -> List(Int) to (List(Int), Int) -> List(Int)
的事实并没有改变,只是在 .push
方法中表示强制转换无法执行
类似地,从 List T
到 List U
的转换可能会受到约束 U :> T
的约束,因此可以推断出变体规范。此约束将 .upcast(U)
的返回类型更改为向上转换 List(T) -> List(T) 到 List(T) -> List(T)
(例如 List(Object) .upcast(Int )
) 被禁止
现在让我们看看如果我们允许这种向上转换会发生什么 让我们反转变性名称
...
List T = Class {head = T; rest = Cons T}
List(T).
push|U :> T|(self, x: U): List T = Self. new {head = x; rest = self}
upcast(self, U :> T): List U = self
# 类型警告: `.push` 中的 `U` 不能接受除 `U == T` 之外的任何内容。将"U"替换为"T"
# 类型警告: `.upcast` 中的 `U` 不能接受除 `U == T` 之外的任何内容。将"U"替换为"T"
只有当 U == T
时,约束 U <: T
和修改规范U :> T
才满足。所以这个称号没有多大意义
只有"向上转换使得 U == T
" = "向上转换不会改变 U
的位置"实际上是允许的
附录: 用户定义类型的修改
默认情况下,用户定义类型的突变是不可变的。但是,您也可以使用 Inputs/Outputs
标记Trait指定可变性
如果您指定 Inputs(T)
,则类型相对于 T
是逆变的
如果您指定 Outputs(T)
,则类型相对于 T
是协变的
K T = Class(...)
assert not K(Str) <= K(Object)
assert not K(Str) >= K(Object)
InputStream T = Class ..., Impl := Inputs(T)
# 接受Objects的流也可以认为接受Strs
assert InputStream(Str) > InputStream(Object)
OutputStream T = Class ..., Impl := Outputs(T)
# 输出Str的流也可以认为输出Object
assert OutputStream(Str) < OutputStream(Object)
Widening
例如,定义多相关系数如下
ids|T|(x: T, y: T) = x, y
分配同一类的一对实例并没有错 当您分配另一个具有包含关系的类的实例对时,它会向上转换为较大的类并成为相同的类型 另外,很容易理解,如果分配了另一个不在包含关系中的类,就会发生错误
assert ids(1, 2) == (1, 2)
assert ids(1, 2.0) == (1.0, 2.0)
ids(1, "a") # TypeError
现在,具有不同派生类型的类型呢?
i: Int or Str
j: Int or NoneType
ids(i, j) # ?
在解释这一点之前,我们必须关注 Erg 的类型系统实际上并不关注(运行时)类这一事实
1: {__valueclass_tag__ = Phantom Int}
2: {__valueclass_tag__ = Phantom Int}
2.0: {__valueclass_tag__ = Phantom Ratio}
"a": {__valueclass_tag__ = Phantom Str}
ids(1, 2): {__valueclass_tag__ = Phantom Int} and {__valueclass_tag__ = Phantom Int} == {__valueclass_tag__ = Phantom Int}
ids(1, 2.0): {__valueclass_tag__ = Phantom Int} and {__valueclass_tag__ = Phantom Ratio} == {__valueclass_tag__ = Phantom Ratio} # Int < Ratio
ids(1, "a"): {__valueclass_tag__ = Phantom Int} and {__valueclass_tag__ = Phantom Str} == Never # 类型错误
我看不到该类,因为它可能无法准确看到,因为在 Erg 中,对象的类属于运行时信息
例如,一个Int
或Str类型的对象的类是
Int或
Str,但你只有通过执行才能知道它是哪一个 当然,
Int类型的对象的类被定义为
Int,但是在这种情况下,从类型系统中可见的是
Int的结构类型
{valueclass_tag = Int}`
现在让我们回到另一个结构化类型示例。总之,上述代码将导致类型错误,因为类型不匹配 但是,如果您使用类型注释进行类型扩展,编译将通过
i: Int or Str
j: Int or NoneType
ids(i, j) # 类型错误: i 和 j 的类型不匹配
# 提示: 尝试扩大类型(例如 ids<Int or Str or NoneType>)
ids<Int or Str or NoneType>(i, j) # OK
A 和 B
有以下可能性
A and B == A
: 当A <: B
或A == B
时A and B == B
: 当A :> B
或A == B
时A and B == {}
: 当!(A :> B)
和!(A <: B)
时
A 或 B
具有以下可能性
A 或 B == A
: 当A :> B
或A == B
时A or B == B
: 当A <: B
或A == B
时A 或 B
是不可约的(独立类型): 如果!(A :> B)
和!(A <: B)
子程序定义中的类型扩展
如果返回类型不匹配,Erg 默认会出错
parse_to_int s: Str =
if not s.is_numeric():
do parse_to_int::return error("not numeric")
... # 返回 Int 对象
# 类型错误: 返回值类型不匹配
# 3 | 做 parse_to_int::return error("not numeric")
# └─ Error
# 4 | ...
# └ Int
为了解决这个问题,需要将返回类型显式指定为 Or 类型
parse_to_int(s: Str): Int or Error =
if not s.is_numeric():
do parse_to_int::return error("not numeric")
... # 返回 Int 对象
这是设计使然,这样您就不会无意中将子例程的返回类型与另一种类型混合
但是,如果返回值类型选项是具有包含关系的类型,例如 Int
或 Nat
,它将与较大的对齐。
迭代器
迭代器是用于检索容器元素的对象
for! 0..9, i =>
print! i
此代码打印数字 0 到 9
每个数字(=Int 对象)都分配给i
,并执行以下操作(=print!i
)。这种重复执行称为__iteration__
现在让我们看看 for!
过程的类型签名
for!: |T: Type, I <: Iterable T| (I, T => None) => None
第一个参数似乎接受"Iterable"类型的对象
Iterable
是一个具有.Iterator
属性的类型,.iter
方法在request 方法中
Iterable T = Trait {
.Iterator = {Iterator}
.iter = (self: Self) -> Self.Iterator T
}
.Iterator
属性的类型 {Iterator}
是所谓的 set-kind(kind 在 here 中描述)
assert [1, 2, 3] in Iterable(Int)
assert 1..3 in Iterable(Int)
assert [1, 2, 3].Iterator == ArrayIterator
assert (1..3).Iterator == RangeIterator
log [1, 2, 3].iter() # <数组迭代器对象>
log (1..3).iter() # <Range迭代器对象>
ArrayIterator
和 RangeIterator
都是实现 Iterator
的类,它们的存在只是为了提供 Array
和 Range
迭代函数
这种设计模式称为伴生类 1
而"IteratorImpl"补丁是迭代功能的核心。Iterator
只需要一个.next
方法,IteratorImpl
确实提供了几十种方法。ArrayIterator
和RangeIterator
只需实现.next
方法就可以使用IteratorImpl
的实现方法。为了方便起见,标准库实现了许多迭代器
classDiagram
class Array~T~ {
...
iter() ArrayIterator~T~
}
class Range~T~ {
...
iter() RangeIterator~T~
}
class Iterable~T~ {
<<trait>>
iter() Iterator~T~
}
Iterable~T~ <|.. Array~T~: Impl
Iterable~T~ <|.. Range~T~: Impl
class ArrayIterator~T~ {
array: Array~T~
next() T
}
class RangeIterator~T~ {
range: Range~T~
next() T
}
class Iterator~T~ {
<<trait>>
next() T
}
Iterator~T~ <|.. ArrayIterator~T~: Impl
Iterator~T~ <|.. RangeIterator~T~: Impl
Array <-- ArrayIterator
Range <-- RangeIterator
诸如 Iterable
之类的以静态分派但统一的方式提供用于处理Trait(在本例中为 Iterator
)的trait的类型称为伴生类适配器
1 这个模式似乎没有统一的名称,但是在 Rust 中,有 [companion struct 模式]( https://gist.github.com/qnighy/be99c2ece6f3f4b1248608a04e104b38# :~:text=%E3%82%8F%E3%82%8C%E3%81%A6%E3%81%84%E3%82 %8B%E3%80%82-,companion%20struct,-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%80% 81%E3 %81%9D%E3%81%AE),并以此命名。↩
可变性
正如我们已经看到的,所有 Erg 变量都是不可变的。但是,Erg 对象具有可变性的概念 以下面的代码为例
a = [1, 2, 3]
a = a + [4, 5, 6]
print! a # [1, 2, 3, 4, 5, 6]
上面的代码实际上不能被 Erg 执行。这是因为它不可重新分配
可以执行此代码
b = ![1, 2, 3]
b.concat! [4, 5, 6]
print! b # [1, 2, 3, 4, 5, 6]
a, b
的最终结果看起来一样,但它们的含义却大不相同
虽然 a
是表示 Nat
数组的变量,但第一行和第二行指向的对象是不同的。名称a
相同,但内容不同
a = [1, 2, 3]
print! id! a # 0x000002A798DFE940
_a = a + [4, 5, 6]
print! id! _a # 0x000002A798DFE980
id!
过程返回对象驻留的内存地址
b
是一个 Nat
"动态" 数组。对象的内容发生了变化,但变量指向的是同一个东西
b = ![1, 2, 3]
print! id! b # 0x000002A798DFE220
b.concat! [4, 5, 6]
print! id! b # 0x000002A798DFE220
i = !0
if! True. do!
do! i.inc!() # or i.add!(1)
do pass
print! i # 1
!
是一个特殊的运算符,称为 mutation 运算符。它使不可变对象可变
标有"!"的对象的行为可以自定义
Point = Class {.x = Int; .y = Int}
# 在这种情况下 .x 是可变的,而 .y 保持不变
Point! = Class {.x = Int!; .y = Int}
Point!.
inc_x! ref!(self) = self.x.update! x -> x + 1
p = Point!.new {.x = !0; .y = 0}
p.inc_x!()
print! p.x # 1
常量
与变量不同,常量在所有范围内都指向同一事物
常量使用 =
运算符声明
PI = 3.141592653589
match! x:
PI => print! "this is pi"
常量在全局以下的所有范围内都是相同的,并且不能被覆盖。因此,它们不能被 =
重新定义。此限制允许它用于模式匹配
True
和 False
可以用于模式匹配的原因是因为它们是常量
此外,常量总是指向不可变对象。诸如 Str!
之类的类型不能是常量
所有内置类型都是常量,因为它们应该在编译时确定。可以生成非常量的类型,但不能用于指定类型,只能像简单记录一样使用。相反,类型是其内容在编译时确定的记录
变量、名称、标识符、符号
让我们理清一些与 Erg 中的变量相关的术语
变量是一种为对象赋予名称以便可以重用(或指向该名称)的机制 标识符是指定变量的语法元素 符号是表示名称的语法元素、记号
只有非符号字符是符号,符号不称为符号,尽管它们可以作为运算符的标识符
例如,x
是一个标识符和一个符号。x.y
也是一个标识符,但它不是一个符号。x
和 y
是符号
即使 x
没有绑定到任何对象,x
仍然是一个符号和一个标识符,但它不会被称为变量
x.y
形式的标识符称为字段访问器
x[y]
形式的标识符称为下标访问器
变量和标识符之间的区别在于,如果我们在 Erg 的语法理论意义上谈论变量,则两者实际上是相同的 在 C 中,类型和函数不能分配给变量; int 和 main 是标识符,而不是变量(严格来说可以赋值,但有限制) 然而,在Erg语中,"一切都是对象"。不仅函数和类型,甚至运算符都可以分配给变量
所有权
由于 Erg 是一种使用 Python 作为宿主语言的语言,因此内存管理的方法取决于 Python 的实现 但语义上 Erg 的内存管理与 Python 的不同。一个显着的区别在于所有权制度和禁止循环引用
所有权
Erg 有一个受 Rust 启发的所有权系统 Rust 的所有权系统通常被认为是深奥的,但 Erg 的所有权系统被简化为直观 在 Erg 中,mutable objects 是拥有的,并且在所有权丢失后无法引用
v = [1, 2, 3].into [Int; !3]
push! vec, x =
vec.push!(x)
vec
# v ([1, 2, 3])的内容归w所有
w = push! v, 4
print! v # 错误: v 被移动了
print!w # [1, 2, 3, 4]
例如,当一个对象被传递给一个子程序时,就会发生所有权转移 如果您想在赠送后仍然拥有所有权,则需要克隆、冻结或借用 但是,如后所述,可以借用的情况有限
复制
复制一个对象并转移其所有权。它通过将 .clone
方法应用于实际参数来做到这一点
复制的对象与原始对象完全相同,但相互独立,不受更改影响
复制相当于 Python 的深拷贝,由于它完全重新创建相同的对象,因此计算和内存成本通常高于冻结和借用 需要复制对象的子例程被称为"参数消耗"子例程
capitalize s: Str!=
s. capitalize!()
s
s1 = !"hello"
s2 = capitalize s1.clone()
log s2, s1 # !"HELLO hello"
冻结
我们利用了不可变对象可以从多个位置引用的事实,并将可变对象转换为不可变对象
这称为冻结。例如,在从可变数组创建迭代器时会使用冻结
由于您不能直接从可变数组创建迭代器,请将其转换为不可变数组
如果您不想破坏数组,请使用 .freeze_map
方法
# 计算迭代器产生的值的总和
sum|T <: Add + HasUnit| i: Iterator T = ...
x = [1, 2, 3].into [Int; !3]
x.push!(4)
i = x.iter()
assert sum(i) == 10
y # y 仍然可以被触摸
借
借用比复制或冻结便宜 可以在以下简单情况下进行借款:
peek_str ref(s: Str!) =
log s
s = !"hello"
peek_str s
借来的值称为原始对象的 reference 您可以"转租"对另一个子例程的引用,但您不能使用它,因为您只是借用它
steal_str ref(s: Str!) =
# 由于日志函数只借用参数,所以可以转租
log s
# 错误,因为丢弃函数消耗了参数
discard s # OwnershipError: 不能消费借来的值
# 提示: 使用 `clone` 方法
steal_str ref(s: Str!) =
# 这也不好(=消耗右边)
x = s # OwnershipError: 不能消费借来的值
x
Erg 的引用比 Rust 的更严格。引用是语言中的一等对象,但不能显式创建,它们只能指定为通过 ref
/ref!
传递的参数
这意味着您不能将引用填充到数组中或创建将引用作为属性的类
但是,这样的限制是语言中的自然规范,一开始就没有引用,而且它们并没有那么不方便
循环引用
Erg 旨在防止无意的内存泄漏,如果内存检查器检测到循环引用,则会发出错误。在大多数情况下,这个错误可以通过弱引用 Weak
来解决。但是,由于无法生成循环图等具有循环结构的对象,因此我们计划实现一个 API,可以将循环引用作为不安全操作生成
可见性
Erg 变量具有 visibility 的概念
到目前为止,我们看到的所有变量都称为 private variables。这是一个外部不可见的变量
例如,foo
模块中定义的私有变量不能被另一个模块引用
# foo.er
x = "this is an invisible variable"
# bar.er
foo = import "foo"
foo.x # AttributeError: 模块 'foo' 没有属性 'x' ('x' 是私有的)
另一方面,也有__public variables__,可以从外部引用
公共变量用.
定义
# foo.er
.x = "this is a visible variable"
# bar.er
foo = import "foo"
assert foo.x == "this is a visible variable"
您不需要向私有变量添加任何内容,但您也可以添加 ::
或 self::
(用于类型等的Self::
)以表明它们是私有的。增加。如果它是一个模块,它也可以是 module::
::x = "this is an invisible variable"
assert ::x == x
assert self ::x == ::x
assert module::x == ::x
In the context of purely sequential execution, private variables are almost synonymous with local variables. It can be referenced from the inner scope.
::x = "this is a private variable"
y =
x + 1 # 完全是 module::x
通过使用::
,可以区分作用域内同名的变量
在左侧指定要引用的变量的范围。为顶层指定 module
如果未指定,则照常引用最里面的变量
::x = 0
assert x == 0
y =
::x = 1
assert x == 1
z =
::x = 2
assert ::x == 2
assert z::x == 2
assert y::x == 1
assert module::x == 0
在匿名子程序作用域中,self
指定了它自己的作用域
x = 0
f = x ->
log module::x, self::x
f1# 0 1
::
还负责访问私有实例属性
x = 0
C = Class {x = Int}
C.
# 顶级 x 被引用(警告使用 module::x)
f1 self = x
# 实例属性 x 被引用
f2 self = self::x
外部模块中的可见性
在一个模块中定义的类实际上可以定义来自外部模块的方法
# foo.er
.Foo = Class()
# bar.er
{Foo;} = import "foo"
Foo::
private self = pass
Foo.
public self = self::private()
.f() =
foo = Foo.new()
foo.public()
foo::private() # 属性错误
但是,这两种方法都只在该模块中可用 外部定义的私有方法对 Foo 类的方法仅在定义模块内可见 公共方法暴露在类之外,但不在模块之外
# baz.er
{Foo;} = import "foo"
foo = Foo.new()
foo.public() # 属性错误: "Foo"没有属性"public"("public"在模块"bar"中定义)
此外,方法不能在要重新导出的类型中定义 这是为了避免混淆方法是否找到,具体取决于导入方法的模块
# bar.er
{.Foo;} = import "foo"
.Foo::
private self = pass # 错误
Foo.
public self = self::private() # 错误
如果你想做这样的事情,定义一个 patch
# bar.er
{Foo;} = import "foo"
FooImpl = Patch Foo
FooImpl :=:
private self = pass
Foo Impl.
public self = self::private()
# baz.er
{Foo;} = import "foo"
{FooImpl;} = import "bar"
foo = Foo.new()
foo.public()
受限公共变量
可变可见性不限于完全公共/私有 您也可以有限制地发布
# foo.er
.record = {
.a = {
.(.record)x = 0
.(module)y = 0
.z = 0
}
_ = .a.x # OK
_ = .a.y # OK
_ = .a.z # OK
}
_ = .record.a.x # 可见性错误
_ = .record.a.y # OK
_ = .record.a.z # OK
foo = import "foo"
_ = foo.record.a.x # 可见性错误
_ = foo.record.a.y # 可见性错误
_ = foo.record.a.z # OK
命名规定
如果要将变量用作常量表达式,请确保它以大写字母开头。两个或多个字母可能是小写的
i: Option Type = Int
match i:
t: Type -> log "type"
None -> log "None"
具有副作用的对象总是以 !
结尾。程序和程序方法,以及可变类型
然而,Proc
类型本身是不可变的
# Callable == Func or Proc
c: Callable = print!
match c:
p! -> log "proc" # `: Proc` 可以省略,因为它是不言自明的
f -> log "func"
如果您想向外界公开一个属性,请在开头使用 .
定义它。如果你不把.
放在开头,它将是私有的。为避免混淆,它们不能在同一范围内共存
o = {x = 1; .x = 2} # 语法错误: 同名的私有变量和公共变量不能共存
文字标识符
可以通过将字符串括在单引号 ('') 中来规避上述规则。也就是说,程序对象也可以在没有 !
的情况下分配。但是,在这种情况下,即使该值是常量表达式,也不会被视为常量
像这样用单引号括起来的字符串称为文字标识符
这在调用Python等其他语言的API(FFI)时使用
bar! = pyimport("foo").'bar'
在 Erg 中也有效的标识符不需要用 '' 括起来
此外,文字标识符可以包含符号和空格,因此通常不能用作标识符的字符串可以用作标识符
'∂/∂t' y
'test 1: pass x to y'()
Lambda
匿名函数是一种无需命名即可动态创建函数对象的语法
# `->` 是匿名函数操作符
# 同 `f x, y = x + y`
f = (x, y) -> x + y
# same as `g(x, y: Int): Int = x + y`
g = (x, y: Int): Int -> x + y
如果只有一个参数,您可以省略 ()
assert [1, 2, 3].map_collect(i -> i + 1) == [2, 3, 4]
assert ((i, j) -> [i, j])(1, 2) == [1, 2]
在下面的情况下,它是 0..9, (i -> ...)
而不是 (0..9, i) -> ...
->
在左侧只接受一个参数。多个参数作为单个元组接收
for 0..9, i: Int ->
...
在匿名函数中,由于空格,解析存在差异
# 在这种情况下,解释为 `T(() -> Int)`
i: T() -> Int
# 在这种情况下,它被解释为 (U()) -> Int
k: U() -> Int
匿名函数可以不带参数使用
# `=>` 是一个匿名过程操作符
p! = () => print! # `p!` 被调用
# `() ->`, `() =>` 有语法糖 `do`, `do!`
# p! = do! print! "`p!` 被调用
p!() # `p!` 被调用
无参数函数可用于延迟初始化
time = import "time"
date = import "datetime"
now = if! True:
do!:
time. sleep! 1000
date.now!()
do date.new("1970", "1", "1", "00", "00")
您还可以键入和模式匹配。正因为如此,match
函数大多是借助匿名函数的力量来实现的
作为 match
函数的参数给出的匿名函数从顶部开始按顺序尝试。因此,您应该在顶部描述特殊情况,在底部描述更一般的情况。如果你弄错了顺序,编译器会发出警告(如果可能的话)
n = (Complex or Ratio or Int).sample!()
i = matchn:
PI -> PI # 如果等于常数 PI
For (i: 1..10) -> i # 整数从 1 到 10
(i: Int) -> i # Int
(c: Complex) -> c.real() # 对于复杂。Int < Complex,但可以回退
_ -> panic "cannot convert to Int" # 如果以上都不适用。match 必须涵盖所有模式
错误处理通常也使用 ?
或 match
完成
res: ParseResult Int
matchres:
i: Int -> i
err: Error -> panic err.msg
res2: Result Int, Error
match res2:
ok: Not Error -> log Type of ok
err: Error -> panic err.msg
匿名多相关系数
# 与此相同 id|T|x: T = x
id = |T| x: T -> x
子程序
函数
some_func(x: T, y: U) -> V
some_func: (T, U) -> V
过程
some_proc!(x: T, y: U) => V
some_proc!: (T, U) => V
函数方法
方法类型不能用Self
在外部指定
.some_method(self, x: T, y: U) => ()
# (Self, T, U) => () 拥有 self 的所有权
.some_method: (Ref(Self), T, U) => ()
过程方法(依赖)
在下文中,假设类型 T!
采用类型参数 N: Nat
。要在外部指定它,请使用类型变量
K!: Nat -> Type
# ~> 表示应用前后类型参数的状态(此时self必须是变量引用)
K!(N).some_method!: (Ref!(K! N ~> N+X), X: Nat) => ()
注意,.some_method
的类型是 | N,X: Nat| (Ref!(K! N ~> N+X), {X}) => ()
对于没有 ref!
的方法,即在应用后被剥夺所有权,不能使用类型参数转换(~>
)
如果取得所有权,则如下所示
# 如果不使用N,可以用_省略
# .some_method!: |N, X: Nat| (T!(N), {X}) => T!(N+X)
.some_method!|N, X: Nat| (self: T!(N), X: Nat) => T!(N+X)
运算符
可以通过用 ` 括起来将其定义为普通函数
中性字母运算符,例如 and
和 or
可以通过用 ` 括起来定义为中性运算符
and(x, y, z) = x and y and z
`_+_`(x: Foo, y: Foo) = x.a + y.a
`-_`(x: Foo) = Foo.new(-x.a)
闭包
Erg子例程有一个称为"闭包"的功能,可以捕获外部变量
outer = 1
f x = outer + x
assert f(1) == 2
与不可变对象一样,可变对象也可以被捕获
sum = !0
for! 1..10, i =>
sum.add!i
assert sum == 45
p!x=
sum.add!x
p!(1)
assert sum == 46
但是请注意,函数不能捕获可变对象 如果可以在函数中引用可变对象,则可以编写如下代码
# !!! 这段代码实际上给出了一个错误!!!
i = !0
f x = i + x
assert f 1 == 1
i.add! 1
assert f 1 == 2
该函数应该为相同的参数返回相同的值,但假设被打破了
请注意,i
仅在调用时进行评估
如果您想在定义函数时获取可变对象的内容,请调用.clone
i = !0
immut_i = i.clone().freeze()
fx = immut_i + x
assert f 1 == 1
i.add! 1
assert f 1 == 1
avoid mutable state, functional programming
# Erg
sum = !0
for! 1..10, i =>
sum.add!i
assert sum == 45
上面的等效程序可以用Python编写如下:
# Python
sum = 0
for i in range(1, 10):
sum += i
assert sum == 45
但是,Erg 建议使用更简单的表示法 与其使用子例程和可变对象来传递状态,不如使用一种使用函数来定位状态的风格。这称为函数式编程
# 功能风格
sum = (1..10).sum()
assert sum == 45
上面的代码给出了与之前完全相同的结果,但是您可以看到这个代码要简单得多
fold
函数可以用来做比sum更多的事情
fold
是一个迭代器方法,它为每次迭代执行参数f
累加结果的计数器的初始值在init
中指定,并在acc
中累加
# 从0开始,结果会
sum = (1..10).fold(init: 0, f: (acc, i) -> acc + i)
assert sum == 45
Erg被设计为对使用不可变对象进行编程的自然简洁描述
模块
Erg允许您将文件本身视为单个记录(Record)。这称为模块
# foo.er
.i = 1
# 定义 foo 模块与定义这条记录几乎相同
foo = {.i = 1}
# bar.er
foo = import "foo"
print! foo # <module 'foo'>
assert foo.i == 1
由于模块类型也是记录类型,因此可以进行解构赋值
# 和 {sin; cos; ...} = import "math" 一样
{sin; cos} = import "math"
模块可见性
目录和文件都可以是模块
但是,在默认情况下,Erg不将目录识别为Erg模块。要让它被识别,创建一个名为__init__.er
的文件
__init__.er
类似于Python中的__init__.py
└─┬ bar
└─ __init__.er
现在bar
目录被识别为一个模块。如果bar
中的唯一文件是__init__.er
,则目录结构没有多大意义,但如果您想将多个模块捆绑到一个模块中,它会很有用。例如:
└─┬ bar
├─ __init__.er
├─ baz.er
└─ qux.er
在bar
目录之外,您可以像下面这样使用
bar = import "bar"
bar.baz.p!()
bar.qux.p!()
__init__.er
不仅仅是一个将目录作为模块的标记,它还控制模块的可见性
# __init__.er
# `. /` 指向当前目录。可以省略
.baz = import ". /baz"
qux = import ". /qux"
.f x =
.baz.f ...
.g x =
qux.f ...
当你从外部导入 bar
模块时,baz
模块可以访问,但 qux
模块不能。
循环依赖
Erg 允许您定义模块之间的循环依赖关系。
# foo.er
bar = import "bar"
print! bar.g 1
.f x = x
# bar.er
foo = import "foo"
print! foo.f 1
.g x = x
但是,由过程调用创建的变量不能在循环引用模块中定义 这是因为 Erg 根据依赖关系重新排列定义的顺序
# foo.er
bar = import "bar"
print! bar.x
.x = g!(1) # 模块错误:由过程调用创建的变量不能在循环引用模块中定义
# bar.er
foo = import "foo"
print! foo.x
.x = 0
此外,作为入口点的 Erg 模块(即 __name__ == "__main__"
的模块)不能成为循环引用的主题
对象系统
可以分配给变量的所有数据。Object
类的属性如下
.__repr__
: 返回对象的(非丰富)字符串表示.__sizeof__
: 返回对象的大小(包括堆分配).__dir__
: 返回对象属性列表.__hash__
: 返回对象的哈希值.__getattribute__
: 获取并返回对象的属性.clone
: 创建并返回一个对象的克隆(在内存中有一个独立的实体).copy
: 返回对象的副本(指向内存中的同一事物)
记录
由记录文字({attr = value; ...}
)生成的对象
这个对象有基本的方法,比如.clone
和.__sizeof__
obj = {.x = 1}
assert obj.x == 1
obj2 = {*x; .y = 2}
assert obj2.x == 1 and obj2.y == 2
属性
与对象关联的对象。特别是,将 self (self
) 作为其隐式第一个参数的子例程属性称为方法
# 请注意,private_attr 中没有`.`
record = {.public_attr = j; private_attr = 2; .method = self -> self.i + 1}
record. public_attr == 2
record.private_attr # AttributeError: private_attr 是私有的
assert record.method() == 3
元素
属于特定类型的对象(例如,"1"是"Int"类型的元素)。所有对象至少是{=}
类型的元素
类的元素有时称为实例
子程序
表示作为函数或过程(包括方法)实例的对象。代表子程序的类是"子程序"
实现 .__call__
的对象通常称为 Callable
可调用
一个实现.__call__
的对象。它也是 Subroutine
的父类
类型
定义需求属性并使对象通用化的对象
主要有两种类型: 多态类型和单态类型。典型的单态类型有Int
、Str
等,多态类型有Option Int
、[Int; 3]
等
此外,定义改变对象状态的方法的类型称为 Mutable 类型,需要在变量属性中添加 !
(例如动态数组: [T; !_]
)
班级
具有 .__new__
、.__init__
方法等的类型。实现基于类的面向对象
功能
对外部变量(不包括静态变量)有读权限但对外部变量没有读/写权限的子程序。换句话说,它没有外部副作用 Erg 函数的定义与 Python 的不同,因为它们不允许副作用
程序
它对外部变量具有读取和"自我"权限,对静态变量具有读/写权限,并允许使用所有子例程。它可能有外部副作用
方法
隐式将"self"作为第一个参数的子例程。它与简单的函数/过程是不同的类型
实体
不是子例程和类型的对象
单态实体(1
、"a"
等)也称为值对象,多态实体([1, 2, 3], {"a": 1}
)也称为容器对象
模式匹配
Erg 中可用的模式
变量模式
# 基本任务
i = 1
# 有类型
i: Int = 1
# 匿名类型
i: {1, 2, 3} = 2
# 功能
fn x = x + 1
# 等于
fn x: Add(Int) = x + 1
# (匿名)函数
fn = x -> x + 1
fn: Int -> Int = x -> x + 1
# 高阶类型
a: [Int; 4] = [0, 1, 2, 3]
# or
a: Array Int, 4 = [0, 1, 2, 3]
文字字面量
# 如果在编译时无法确定`i`为 1,则引发TypeError
# 省略 `_: {1} = i`
1 = i
# 简单的模式匹配
match x:
1 -> "1"
2 -> "2"
_ -> "other"
# 斐波那契函数
fib0 = 0
fib1 = 1
fibn: Nat = fibn-1 + fibn-2
常量模式
cond=False
match! cond:
True => print! "cond is True"
_ => print! "cond is False"
PI = 3.141592653589793
E = 2.718281828459045
num = PI
name = match num:
PI -> "pi"
E -> "e"
_ -> "unnamed"
筛子图案
# 这两个是一样的
Array(T, N: {N | N >= 3})
Array(T, N | N >= 3)
f M, N | M >= 0, N >= 1 = ...
f(1, 0) # 类型错误: N(第二个参数)必须为 1 或更多
丢弃(通配符)模式
_ = 1
_: Int = 1
zero_ = 0
right(_, r) = r
可变长度模式
它与稍后描述的元组/数组/记录模式结合使用
[i, *j] = [1, 2, 3, 4]
assert j == [2, 3, 4]
first|T|(fst: T, *rest: T) = fst
assert first(1, 2, 3) == 1
元组模式
(i, j) = (1, 2)
((k, l), _) = ((1, 2), (3, 4))
# 如果不嵌套,() 可以省略(1, 2 被视为(1, 2))
m, n = 1, 2
f(x, y) = ...
数组模式
[i, j] = [1, 2]
[[k, l], _] = [[1, 2], [3, 4]]
length [] = 0
length [_, *rest] = 1 + length rest
record 模式
record = {i = 1; j = 2; k = 3}
{j;} = record # i, k 将被释放
{sin; cos; tan;} = import "math"
{*} = import "math" # import all
person = {name = "John Smith"; age = 20}
age = match person:
{name = "Alice"; _} -> 7
{_; age} -> age
f {x: Int; y: Int} = ...
数据类模式
Point = Inherit {x = Int; y = Int}
p = Point::{x = 1; y = 2}
Point::{x; y} = p
Nil T = Class Impl := Phantom T
Cons T = Inherit {head = T; rest = List T}
List T = Enum Nil(T), Cons(T)
List T.
first self =
match self:
Cons::{head; ...} -> x
_ -> ...
second self =
match self:
Cons::{rest=Cons::{head; ...}; ...} -> head
_ -> ...
枚举模式
- 其实只是枚举类型
match x:
i: {1, 2} -> "one or two: \{i}"
_ -> "other"
Range 模式
- 实际上,它只是一个区间类型
# 0 < i < 1
i: 0<..<1 = 0.5
# 1 < j <= 2
_: {[I, J] | I, J: 1<..2} = [1, 2]
# 1 <= i <= 5
match i
i: 1..5 -> ...
不是模式的东西,不能被模式化的东西
模式是可以唯一指定的东西。在这方面,模式匹配不同于普通的条件分支
条件规格不是唯一的。例如,要检查数字 n
是否为偶数,正统是 n % 2 == 0
,但也可以写成 (n / 2).round() == n / 2
非唯一形式无论是正常工作还是等效于另一个条件都不是微不足道的
Set
没有固定的模式。因为集合没有办法唯一地检索元素 您可以通过迭代器检索它们,但不能保证顺序
推导式
Array和[expr | (name <- iterable)+ (predicate)*]
,
set和{expr | (name <- iterable)+ (predicate)*}
,
你可以创建一个字典{key: value | (name <- iterable)+ (predicate)*}
.
由|
分隔的子句的第一部分称为布局子句(位置子句),第二部分称为绑定子句(绑定子句),第三部分称为保护子句(条件子句)
保护子句可以省略,但绑定子句不能省略,保护子句不能在绑定子句之前
理解示例
# 布局子句是 i
# 绑定子句是 i <- [0, 1, 2]
assert [i | i <- [0, 1, 2]] == [0, 1, 2]
# 布局子句是 i / 2
# 绑定子句是 i <- 0..2
assert [i/2 | i <- 0..2] == [0.0, 0.5, 1.0]
# 布局子句是 (i, j)
# 绑定子句 i <- 0..2, j <- 0..2
# 保护子句是 (i + j) % 2 == 0
assert [(i, j) | i <- 0..2; j <- 0..2; (i + j) % 2 == 0] == [(0, 0), (0, 2), (1, 1), (2, 0), (2, 2)]
assert {i % 2 | i <- 0..9} == {0, 1}
assert {k: v | k <- ["a", "b"]; v <- [1, 2]} == {"a": 1, "b": 2}
Erg推导式受到Haskell的启发,但有一些不同 对于Haskell列表推导,变量的顺序会对结果产生影响,但在Erg中这并不重要
-- Haskell
[(i, j) | i <- [1..3], j <- [3..5]] == [(1,3),(1,4),(1,5),(2 ,3),(2,4),(2,5),(3,3),(3,4),(3,5)]
[(i, j) | j <- [3..5], i <- [1..3]] == [(1,3),(2,3),(3,3),(1 ,4),(2,4),(3,4),(1,5),(2,5),(3,5)]
# Erg
assert [(i, j) | i <- 1..<3; j <- 3..<5] == [(i, j) | j <- 3..<5; i <- 1.. <3]
该规范与Python的规范相同
# Python
assert [(i, j) for i in range(1, 3) for j in range(3, 5)] == [(i, j) for j in range(3, 5) for i in range(1, 3)]
筛子类型
与推导类似的是筛类型。筛子类型是以{Name: Type | Predicate}
创建的(枚举类型)
refinement类型的情况下,只能指定一个Name,不能指定布局(但是如果是tuple类型可以处理多个值),Predicate可以在编译时计算,即 ,只能指定一个常量表达式
Nat = {I: Int | I >= 0}
# 如果谓词表达式只有and,可以替换为:
# Nat2D = {(I, J): (Int, Int) | I >= 0; J >= 0}
Nat2D = {(I, J): (Int, Int) | I >= 0 and J >= 0}
扩展语法
在分解赋值中,将 *
放在变量前面会将所有剩余元素展开到该变量中。这称为展开赋值
[x, *y] = [1, 2, 3]
assert x == 1
assert y == [2, 3]
x, *y = (1, 2, 3)
assert x == 1
assert y == (2, 3)
提取赋值
提取分配是一种方便的语法,用于本地化模块或记录中的特定属性
{sin; cos; tan} = import "math"
之后,您可以在本地使用sin,cos,tan
您可以对记录执行相同的操作。
record = {x = 1; y = 2}
{x; y} = record
如果要全部展开,请使用{*}=record
。它在OCaml中是open
。
record = {x = 1; y = 2}
{*} = records
assert x == 1 and y == 2
装饰器
装饰器用于向类型或函数添加或演示特定状态或行为 装饰器的语法如下
@deco
X = ...
你可以有多个装饰器,只要它们不冲突
装饰器不是一个特殊的对象,它只是一个单参数函数。装饰器等价于下面的伪代码
X = ...
X = deco(X)
Erg 不允许重新分配变量,因此上面的代码不起作用
对于简单的变量,它与X = deco(...)
相同,但对于即时块和子例程,你不能这样做,所以你需要一个装饰器
@deco
f x =
y = ...
x + y
# 还可以防止代码变成水平的
@LongNameDeco1
@LongNameDeco2
C = Class ...
下面是一些常用的内置装饰器
可继承
指示定义类型是可继承的类。如果为参数 scope
指定 "public"
,甚至可以继承外部模块的类。默认情况下它是"private"
,不能被外部继承
最后
使该方法不可覆盖。将它添加到类中使其成为不可继承的类,但由于它是默认值,因此没有意义
覆盖
覆盖属性时使用。默认情况下,如果您尝试定义与基类相同的属性,Erg将抛出错误
实现
表示参数trait已实现
Add = Trait {
.`_+_` = Self.(Self) -> Self
}
Sub = Trait {
.`_-_` = Self.(Self) -> Self
}
C = Class({i = Int}, Impl := Add and Sub)
C.
@Impl Add
`_+_` self, other = C.new {i = self::i + other::i}
@Impl Sub
`_-_` self, other = C.new {i = self::i - other::}
附
指定默认情况下随trait附带的附件补丁 这允许您重现与Rust Trait相同的行为
# foo.er
Add R = Trait {
.AddO = Type
.`_+_` = Self.(R) -> Self.AddO
}
@Attach AddForInt, AddForOdd
ClosedAdd = Subsume Add(Self)
AddForInt = Patch(Int, Impl := ClosedAdd)
AddForInt.AddO = Int
AddForOdd = Patch(Odd, Impl := ClosedAdd)
AddForOdd.AddO = Even
当从其他模块导入Trait时,这将自动应用附件补丁
# 本来应该同时导入IntIsBinAdd和OddIsBinAdd,但是如果是附件补丁可以省略
{BinAdd;} = import "foo"
assert Int. AddO == Int
assert Odd.AddO == Even
在内部,它只是使用trait的.attach方法附加的。可以使用trait的.detach
方法消除冲突
@Attach X
T = Trait ...
assert X in T. attaches
U = T.detach(X).attach(Y)
assert X not in U. attaches
assert Y in U. attaches
已弃用
指示变量规范已过时且不推荐使用
测试
表示这是一个测试子例程。测试子程序使用erg test
命令运行
错误处理系统
主要使用Result类型 在Erg中,如果您丢弃Error类型的对象(顶层不支持),则会发生错误
异常,与 Python 互操作
Erg没有异常机制(Exception)。导入Python函数时
- 将返回值设置为
T 或 Error
类型 T or Panic
类型(可能导致运行时错误)
有两个选项,pyimport
默认为后者。如果要作为前者导入,请使用
在pyimport
exception_type
中指定Error
(exception_type: {Error, Panic}
)
异常和结果类型
Result
类型表示可能是错误的值。Result
的错误处理在几个方面优于异常机制
首先,从类型定义中可以看出子程序可能会报错,实际使用时也很明显
# Python
try:
x = foo().bar()
y = baz()
qux()
except e:
print(e)
在上面的示例中,仅凭此代码无法判断哪个函数引发了异常。即使回到函数定义,也很难判断函数是否抛出异常
# Erg
try!:
do!:
x = foo!()?.bar()
y = baz!()
qux!()?
e =>
print! e
另一方面,在这个例子中,我们可以看到foo!
和qux!
会引发错误
确切地说,y
也可能是Result
类型,但您最终必须处理它才能使用里面的值
使用 Result
类型的好处不止于此。Result
类型也是线程安全的。这意味着错误信息可以(轻松)在并行执行之间传递
语境
由于Error
/Result
类型本身不会产生副作用,不像异常,它不能有发送位置(Context)等信息,但是如果使用.context
方法,可以将信息放在错误
对象。可以添加。.context
方法是一种使用Error
对象本身并创建新的 Error
对象的方法。它们是可链接的,并且可以包含多个上下文
f() =
todo() \
.context "to be implemented in ver 1.2" \
.context "and more hints ..."
f()
# Error: not implemented yet
# hint: to be implemented in ver 1.2
# hint: and more hints ...
请注意,诸如 .msg
和 .kind
之类的 Error
属性不是次要的,因此它们不是上下文,并且不能像最初创建时那样被覆盖
堆栈跟踪
Result
类型由于其方便性在其他语言中经常使用,但与异常机制相比,它的缺点是难以理解错误的来源
因此,在 Erg 中,Error
对象具有名为 .stack
的属性,并再现了类似伪异常机制的堆栈跟踪
.stack
是调用者对象的数组。每次 Error 对象被return
(包括通过?
)时,它都会将它的调用子例程推送到.stack
如果它是 ?
ed 或 .unwrap
ed 在一个不可能 return
的上下文中,它会因为回溯而恐慌
f x =
...
y = foo.try_some(x)?
...
g x =
y = f(x)?
...
i = g(1)?
# Traceback (most recent call first):
# ...
# Foo.try_some, line 10, file "foo.er"
# 10 | y = foo.try_some(x)?
# module::f, line 23, file "foo.er"
# 23 | y = f(x)?
# module::g, line 40, file "foo.er"
# 40 | i = g(1)?
# Error: ...
恐慌
Erg 还有一种处理不可恢复错误的机制,称为 panicing 不可恢复的错误是由外部因素引起的错误,例如软件/硬件故障、严重到无法继续执行代码的错误或程序员未预料到的错误。等如果发生这种情况,程序将立即终止,因为程序员的努力无法恢复正常运行。这被称为"恐慌"
恐慌是通过 panic
功能完成的
panic "something went wrong!"
管道运算符
管道运算符的使用方式如下:
assert f(g(x)) == (x |> g() |> f())
assert f(g(x, y)) == (x |> g(y) |> f())
换句话说,Callable(object)
的顺序可以更改为object |> Callable()
管道运算符也可用于方法。对于方法,object.method(args)
更改为object |>.method(args)
它看起来只是更多的|>
,但由于粘合强度较低,您可以减少()
的数量
rand = -1.0..1.0 |>.sample!()
log rand # 0.2597...
1+1*2 |>.times do log("a", end := "") # aaa
evens = 1..100 |>.iter() |>.filter i -> i % 2 == 0 |>.collect Array
# 在没有管道操作符的情况下实现,
_evens = (1..100).iter().filter(i -> i % 2 == 0).collect(Array)
# or
__evens = 1..100 \
.iter() \
.filter i -> i % 2 == 0 \
.collect Array
与 Python 集成
导出到 Python
编译 Erg 脚本时,会生成一个 .pyc 文件,可以简单地将其作为 Python 模块导入 但是,无法从 Python 访问在 Erg 端设置为私有的变量
# foo.er
.public = "this is a public variable"
private = "this is a private variable"
erg --compile foo.er
import foo
print(foo.public)
print(foo.private) # 属性错误:
从 Python 导入
默认情况下,从 Python 导入的所有对象都是"Object"类型。由于此时无法进行比较,因此有必要细化类型
标准库中的类型规范
Python 标准库中的所有 API 都是由 Erg 开发团队指定的类型
time = pyimport "time"
time.sleep! 1
用户脚本的类型规范
创建一个类型为 Python foo
模块的 foo.d.er
文件
Python 端的类型提示被忽略,因为它们不是 100% 保证的
# foo.py
X = ...
def bar(x):
...
def baz():
...
...
# foo.d.er
foo = pyimport "foo"
.X = declare foo.'X', Int
.bar = declare foo.'bar', Int -> Int
.baz! = declare foo.'baz', () => Int
foo = pyimport "foo"
assert foo.bar(1) in Int
这通过在运行时执行类型检查来确保类型安全。declare
函数大致如下工作
declare|S: Subroutine| sub!: S, T =
# 实际上,=> 可以强制转换为没有块副作用的函数
x =>
assert x in T.Input
y = sub!(x)
assert y in T.Output
y
由于这是运行时开销,因此计划使用 Erg 的类型系统对 Python 脚本进行静态类型分析
包系统
Erg包大致可以分为app包,即应用程序,以及lib包,即库
应用包的入口点是src/app.er
。app.er
中定义的main
函数被执行
lib 包的入口点是src/lib.er
。导入包相当于导入 lib.er
一个包有一个称为模块的子结构,在 Erg 中是一个 Erg 文件或由 Erg 文件组成的目录。外部 Erg 文件/目录是作为模块对象的可操作对象
为了将目录识别为模块,有必要在目录中放置一个"(目录名称).er"文件
这类似于 Python 的 __init__.py
,但与 __init__.py
不同的是,它放在目录之外
例如,考虑以下目录结构
└─┬ ./src
├─ app.er
├─ foo.er
├─ bar.er
└─┬ bar
├─ baz.er
└─ qux.er
您可以在 app.er
中导入 foo
和 bar
模块。由于 bar.er
文件,bar
目录可以被识别为一个模块
foo
模块是由文件组成的模块,bar
模块是由目录组成的模块。bar
模块还包含 baz
和 qux
模块
该模块只是 bar
模块的一个属性,可以从 app.er
访问,如下所示
# app.er
foo = import "foo"
bar = import "bar"
baz = bar.baz
# or `baz = import "bar/baz"`
main args =
...
请注意用于访问子模块的 /
分隔符。这是因为可以有诸如 bar.baz.er
之类的文件名
不鼓励使用此类文件名,因为 .er
前缀在 Erg 中是有意义的
例如,用于测试的模块。以 .test.er
结尾的文件是一个(白盒)测试模块,它在运行测试时执行一个用 @Test
修饰的子例程
└─┬ ./src
├─ app.er
├─ foo.er
└─ foo.test.er
./src
```python
# app.er
foo = import "foo"
main args =
...
此外,以 .private.er 结尾的文件是私有模块,只能由同一目录中的模块访问
└─┬
├─ foo.er
├─ bar.er
└─┬ bar
├─ baz.private.er
└─ qux.er
# foo.er
bar = import "bar"
bar.qux
bar.baz # AttributeError: module 'baz' is private
# qux.er
baz = import "baz"
生成器
生成器是在块中使用 yield!
过程的特殊过程
g!() =
yield! 1
yield! 2
yield! 3
yield!
是在调用self!.yield!
的子程序块中定义的过程。和return
一样,它把传递给它的值作为返回值返回,但它具有保存block当前执行状态,再次调用时从头开始执行的特性
生成器既是过程又是迭代器; Python 生成器是一个创建迭代器的函数,而 Erg 直接迭代。过程本身通常不是可变对象(没有!
),但生成器是可变对象,因为它自己的内容可以随着每次执行而改变
# Generator!
g!: Generator!((), Int)
assert g!() == 1
assert g!() == 2
assert g!() == 3
Python 风格的生成器可以定义如下
make_g() = () =>
yield! 1
yield! 2
yield! 3
make_g: () => Generator!
Erg 的语法(版本 0.1.0, 临时)
special_op ::= '=' | '->' | '=>' | '.' | ',' | ':' | '::' | '|>' | '&'
separator ::= ';' | '\n'
escape ::= '\'
comment_marker ::= '#'
reserved_symbol ::= special_op | separator | comment_marker
number ::= [0-9]
first_last_dight ::= number
dight ::= number | '_'
bin_dight ::= [0-1]
oct_dight ::= [0-8]
hex_dight ::= [0-9]
| [a-f]
| [A-F]
int ::= first_last_dight
| first_last_dight dight* first_last_dight
| '0' ('b' | 'B') binary_dight+
| '0' ('o' | 'O') octa_dight+
| '0' ('x' | 'X') hex_dight+
ratio ::= '.' dight* first_last_dight
| first_last_dight dight* '.' dight* first_last_dight
bool ::= 'True' | 'False'
none ::= 'None'
ellipsis ::= 'Ellipsis'
not_implemented ::= 'NotImplemented'
parenthesis ::= '(' | ')'
bracket ::= '{' | '}'
square_bracket ::= '[' | ']'
enclosure ::= parenthesis | bracket | square_bracket
infix_op ::= '+' | '-' | '*' | '/' | '//' | '**'
| '%' | '&&' | '||' | '^^' | '<' | '<=' | '>' | '>='
| 'and' | 'or' | 'is' | 'as' | 'isnot' | 'in' | 'notin' | 'dot' | 'cross'
prefix_op ::= '+' | '-' | '*' | '**' | '..' | '..<' | '~' | '&' | '!'
postfix_op ::= '?' | '..' | '<..'
operator ::= infix_op | prefix_op | postfix_op
char ::= /* ... */
str ::= '\"' char* '\"
symbol_head ::= /* char except dight */
symbol ::= symbol_head /* char except (reserved_symbol | operator | escape | ' ') */
subscript ::= accessor '[' expr ']'
attr ::= accessor '.' symbol
accessor ::= symbol | attr | subscript
literal ::= int | ratio | str | bool | none | ellipsis | not_implemented
pos_arg ::= expr
kw_arg ::= symbol ':' expr
arg ::= pos_arg | kw_arg
enc_args ::= pos_arg (',' pos_arg)* ','?
args ::= '()' | '(' arg (',' arg)* ','? ')' | arg (',' arg)*
var_pattern ::= accessor | `...` accessor | '[' single_patterns ']'
var_decl_opt_t = var_pattern (':' type)?
var_decl = var_pattern ':' type
param_pattern ::= symbol | `...` symbol | literal | '[' param_patterns ']'
param_decl_opt_t = param_pattern (':' type)?
param_decl = param_pattern ':' type
params_opt_t ::= '()' (':' type)?
| '(' param_decl_opt_t (',' param_decl_opt_t)* ','? ')' (':' type)?
| param_decl_opt_t (',' param_decl_opt_t)*
params ::= '()' ':' type
| '(' param_decl (',' param_decl)* ','? ')' ':' type
subr_decl ::= accessor params
subr_decl_opt_t ::= accessor params_opt_t
decl ::= var_decl | subr_decl
decl_opt_t = var_decl_opt_t | subr_decl_opt_t
body ::= expr | indent line+ dedent
def ::= ('@' decorator '\n')* decl_opt_t '=' body
call ::= accessor args | accessor call
decorator ::= call
lambda_func ::= params_opt_t '->' body
lambda_proc ::= params_opt_t '=>' body
lambda ::= lambda_func | lambda_proc
normal_array ::= '[' enc_args ']'
array_comprehension ::= '[' expr | (generator)+ ']'
array ::= normal_array | array_comprehension
record ::= '{' '=' '}'
| '{' def (';' def)* ';'? '}'
set ::= '{' '}'
| '{' expr (',' expr)* ','? '}'
dict ::= '{' ':' '}'
| '{' expr ':' expr (',' expr ':' expr)* ','? '}'
tuple ::= '(' ')'
| '(' expr (',' expr)* ','? ')'
indent ::= /* ... */
expr ::= accessor | literal
| prefix | infix | postfix
| array | record | set | dict | tuple
| call | def | lambda
line ::= expr separator+
program ::= expr? | (line | comment)*
索引
有关不在此索引中的 API,请参阅 此处
有关术语,请参见 此处
符号
- ! → side effect
- !-type → mutable type
- ? → error handling
- # → Str
- $ → shared
- %
- &
- &&
- ′ (single quote)
- " (double quote)
- () → Tuple
- *
- + (前置) → operator
- +_ → + (前置)
- + (中置) → operator
- + (中置) → Trait
- ,
- − (前置)
- −_ → − (前置)
- − (中置) → operator
- − (中置) → Trait
- −> → anonymous function
- . → Visibility
- /
- :
- : → Colon application style
- : → Declaration
- : → Keyword Arguments
- :: → visibility
- := → default parameters
- ;
- <
- <: → Subtype specification
- <<
- <=
- = → Variable
- ==
- => → procedure
- >
- >>
- >=
- @ → decorator
- [] → Array
- \ → Indention
- \ → Str
- ^
- ^^
- _ → Type erasure
- _+_ → + (infix)
- _-_ → − (infix)
- `` (back quote)
- {}
- {:}
- {=} → Type System
- |
- || → Type variable list
- ~
拉丁字母
A
- [Add]
- alias
- Aliasing
- algebraic type
- [And]
- [and]
- anonymous function
- Anonymous polycorrelation coefficient
- anonymous type → Type System
- Array
- [assert]
- Attach
- attribute
- Attribute definitions
- Attribute Type
B
C
- Cast
- Comments
- Complex Object
- Compile-time functions
- circular references
- Class
- Class Relationship
- Class upcasting
- Colon application style
- Closure
- Compound Literals
- Complement
- Comprehension
- constant
- Constants
- Context
D
- Data type
- Declaration
- decorator
- Default parameters
- Del
- Dependent Type
- Deconstructing a record
- Deprecated
- Dict
- Diff
- Difference from Data Class
- Difference from structural types
- distinct
- Downcasting
E
- Empty Record
- Enum Class
- Enum type
- Enumerated, Interval and Refinement Types
- error handling
- Existential type
- Exponential Literal
- Extract assignment
F
- False → Boolean Object
- Float&sbsp;Object
- for
- For-All Patch
- For all types
- freeze
- Function
- Function definition with multiple patterns
G
H
I
- id
- if
- import
- impl
- [in]
- Indention
- Instant Block
- Instance and class attributes
- Implementing and resolving duplicate traits in the API
- inheritable
- inheritance
- Inheritance of Enumerated Classes
- Int
- Integration with Python
- Interval Type
- Intersection
- Iterator
J
K
L
- lambda → anonymous function
- let-polymorphism → [rank 1 polymorphism]
- Literal Identifiers
- log → side effect
M
- [match]
- Marker Trait
- Method
- Modifier → decorator
- module
- Multiple Inheritance
- Multi-layer (multi-level) Inheritance
- Mutable Type
- Mutable Structure Type
- Mutability
N
- Nat
- [Never]
- New type
- Heterogeneous Dict
- None → [None Object]
- [None Object]
- Nominal Subtyping → Class
- [Not]
- [not]
O
- Object
- [Option]
- [Or]
- [or]
- [Ord]
- ownership system
- Overloading
- Overriding
- Override in Trait
P
- Panic
- Patch
- Pattern match
- Phantom class
- pipeline operator
- Predicate
- [print!]
- Procedures
- Projection Type
- Python → Integration with Python
Q
R
- Range Object
- [ref]
- [ref!]
- Record
- Record Type Composite
- Recursive functions
- Refinement pattern
- Refinement Type
- replication
- Replacing Traits
- Result → error handling
- Rewriting Inherited Attributes
- rootobj
S
- Script
- Selecting Patches
- self
- Self
- Shared Reference
- side-effect
- Smart Cast
- Spread assignment
- special type variables
- Stack trace
- Structure type
- Structural Patch
- Structural Trait
- Structural Subtyping
- Structural types and class type relationships
- Str
- Subtyping
- Subtyping of subroutines
- Subtype specification
- Subtyping of Polymorphic Function Types
- Subroutine Signatures
T
- Test
- Traits
- Trait inclusion
- True → Boolean Object
- True Algebraic type
- [Type]
- type
- Type arguments in method definitions
- Type Bound
- Type Definitions
- Type erasure
- Type Inference System
- Type specification
- Type System
- Type Widening
- Tuple
U
V
W
- [while]
X
Y
Z
快速浏览
syntax
下面的文档是为了让编程初学者也能理解而编写的
对于已经掌握 Python、Rust、Haskell 等语言的人来说,可能有点啰嗦
所以,这里是 Erg 语法的概述 请认为未提及的部分与 Python 相同
基本计算
Erg 有一个严格的类型。但是, 由于类和Trait提供的灵活性, 类型会自动转换为子类型(有关详细信息,请参阅 [API])
另外,不同的类型可以相互计算,只要类型是数值类型即可
a = 1 # 1: Nat
b = a - 10 # -9: Int
c = b / 2 # -4.5: Float
d = c * 0 # -0.0: Float
e = f // 2 # 0: Nat
如果您不想允许这些隐式类型转换,您可以在声明时指定类型以在编译时将它们检测为错误
a = 1
b: Int = a / 2
# 错误信息
Error[#0047]: File <stdin>, line 1, in <module>
2│ b: Int = int / 2
^
类型错误: ratio的类型不匹配:
期待: Int
但找到: Float
布尔类型
True
和 False
是 Boolean 类型的单例,但它们也可以转换为 Int 类型
因此,如果它们是 Int 类型,则可以进行比较,但与其他类型比较会导致错误
True == 1 # OK
False == 0 # OK
True == 1.0 # NG
False == 0.0 # NG
True == "a" # NG
变量,常量
变量用 =
定义。与 Haskell 一样,变量一旦定义就不能更改。但是,它可以在另一个范围内被遮蔽
i = 0
if True:
i = 1
assert i == 0
任何以大写字母开头的都是常数。只有可以在编译时计算的东西才能是常量 此外,自定义以来,常量在所有范围内都是相同的
PI = 3.141592653589793
match random.random!(0..10):
PI ->
log "You get PI, it's a miracle!"
类型声明
与 Python 不同的是,只能先声明变量类型 当然,声明的类型和实际分配的对象的类型必须兼容
i: Int
i = 10
函数
你可以像在 Haskell 中一样定义它
fib 0 = 0
fib 1 = 1
fib n = fib(n - 1) + fib(n - 2)
匿名函数可以这样定义:
i -> i + 1
assert [1, 2, 3].map(i -> i + 1).to_arr() == [2, 3, 4]
运算符
特定于 Erg 的运算符是:
变异运算符 (!)
这就像 Ocaml 中的ref
i = !0
i.update! x -> x + 1
assert i == 1
程序
具有副作用的子例程称为过程,并标有!
您不能在函数中调用过程
print! 1 # 1
泛型函数(多相关)
id|T|(x: T): T = x
id(1): Int
id("a"): Str
记录
您可以使用类似 ML 的语言中的记录等价物(或 JS 中的对象字面量)
p = {x = 1; y = 2}
assert p.x == 1
所有权
Ergs 由可变对象(使用 !
运算符突变的对象)拥有,并且不能从多个位置重写
i = !0
j = i
assert j == 0
i# 移动错误
另一方面,不可变对象可以从多个位置引用
可见性
使用 .
前缀变量使其成为公共变量并允许从外部模块引用它
# foo.er
.x = 1
y = 1
foo = import "foo"
assert foo.x == 1
foo.y # 可见性错误
模式匹配
变量模式
# 基本任务
i = 1
# with 类型
i: Int = 1
# 函数
fn x = x + 1
fn: Int -> Int = x -> x + 1
文字模式
# 如果 `i` 在编译时无法确定为 1,则发生 类型错误
# 简写: `_ {1} = i`
1 = i
# 简单的模式匹配
match x:
1 -> "1"
2 -> "2"
_ -> "other"
# 斐波那契函数
fib0 = 0
fib1 = 1
fibn: Nat = fibn-1 + fibn-2
常量模式
PI = 3.141592653589793
E = 2.718281828459045
num = PI
name = match num:
PI -> "pi"
E -> "e"
_ -> "unnamed"
丢弃(通配符)模式
_ = 1
_: Int = 1
right(_, r) = r
可变长度模式
与稍后描述的元组/数组/记录模式结合使用
[i, *j] = [1, 2, 3, 4]
assert j == [2, 3, 4]
first|T|(fst: T, *rest: T) = fst
assert first(1, 2, 3) == 1
元组模式
(i, j) = (1, 2)
((k, l), _) = ((1, 2), (3, 4))
# 如果不嵌套,() 可以省略(1, 2 被视为(1, 2))
m, n = 1, 2
数组模式
length [] = 0
length [_, *rest] = 1 + length rest
记录模式
{sin; cos; tan} = import "math"
{*} = import "math" # 全部导入
person = {name = "John Smith"; age = 20}
age = match person:
{name = "Alice"; _} -> 7
{_; age} -> age
数据类模式
Point = Inherit {x = Int; y = Int}
p = Point::{x = 1; y = 2}
Point::{x; y} = p
理解(Comprehensions)
odds = [i | i <- 1..100; i % 2 == 0]
Class
Erg 不支持多重继承
Trait
它们类似于 Rust Trait,但在更字面意义上,允许组合和解耦,并将属性和方法视为平等 此外,它不涉及实施
XY = Trait {x = Int; y = Int}
Z = Trait {z = Int}
XYZ = XY and Z
Show = Trait {show: Self.() -> Str}
@Impl XYZ, Show
Point = Class {x = Int; y = Int; z = Int}
Point.
...
修补
您可以为类和Trait提供实现
筛子类型
谓词表达式可以是类型限制的
Nat = {I: Int | I >= 0}
带值的参数类型(依赖类型)
a: [Int; 3]
b: [Int; 4]
a + b: [Int; 7]