基本

badge

Warning: 本文檔不完整。它未經校對(樣式、正確鏈接、誤譯等)。此外,Erg 的語法可能在版本 0.* 期間發生破壞性更改,并且文檔可能沒有相應更新。請事先了解這一點 如果您在本文檔中發現任何錯誤,請報告至 此處的表單GitHub repo。我們將不勝感激您的建議

本文檔描述 Erg 的基本語法 如果您已經有使用 Python 等語言的經驗,請參閱 快速瀏覽 了解概覽 還有一個單獨的 標準 APIErg 貢獻者的內部文檔。如果您需要語法或 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

字面量

badge

基本字面量

整數字面量

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.01. 相等 后續文檔可能會使用 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)

變量和常量

badge

變量

變量是一種代數; 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"

x3.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>

聲明

badge

聲明是用于指定要使用的變量類型的語法 可以在代碼中的任何地方進行聲明,但單獨的聲明并不引用變量。它們必須被初始化 分配后,可以檢查聲明以確保類型與分配它的對象兼容

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`

函數

badge

函數是一個塊,它接受一個"參數",對其進行處理,并將其作為"返回值"返回。定義如下

add x, y = x + y
# 或者
add(x, y) = x + y

在函數名之后指定的名稱稱為參數 相反,傳遞給函數的對象稱為參數 函數 add 是一個以 xy 作為參數并返回它們之和的函數,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

: 之后,除了注釋之外,不得編寫任何代碼,并且必須始終在新行上 此外,您不能在函數后立即使用 :。只有 dodo! 可以做到這一點

# 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 # 類型錯誤: 無法比較函數

盡管 fg 總是返回相同的結果,但要做出這樣的決定是極其困難的。我們必須向編譯器教授代數 所以 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的語法糖

內置函數

badge

如果

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

運算符

badge

運算符是表示操作的符號。操作數是運算符(左)右側的東西

運算符是一種函數,因此它們本身就是可以綁定到變量的一流對象。綁定時,需要用```括起來 對于+(和-),有一元和二元運算符,所以必須指定_+_(二元運算)/+_(一元運算)

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

副作用

badge

我們一直忽略了解釋"!"的含義,但現在它的含義終于要揭曉了。這個 ! 表示這個對象是一個帶有"副作用"的"過程"。過程是具有副作用的函數

f x = print! x # EffectError: 不能為函數分配有副作用的對象
# 提示: 將名稱更改為 'f!'

上面的代碼會導致編譯錯誤。這是因為您在函數中使用了過程。在這種情況下,您必須將其定義為過程

p! x = print! x

p!, q!, ... 是過程的典型變量名 以這種方式定義的過程也不能在函數中使用,因此副作用是完全隔離的

方法

函數和過程中的每一個都可以是方法。函數式方法只能對self進行不可變引用,而程序性方法可以對self進行可變引用 self 是一個特殊的參數,在方法的上下文中是指調用對象本身。引用 self 不能分配給任何其他變量

C!.
    method ref self =
        x = self # 所有權錯誤: 無法移出`self`
        x

程序方法也可以采取 selfownership。從方法定義中刪除 refref!

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"
# 這將立即打印
# 這將在執行后打印

如果沒有反饋給程序,或者換句話說,如果沒有外部對象可以使用內部信息,那么信息的"泄漏"是可以允許的。只需要不"傳播"信息

程序

badge

程序是指允許副作用的函數 基本用法或定義請參考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

內置程序

badge

id!

返回對象的唯一標識號 盡管在純 Erg 語義中,結構相同的對象之間沒有區別,但實際上對象在內存中具有不同的位置 id! 返回一個代表這個位置的數字

數組

badge

數組是最基本的__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]

字典

badge

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}

下標

badge

[] 不同于普通的方法

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`

元組

badge

元組類似于數組,但可以保存不同類型的對象 這樣的集合稱為不等集合。相比之下,同構集合包括數組、集合等

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)

badge

記錄是一個集合,它結合了通過鍵訪問的 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 = xx = .xx,和 .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

badge

一個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)

類型

badge

類型是 Erg 中一個非常重要的特性,所以我們有一個 dedicated section。請看那里

Erg 的類型系統

badge

下面簡單介紹一下 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(代數類型)

RecordEnumIntervalUnionIntersectionDiff
kindAttributiveRefinementRefinementAlgebraicAlgebraicAlgebraic
generatorrecordsetrange operatoror operatorand operatornot operator

也可以使用名義子類型(NST),將 SST 類型轉換為 NST 類型稱為類型的名義化。結果類型稱為名義類型 在 Erg 中,名義類型是類和Trait。當我們簡單地說類/Trait時,我們通常指的是記錄類/Trait

TypeAbstractionSubtyping procedure
NSTNominalTypeTraitInheritance
SSTStructuralTypeStructural Trait(Implicit)

整個名義類型的類型(NominalType)和整個結構類型的類型(StructuralType)是整個類型(Type)的類型的子類型

Erg 可以將參數(類型參數)傳遞給類型定義。帶有類型參數的 OptionArray 等稱為多項式類型。這些本身不是類型,但它們通過應用參數成為類型。諸如 IntStr 等沒有參數的類型稱為簡單類型(標量類型)

一個類型可以看成一個集合,并且存在包含關系。例如,"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 是所謂的類型參數,可以是真正的類型(類),例如 IntStr。在其他語言中,類型參數被賦予特殊的符號(泛型、模板等),但在 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} 的情況下,NatNat 是單態的,因為 Nat 是具有 + 實現的最小類型 如果是 {0}, {-1},它與 Int 是單態的,因為它不匹配 Nat。如果子類型和父類型之間沒有關系,則首先嘗試具有最低濃度(實例數)(或者在多態類型的情況下參數更少)的那個 {0}{1} 是枚舉類型,它們是部分類型,例如 IntNat 例如,可以為枚舉類型指定名稱和請求/實現方法。在有權訪問該類型的命名空間中,滿足請求的對象可以使用實現方法

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 是一個類型,其元素是 01。它也是 Int 類型的子類型,它同時具有 01{} 這樣的對象本身就是一種類型,可以在分配或不分配給上述變量的情況下使用 這樣的類型稱為結構類型。當我們想強調它作為后者而不是類(命名類型)的用途時,它也被稱為未命名類型。{0, 1}這樣的結構類型稱為枚舉類型,還有區間類型、記錄類型等

類型標識

無法指定以下內容。例如,您不能指定 IntIntIntIntIntInt 例如,IntStr都是Add,但是IntStr不能相加

add l: Add, r: Add =
    l + r # 類型錯誤: `_+_` 沒有實現: |T, U <: Add| (T, U) -> <失敗>

此外,下面的類型 AB 不被認為是同一類型。但是,類型"O"被認為是匹配的

... |R1, R2, O, A <: Add(R1, O); B <: Add(R2, O)|

基本語法

badge

類型規范

在 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

badge

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 包含 BinAddBinSub 這對應于類中的繼承,但與繼承不同的是,可以使用"和"組合多個基類型。也允許被 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

AddSubMul 的實際定義如下所示

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

badge

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 的類和類型是不同的 只有一個類 Int1 的生成器。可以通過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 ObjClass() 在實踐中幾乎是等價的。一般使用后者

類具有與類型不同的等價檢查機制 類型基于其結構進行等效性測試

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!"

DogPerson 的結構完全一樣,但讓動物打招呼,讓人類吠叫顯然是無稽之談 前者是不可能的,所以讓它不適用更安全。在這種情況下,最好使用類

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.xT.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.XXorY.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

繼承

badge

繼承允許您定義一個新類,為現有類添加功能或專業化 繼承類似于包含在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!,從而導致無限循環。.fBase! 類中的一個沒有問題的方法,但它被覆蓋以一種意想不到的方式使用,并且被破壞了

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,但有一些示例似乎可以這樣做

例如,IntReal 的子類型(實現了 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

badge

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 進行編碼并不是一個壞主意

補丁

badge

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 補丁涉及 StrReverse 這樣的補丁稱為 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 traitimpl 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:

值類型

badge

值類型是可以在編譯時評估的 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 語義中沒有內存的概念,并且因為它被放置在堆棧上而說它是值類型,或者因為它實際上是一個指針而說它不是值類型是不正確的。值類型僅表示它是"值"類型或其子類型。

屬性類型

badge

屬性類型是包含 Record 和 Dataclass、Patch、Module 等的類型 屬于屬性類型的類型不是值類型

記錄類型復合

可以展平復合的記錄類型 例如,{... {.name = Str; .age = Nat}; ... {.name = Str; .id = Nat}} 變成 {.name = Str; .age = 自然; .id = Nat}

區間類型

badge

Range 對象最基本的用途是作為迭代器

for! 0..9, i =>
    print! i

請注意,與 Python 不同,它包含一個結束編號

然而,這不僅僅用于 Range 對象。也可以使用類型。這種類型稱為Interval類型

i: 0..10 = 2

Nat 類型等價于 0..<Inf 并且,IntRatio 等價于 -Inf<..<Inf0..<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"

枚舉類型

badge

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() # 類型錯誤

細化類型

badge

細化類型是受謂詞表達式約束的類型。枚舉類型和區間類型是細化類型的語法糖

細化類型的標準形式是{Elem: Type | (Pred)*}。這意味著該類型是其元素為滿足 PredElem 的類型 可用于篩選類型的類型僅為 數值類型

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 時,可以用 ;andor 分隔。;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) = ...

代數類型

badge

代數類型是通過將類型視為代數來操作類型而生成的類型 它們處理的操作包括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) 或 StrOption(Int or Str) 屬于同一類型

路口

交集類型是通過將類型與 and 操作組合得到的

Num = Add and Sub and Mul and Eq

如上所述,普通類不能與"and"操作結合使用。這是因為實例只屬于一個類

差異

Diff 類型是通過 not 操作獲得的 最好使用 and not 作為更接近英文文本的符號,但建議只使用 not,因為它更適合與 andor 一起使用

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 類型的 orand 這些不是真正的代數類型,因為它們被簡化了,并且將它們用作類型說明符將導致警告; 要消除警告,您必須簡化它們或定義它們的類型

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 類型不是真正的代數類型,因為它們總是可以被簡化

依賴類型

badge

依賴類型是一個特性,可以說是 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]}

類型變量

badge

類型變量是用于例如指定子程序參數類型的變量,它的類型是任意的(不是單態的) 首先,作為引入類型變量的動機,考慮 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 類型的子類,并且要分配的lr 的實際類型必須相同 在這種情況下,"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

子類型

badge

在 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}

類型轉換

badge

向上轉換

因為 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".

可變類型

badge

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!)?

類型綁定

badge

類型邊界為類型規范添加條件。實現這一點的函數是守衛(守衛子句) 此功能可用于函數簽名、匿名函數簽名以及篩選類型 守衛寫在返回類型之后

謂詞

您可以使用返回 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}

復合型

badge

元組類型

(), (X,), (X, Y), (X, Y, Z), ...

元組具有長度和內部類型的子類型化規則 對于任何元組TU,以下成立

* 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

高級類型

下面,我們將討論更高級的類型系統。初學者不必閱讀所有部分。

badge

廣義代數數據類型 (GADT)

badge

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() # 類型錯誤

默認參數

badge

首先,讓我們看一個使用默認參數的示例

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)

第一個意味著可以用沒有默認參數的函數來識別具有默認參數的函數 第二個意味著可以省略任何默認參數。

類型擦除

badge

類型擦除是將類型參數設置為 _ 并故意丟棄其信息的過程。類型擦除是許多多態語言的特性,但在 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)

存在類型

badge

如果存在對應于?的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的任何類型,它不是知道實際分配了什么類型的地方。

關鍵字參數

badge

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

badge

一切都在 Erg 中輸入。類型本身也不例外。kind 表示"類型的類型"。例如,Int 屬于 Type,就像 1 屬于 IntType 是最簡單的一種,atomic kind。在類型論符號中,Type 對應于 *

在Kind的概念中,實際上重要的是一種或多種Kind(多項式Kind)。單項類型,例如Option,屬于它。一元Kind表示為 Type -> Type 1。諸如 ArrayOption 之類的 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 <: TU: |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 -> IntT -> T 而不是 K(T, T)(替換類型變量: K, T)或 T -> U(替換類型變量: T, U )。(替換類型變量: T)優先匹配
  • 如果更換的次數也相同,則報錯為不可選擇

1 在類型理論符號中,*=>*

2 可見性等細微差別。

標記Trait

badge

標記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

可變結構類型

badge

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}

新類型模式

badge

這是 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 擁有的所有方法,所以每次都必須重新定義必要的操作 如果重新定義的成本不值得,最好使用繼承。另一方面,在某些情況下,方法丟失是可取的,因此請根據情況選擇適當的方法

重載

badge

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 -> RatioRatio -> 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

幻影類

badge

幻像類型是標記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!相同。

投影類型

badge

投影類型表示如下代碼中的"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 = IntOdd.AddO = Even

assert Int < Add
assert Int.AddO == Int
assert Odd < Add
assert Odd.AddO == Even

量化依賴類型

badge

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]

共享引用

badge

共享引用是必須小心處理的語言特性之一 例如,在 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。這是一個明顯的錯誤,但是出了什么問題? 原因是共享引用 denaturednormal_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)

badge

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

特殊類型變量

SelfSuper 可以用作結構化類型和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

badge

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: CTypeof(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}

變性(逆變與協變)

badge

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 when s: Str (just upcast Str to Object)
  • When o: Object, Array!(Str).push!(o) is NG
  • Array!(Object).pop!().into(Str) is NG
  • Array!(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 when U > T)
  • 函數 T -> U 對于參數類型 T 是逆變的((S -> U) < (T -> U) when S > 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 Thead中的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 TList 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

badge

例如,定義多相關系數如下

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類型的對象的類是IntStr,但你只有通過執行才能知道它是哪一個 當然,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 <: BA == B
  • A and B == B: 當 A :> BA == B
  • A and B == {}: 當 !(A :> B)!(A <: B)

A 或 B 具有以下可能性

  • A 或 B == A: 當A :> BA == B
  • A or B == B: 當A <: BA == 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 對象

這是設計使然,這樣您就不會無意中將子例程的返回類型與另一種類型混合 但是,如果返回值類型選項是具有包含關系的類型,例如 IntNat,它將與較大的對齊。

迭代器

badge

迭代器是用于檢索容器元素的對象

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迭代器對象>

ArrayIteratorRangeIterator 都是實現 Iterator 的類,它們的存在只是為了提供 ArrayRange 迭代函數 這種設計模式稱為伴生類 1 而"IteratorImpl"補丁是迭代功能的核心。Iterator 只需要一個.next 方法,IteratorImpl 確實提供了幾十種方法。ArrayIteratorRangeIterator只需實現.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),并以此命名。

可變性

badge

正如我們已經看到的,所有 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"

常量在全局以下的所有范圍內都是相同的,并且不能被覆蓋。因此,它們不能被 = 重新定義。此限制允許它用于模式匹配 TrueFalse 可以用于模式匹配的原因是因為它們是常量 此外,常量總是指向不可變對象。諸如 Str! 之類的類型不能是常量 所有內置類型都是常量,因為它們應該在編譯時確定。可以生成非常量的類型,但不能用于指定類型,只能像簡單記錄一樣使用。相反,類型是其內容在編譯時確定的記錄

變量、名稱、標識符、符號

讓我們理清一些與 Erg 中的變量相關的術語

變量是一種為對象賦予名稱以便可以重用(或指向該名稱)的機制 標識符是指定變量的語法元素 符號是表示名稱的語法元素、記號

只有非符號字符是符號,符號不稱為符號,盡管它們可以作為運算符的標識符 例如,x 是一個標識符和一個符號。x.y 也是一個標識符,但它不是一個符號。xy 是符號 即使 x 沒有綁定到任何對象,x 仍然是一個符號和一個標識符,但它不會被稱為變量 x.y 形式的標識符稱為字段訪問器 x[y] 形式的標識符稱為下標訪問器

變量和標識符之間的區別在于,如果我們在 Erg 的語法理論意義上談論變量,則兩者實際上是相同的 在 C 中,類型和函數不能分配給變量; int 和 main 是標識符,而不是變量(嚴格來說可以賦值,但有限制) 然而,在Erg語中,"一切都是對象"。不僅函數和類型,甚至運算符都可以分配給變量

所有權

badge

由于 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,可以將循環引用作為不安全操作生成

可見性

badge

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

命名規定

badge

如果要將變量用作常量表達式,請確保它以大寫字母開頭。兩個或多個字母可能是小寫的

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

badge

匿名函數是一種無需命名即可動態創建函數對象的語法

# `->` 是匿名函數操作符
# 同 `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

子程序

badge

函數

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)

運算符

可以通過用 ` 括起來將其定義為普通函數

中性字母運算符,例如 andor 可以通過用 ` 括起來定義為中性運算符

and(x, y, z) = x and y and z
`_+_`(x: Foo, y: Foo) = x.a + y.a
`-_`(x: Foo) = Foo.new(-x.a)

閉包

badge

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被設計為對使用不可變對象進行編程的自然簡潔描述

模塊

badge

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__" 的模塊)不能成為循環引用的主題

對象系統

badge

可以分配給變量的所有數據。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 的父類

類型

定義需求屬性并使對象通用化的對象 主要有兩種類型: 多態類型和單態類型。典型的單態類型有IntStr等,多態類型有Option Int[Int; 3]等 此外,定義改變對象狀態的方法的類型稱為 Mutable 類型,需要在變量屬性中添加 !(例如動態數組: [T; !_])

班級

具有 .__new__.__init__ 方法等的類型。實現基于類的面向對象

功能

對外部變量(不包括靜態變量)有讀權限但對外部變量沒有讀/寫權限的子程序。換句話說,它沒有外部副作用 Erg 函數的定義與 Python 的不同,因為它們不允許副作用

程序

它對外部變量具有讀取和"自我"權限,對靜態變量具有讀/寫權限,并允許使用所有子例程。它可能有外部副作用

方法

隱式將"self"作為第一個參數的子例程。它與簡單的函數/過程是不同的類型

實體

不是子例程和類型的對象 單態實體(1"a" 等)也稱為值對象,多態實體([1, 2, 3], {"a": 1})也稱為容器對象

模式匹配

badge

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

沒有固定的模式。因為集合沒有辦法唯一地檢索元素 您可以通過迭代器檢索它們,但不能保證順序

推導式

badge

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}

擴展語法

badge

在分解賦值中,將 * 放在變量前面會將所有剩余元素展開到該變量中。這稱為展開賦值

[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

裝飾器

badge

裝飾器用于向類型或函數添加或演示特定狀態或行為 裝飾器的語法如下

@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命令運行

錯誤處理系統

badge

主要使用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 或 .unwraped 在一個不可能 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!"

管道運算符

badge

管道運算符的使用方式如下:

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 集成

badge

導出到 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 腳本進行靜態類型分析

包系統

badge

Erg包大致可以分為app包,即應用程序,以及lib包,即庫 應用包的入口點是src/app.erapp.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 中導入 foobar 模塊。由于 bar.er 文件,bar 目錄可以被識別為一個模塊 foo 模塊是由文件組成的模塊,bar 模塊是由目錄組成的模塊。bar 模塊還包含 bazqux 模塊 該模塊只是 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"

生成器

badge

生成器是在塊中使用 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, 臨時)

badge

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)*

索引

badge

有關不在此索引中的 API,請參閱 此處

有關術語,請參見 此處

符號

拉丁字母

A

B

C

D

E

F

G

H

I

J

K

L

M

N

O

P

Q

R

S

T

U

V

W

  • [while]

X

Y

Z

快速瀏覽

badge

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

布爾類型

TrueFalse 是 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]