RFC-010: 統一型構文 - name: type = value モデル
概要
本 RFC は極限まで簡素化された統一型構文モデルを提案する:すべては name: type = value。
YaoXiang には唯一の宣言形式しかない:
identifier : type = expressionここで type は任意の型式であり、expression は任意の値式である。 fn もなく、struct もなく、trait もなく、impl もなく、小文字の type キーワードもない(ただし Type は元型キーワードとして存在する)。
核心設計:
Type自体が一つのジェネリック型である。(T: Type) -> Typeは「型引数 T を受け取る型」を表す。
| 概念 | コード記述 |
|---|---|
| 変数 | x: Int = 42 |
| 関数 | add: (a: Int, b: Int) -> Int = a + b |
| 記録型 | Point: Type = { x: Float, y: Float } |
| インターフェース | Drawable: Type = { draw: (Surface) -> Void } |
| ジェネリック型 | List: (T: Type) -> Type = { data: Array(T), length: Int } |
| ジェネリック型 | Map: (K: Type, V: Type) -> Type = { keys: Array(K), values: Array(V) } |
| メソッド | Point.draw: (p: Point, s: Surface) -> Void = ...Point.draw = draw[0] |
| ジェネリック関数 | map: (T: Type, R: Type) -> ((list: List(T), f: (x: T) -> R) -> List(R)) |
Type は言語で唯一の元型キーワードである。
名前空間 vs メソッド束縛:
Type.nameの接頭辞は名前空間所属を表すだけで、それ以上の意味はない。p.draw(screen)のような.呼び出し構文を有効にするには、明示的な束縛が必要である:Point.draw = draw[0]。詳細は後述の「名前空間とメソッド束縛」セクションを参照。これは型階層の注記に用いられ、コンパイラが Type0、Type1、Type2... の区別を自動処理し、ユーザーには透過的である。
// 核心構文:統一+区別
// 変数
x: Int = 42
// 関数(引数名はシグネチャ内に)
add: (a: Int, b: Int) -> Int = a + b
// 記録型
Point: Type = {
x: Float,
y: Float,
draw: (Surface) -> Void,
serialize: () -> String
}
// インターフェース(本質はフィールドがすべて関数の記録型)
Drawable: Type = {
draw: (Surface) -> Void,
bounding_box: () -> Rect
}
Serializable: Type = {
serialize: () -> String
}
// メソッド定義(Type.method 構文を使用)
Point.draw: (self: Point, surface: Surface) -> Void = {
surface.plot(self.x, self.y)
}
Point.serialize: (self: Point) -> String = {
"Point(${self.x}, ${self.y})"
}
// ジェネリック型((T: Type) -> Type = 型引数を受け取るジェネリック型)
List: (T: Type) -> Type = {
data: Array(T),
length: Int
}
Map: (K: Type, V: Type) -> Type = {
keys: Array(K),
values: Array(V)
}
// 使用
p: Point = Point(1.0, 2.0)
p.draw(screen) // 糖衣構文 → Point.draw(p, screen)
s: Drawable = p // 構造的部分型:Point は Drawable を実装
drawables: List(Drawable) = [p, r]
process_all(drawables)動機
なぜこの機能が必要か?
現在の型システムには複数の分離した概念が存在する:
- 変数宣言構文
- 関数定義構文
- 型定義構文(異なる構文)
- インターフェース定義構文
- メソッド束縛構文
これらの概念間には統一性がなく、構文の断片化と学習コストの増大を招いている。
設計目標
- 極限の統一性:一つの構文規則ですべての状況をカバー
- 簡潔でエレガント:
name: type = valueの対称美 - 新しいキーワード不要:既存の構文要素を再利用
- 理論的にエレガント:型自体も
Type型の値 - ジェネリックに親和的:ジェネリックシステム(RFC-011)とシームレスに統合
ジェネリックシステムとの統合
RFC-010 の統一構文モデルは RFC-011 のジェネリックシステム設計と自然に契合し、ジェネリック引数は統一モデルにシームレスに溶け込む:
// 基本ジェネリック(RFC-011 Phase 1)
List: (T: Type) -> Type = { data: Array(T), length: Int }
// ジェネリック関数(RFC-023 構文:シグネチャ内の Type 位置は省略可能、呼び出し時に自動推論)
map: (: Type, R: Type) -> (( list: List(T), f: (T) -> R) -> List(R)) = ...
// 型制約(RFC-011 Phase 2)
clone: (value: T) -> T = value.clone() // T: Clone 制約は引数型が運ぶ
// Const ジェネリック(RFC-011 Phase 4)
Array: (T: Type, N: Int) -> Type = { data: Array(T, N), length: N }依存関係:
- RFC-011 Phase 1(基本ジェネリック)は RFC-010 の強い依存先
- 基本ジェネリックがなければ、RFC-010 のジェネリック例はコンパイルできない
- 推奨:RFC-011 Phase 1 を RFC-010 と同期して実装
提案
核心原則:型コンストラクタ vs 関数/変数
これは重要な設計選択であり、構文の曖昧性解消ルールを決定する:
| 記述 | 意味 | 規則 |
|---|---|---|
x: Type = ... | 型コンストラクタ | : Type 明示宣言 → 型として強制 |
f = ... | 関数または変数 | : Type なし → HM が能動的に関数/変数と推論 |
なぜこのように設計するのか?
{ ... } 構文自体に曖昧性がある:
{ x: Float, y: Float }は型リテラル(記録型)になり得る{ a = 1 + 1 }はコードブロック(実行文、Void を返す)になり得る
曖昧性解消ルール:
: Typeがある → 型コンストラクタとして強制解釈、{ ... }は型リテラル: Typeがない → HM が能動的に{ ... }をコードブロックとして解釈、関数型と推論
# ✅ 型コンストラクタ::Type あり
Point: Type = { x: Float, y: Float }
# ✅ 関数::Type なし、HM が () -> Void と推論
main: () -> Void = { println("Hello") }
# ❌ エラー::Type なし、コンパイラは { ... } を型として解釈できない
Point = { x: Float, y: Float } // HM は関数と推論し、型ではない!統一モデル:identifier : type = expression
├── 変数
│ └── x: Int = 42
│
├── 関数
│ └── add: (a: Int, b: Int) -> Int = a + b # : Type なし、HM が関数と推論
│
├── 記録型
│ └── Point: Type = { x: Float, y: Float } # 戻り値: Type
│
├── インターフェース
│ └── Drawable: Type = { draw: (Surface) -> Void } # 戻り値: Type
│
├── ジェネリック型
│ └── List: (T: Type) -> Type = { data: Array(T), length: Int } # 戻り値: Type
│
├── ジェネリック型(複数引数)
│ └── Map: (K: Type, V: Type) -> Type = { keys: Array(K), values: Array(V) } # 戻り値: Type
│
├── 名前空間関数
│ └── draw: (p: Point, surface: Surface) -> Void = ...
│ Point.draw = draw[0] # 明示的束縛後にのみドット呼び出し構文が有効
│
└── ジェネリック関数
└── map: (T: Type, R: Type) -> ((list: List(T), f: (x: T) -> R) -> List(R)) # Type を返さない、HM が関数と推論元型階層(コンパイラ内部)
コンパイラ内部は宇宙階層 level: selfpointnum(文字列で保存、理論上は無限に延伸可能)を維持する。
| Level | 説明 |
|---|---|
Type0 | 日常型(Int、Float、Point) |
Type1 | 型コンストラクタ(List、Maybe) |
Type2+ | 高階コンストラクタ |
ユーザーはこれらの数字を見ない、: Type だけを見る。
Curry-Howard 同型:型は命题、プログラムは証明
YaoXiang の統一構文 name: type = value は恣意的に選ばれたものではない—これはまさに Curry-Howard 同型(Curry-Howard correspondence)の直接的な写像である。この同型は深い事実を明らかにする:型システムと論理システムは同じものの二つの面に過ぎない。
| 論理(命題) | 型システム(YaoXiang) | 例 |
|---|---|---|
| 命題 P | 型 T | Int、Bool |
| P が真である証明 | 型 T の値 | 42: Int、true: Bool |
| P → Q(含意) | 関数型 (P) -> Q | (x: Int) -> Bool |
| P ∧ Q(連言) | 記録型 { p: P, q: Q } | { x: Int, y: Bool } |
| ∀x.P(x)(全称量化) | ジェネリック関数 (T: Type) -> ... | map: (T: Type, R: Type) -> ... |
| P ⊕ Q(選言) | enum / tagged union | Maybe: (T: Type) -> Type = { ... } |
name: type = value の Curry-Howard 下的意味:
// "x: Int = 42" は「Int 型の証明 x が存在し、その値は 42 である」と読む
x: Int = 42
// "add: (a: Int, b: Int) -> Int = a + b" は
//「含意証明が存在する:Int の証明 a と b が与えられると、Int の証明を構成できる」と読む
add: (a: Int, b: Int) -> Int = a + b
// "Point: Type = { x: Float, y: Float }" は
//「Point は命題であり、その証明には Float 証明 x と Float 証明 y の同時提供が必要」と読む
Point: Type = { x: Float, y: Float }なぜこれが重要か?
論理的一貫性 = 型安全性:もし型システムが
T型の値の構成を許すが、正当な実行時表現を持たないなら、それは論理で偽の命題の証明を許すようなものだ—システムが崩壊する。Curry-Howard は次のように教える:型安全な言語は、本質的に一貫した論理システムである。宇宙階層は必要条件:後述のように、
Type: Type(つまり「型の型も型」)を許せば Russell のパラドックス(型論では Girard のパラドックスとして現れる)が発生する。YaoXiang のType₀ : Type₁ : Type₂ : ...の階層構造は、各型が特定の階層にのみ属することを保証し、閉じない上昇連鎖を形成し、根本からパラドックスを回避する。これは YaoXiang の型システムが Curry-Howard 意味で論理的に一貫していることを意味する。統一構文の理論的基礎:
name: type = valueが一つの構文で変数、関数、型、インターフェース、ジェネリックスすべてをカバーできるのは、Curry-Howard 下でそれらすべてが同じこと—命題に証明を提供すること—だからである。変数は命題の証拠、関数は含意の証拠、記録は連言の証拠、ジェネリックは全称量化の証拠。統一構文は人為的な設計の一致ではなく、Curry-Howard 同型の自然な帰結である。
さらに読む:Wadler, P. (2015). "Propositions as Types." Communications of the ACM, 58(12), 75–84. この記事は Curry-Howard 同型の歴史と意義を平易に解説している。
構文定義
1. 変数宣言
// 基本構文
x: Int = 42
name: String = "Alice"
flag: Bool = true
// 型推論(省略可能)
y = 100 // Int と推論2. 関数定義
ブロックの値 = 末尾式、return は関数を退出(型 Never)——詳細は RFC-010aを参照。
// 単一式形式
add: (a: Int, b: Int) -> Int = a + b
greet: (name: String) -> String = "Hello, ${name}!"
// コードブロック形式:値は末尾式
process: (x: Int) -> Int = {
a = x * 2
b = a + 1
b // 末尾式 → ブロックの値
}
// 複数行コードブロック
calc: (x: Float, y: Float, op: String) -> Float = {
match op {
"+" -> x + y,
"-" -> x - y,
_ -> 0.0
} // match を末尾式として
}
// Void 関数:末尾式が Void
print: (msg: String) -> Void = {
console.write(msg) // console.write : Void
}戻り規則
ブロックの値 = 末尾式(唯一の出口):
| 記述 | 値 |
|---|---|
= expr(中括弧なし) | expr |
= { ...; e }(中括弧あり) | 末尾式 e |
= { ...; s }(末尾が文/代入) | Void(代入の値は Void) |
= {}(空ブロック) | Void |
return の意味:非局所的退出、最も近い関数境界を退出する(ブロックに「返さない」)、型は Never。Never <: T は任意の型に対して成立(爆発原理)、したがって return は任意の戻り型位置に現れ得る。
# 単一式:直接値を返す
add: (a: Int, b: Int) -> Int = a + b
# コードブロック:値は末尾式
process: (x: Int) -> Int = {
a = x * 2
b = a + 1
b
}
# 早期リターン:return はブロックを貫通し関数を退出(型 Never)
factorial: (n: Int) -> Int = {
if n <= 1 { return 1 }
n * factorial(n - 1) # 末尾式
}設計根拠:{ ... } は依存駆動計算ユニット(後述)であり、その評価意味は単一式とは異なる—中括弧は複数文のコンテキストを導入し、その値は末尾式で与えられる、「最後の式が戻り値か」の曖昧性は存在しない。
{} 意味:依存駆動計算ユニット
YaoXiang における { ... } は単なるコードブロックではない—依存駆動計算ユニットである。この意味は関数本体、変数初期化、spawn で一貫している:
核心ルール:
{}内の代入文は記述順序ではなく依存関係に従って自動ソートされる- 依存が満たされれば即座に実行、不足すればブロックして待機
- ブロックの値 = 末尾式(戻り規則を参照);
returnはNever型の非局所退出で、関数を退出する
# 依存駆動:b は a に依存、コンパイラが自動ソート
result: Int = {
b = a + 1 # a に依存 → a の後に自動配置
a = 10 # 依存なし → 先に実行可能
b # 末尾式 → ブロックの値 11
}単一式との違い:
= expr(中括弧なし)は直接値を返す単純束縛;= { ... }(中括弧あり)は依存駆動計算コンテキストを導入し、複数文を許可し、その値は末尾式で与えられる。
spawn ブロック
spawn { ... } は YaoXiang の唯一の並列プリミティブである。{} の依存駆動意味を利用して自動並列化を実現:
spawn { ... }内の直接子代入は自動的に並列タスクを生成- 依存が満たされたタスクは即座に並列実行
- 呼び出し側はすべての子タスクの完了をブロックして待機
result = spawn {
a = fetch_data("url1") # タスク 1
b = fetch_data("url2") # タスク 2(a と依存なし、並列実行)
c = process(a, b) # a, b に依存 → 両方の完了を待って実行
c # 末尾式 → spawn の値
}
// 呼び出し側はここでブロック、spawn ブロック内のすべてのタスクが完了するまで詳細定義:
spawnの完全な意味、タスク生成ルール、ブロックモデルは008-runtime-concurrency-model.mdを参照。
unsafe ブロック
unsafe { ... } は不透明型の定義と raw ポインタ操作に使用する。{} の評価意味を利用して型定義を上位スコープに引き渡す(値出口は末尾式):
核心ルール:
unsafe {}内で型を定義し raw ポインタを操作可能- 末尾式 が
unsafe {}の値を与える(型定義は上位スコープに引き渡される) - 返される型は
unsafe {}外でも使用可能 - 型のフィールドアクセスには unsafe 権限が必要
# unsafe ブロック内で不透明型を定義
SqliteDb = unsafe {
SqliteDb: Type = {
handle: *Void # raw ポインタ
}
SqliteDb # 末尾式 → unsafe ブロックの値
}
# SqliteDb は unsafe ブロック外でも使用可能
db = sqlite3_open("test.db")
# ❌ コンパイルエラー:handle フィールドには unsafe 権限が必要
handle = db.handle
# ✅ メソッド呼び出しで
db.close()詳細定義:
unsafeの完全な意味、FFI 型定義、メソッド束縛はffi.mdを参照。
3. 型定義
型定義は YaoXiang 統一構文の核心であり、フィールド、デフォルト値、束縛メソッド、インターフェース実装を含む:
基本型
記録型:フィールドリスト、フィールド型は任意の型式。
Point: Type = {
x: Float,
y: Float
}デフォルト値を持つフィールド:フィールドはデフォルト値を持て、構築時にオプション。
Point: Type = {
x: Float = 0,
y: Float = 0
}使用:
Point() → Point(x=0, y=0)
Point(x=1) → Point(x=1, y=0)
Point(x=1, y=2) → Point(x=1, y=2)デフォルト値なしのフィールド:構築時に必ず提供しなければならない。
Point2: Type = {
x: Float,
y: Float
}使用:
Point2(x=1, y=2) //✓
Point2() //✗
Point2(x=1) //✗内蔵型
YaoXiang の識別子体系は三層に分かれ、異なるコンパイラ段階で順次認識される:
- キーワード(parser 独立 token)— 制御構造と宣言キーワード、例:
if、match、pub、return - リテラル予約語(parser 独立 token)—
true、false、void、Type、通常の識別子にはなれない - 内蔵型名(type checker 事前登録)— パーサは通常識別子として扱い、型チェッカが解析を担当。予約語ではなく、シャドウ可能(非推奨)
void(小文字、リテラル予約語)と Void(大文字、内蔵型名)の違い:void は値リテラル(Unit の唯一の値と等しい)、Void は型名(Unit 型と等しい、論理 ⊤)。let x: Void = void は合法。
事前定義された内蔵型名:
| 型 | 論理対応 | 説明 |
|---|---|---|
Never | ⊥(偽/空型) | ゼロコンストラクタ、この型に住み得る値はない。「不可能」を表す—発散、panic、デッドコード。Never <: T は任意の T に対して成立(爆発原理)。Never を返す関数は正常に返らないことを示す。キーワードではなく内蔵型名。 |
Void | ⊤(真/Unit) | ちょうど一つの住人(デフォルト void 値)。x: Void = <デフォルト> は合法。直和の単位元は直積の単位元に対応—Void はゼロフィールドの直積型(Unit)、Never はゼロバリアントの直和型。 |
Int | — | 符号付き整数 |
Float | — | 浮動小数点数 |
Bool | — | ブール値:true / false |
Char | — | Unicode 文字 |
String | — | 文字列 |
メソッド束縛
方法1:型定義体内で外部関数を直接束縛
distance: (a: Point, b: Point) -> Float = { ... }
Point: Type = {
x: Float = 0,
y: Float = 0,
distance = distance[0] // 位置 0 に束縛、カリー化後 method: (b: Point) -> Float
}
// 呼び出し:p1.distance(p2) → distance(p1, p2)方法2:無名関数 + 位置束縛
Point: Type = {
x: Float = 0,
y: Float = 0,
distance: ((a: Point, b: Point) -> Float)[0] = ((a, b) => {
dx = a.x - b.x
dy = a.y - b.y
(dx * dx + dy * dy).sqrt() # 末尾式
})
}
// 構文:((params) => body)[position]
// 呼び出し:p1.distance(p2) → distance(p1, p2)インターフェース実装
インターフェース名は型体内に書き、コンパイラが自動的に実装をチェック
Drawable: Type = {
draw: (Surface) -> Void,
bounding_box: () -> Rect
}
Serializable: Type = {
serialize: () -> String
}
Point: Type = {
x: Float,
y: Float,
Drawable, // Drawable インターフェースを実装
Serializable // Serializable インターフェースを実装
}インターフェース定義
インターフェース = フィールドがすべて関数の記録型
Drawable: Type = {
draw: (Surface) -> Void,
bounding_box: () -> Rect
}
Serializable: Type = {
serialize: () -> String
}
// 空型/空インターフェース
EmptyType: Type = {}
Empty: Type = {}名前空間関数定義
Type.name 接頭辞は名前空間所属を表す、それ以上の意味はない。暗黙の束縛はトリガーしない。
// 名前空間関数:Point 名前空間下の通常の関数
Point.draw: (p: &Point, surface: Surface) -> Void = {
surface.plot(p.x, p.y)
}
Point.serialize: (p: &Point) -> String = {
"Point(${p.x}, ${p.y})"
}
// 呼び出し:通常の関数呼び出しそのもの
Point.draw(p, screen)
Point.serialize(p)注意:
selfはキーワードではなく、引数名の慣習に過ぎない。p、this、xと書いても全く同じ効果。コンパイラは引数名を見ず、型を見る。
メソッド束縛(唯一の方法)
p.draw(screen) のような . メソッド呼び出し構文を有効にするには、明示的な束縛が必要。 [position] 構文は関数を「メソッド」として束縛する唯一の仕組み(詳細構文は RFC-004)。
// 関数を定義
draw: (p: &Point, surface: Surface) -> Void = {
surface.plot(p.x, p.y)
}
// 明示的束縛 — これ以降 p.draw(screen) 構文が使える
Point.draw = draw[0] // 位置 0 の引数(&Point)が呼び出し側から渡される
// 使用
p.draw(screen) // 糖衣構文 → draw(&p, screen)
Point.draw(p, screen) // 二つの呼び出し方は等価
// [0] を書かない = 束縛しない。Point.draw は通常の関数エイリアスで、. 構文はない
Point.draw = draw // 束縛なし:Point.draw(p, screen) のみ可能デフォルト動作:[n] を書かない = どの引数も束縛しない。ユーザーが明示的にどの引数を呼び出し側から渡すかを決定しなければならない。
複数位置束縛:
// 複数の位置を束縛(自動カリー化)
Point.transform = transform_points[0, 1]
// 呼び出し:p1.transform(p2)(2.0) → transform_points(p1, p2, 2.0)逆操作(メソッドを通常関数へ):
// 束縛から関数を取り出す
draw_point: (p: &Point, surface: Surface) -> Void = Point.draw4. インターフェース合成
// インターフェース合成 = 型の交差
DrawableSerializable: Type = Drawable & Serializable
// 交差型を使用
process: (T: Drawable & Serializable) -> ((item: T, screen: Surface) -> String) = {
item.draw(screen)
item.serialize()
}5. ジェネリック型
// 基本ジェネリック(RFC-011 Phase 1)
List: (T: Type) -> Type = {
data: Array(T),
length: Int,
push: (T:Type)-((self: List(T), item: T) -> Void),
get: (T:Type)->((self: List(T), index: Int) -> Maybe(T))
}
// 具体的なインスタンス化(RFC-023 構文)
IntList: Type = List(Int)
IntList.push = {
self.data.append(item)
self.length = self.length + 1
}
List.push = (type: Type) -> {
(self: List(type), item: type) -> {
self.data.append(item)
self.length = self.length + 1
}
}
IntList.push(Int)(self, item) // 呼び出し例
// ジェネリックメソッド(RFC-023 構文:型引数は呼び出し側で自動推論)
List.push: (self: List(T), item: T) -> Void = {
self.data.append(item)
self.length = self.length + 1
}
List.get: (self: List(T), index: Int) -> Maybe(T) = {
if index >= 0 and index < self.length {
Maybe.Just(self.data[index])
} else {
Maybe.Nothing
}
}6. ジェネリック呼び出し構文
ジェネリック型とジェネリック関数の呼び出しは統一して () 構文を使用。[] はジェネリックコンテキストでは一切使用しない。
核心ルール:
()ですべてを適用:型適用、関数呼び出し、値構築はすべて()
# 型注釈
numbers: List(Int) = List(1, 2, 3)
# 空コンテナ:T は左側から
empty: List(Int) = List()
# ジェネリック関数呼び出し——型は引数から自動フロー
strings = map(numbers, f)
// T=Int は numbers: List(Int) から
// R=String は f: (Int) -> String からType は左、値は右:
name: type = value——Type 引数は左で宣言、右は常に具体的な値。空コンテナList()のTは左の型注釈から取らなければならない。型情報は一度だけ書けばよい——引数宣言時に、コンパイラがそれを運んでフロー:
numbers: List(Int) = List(1, 2, 3) // Int は左に一度だけ
f: (Int) -> String = (x) => x.to_string()
strings = map(numbers, f) // T=Int, R=String が numbers と f の型から自動- 値構築は要素から型を推論:
x = List(1, 2, 3) // List(Int) と推論
y = List("a", "b") // List(String) と推論
z = List() // ❌ コンパイルエラー:T を推論できない
z: List(Int) = List() // ✅ T=Int は左の注釈から- 型エイリアス:
IntList: Type = List(Int)
StringToInt: Type = (String) -> Int
Matrix3x3: Type = Matrix(Float, 3, 3)旧構文との比較:
List[Int]→List(Int)、List[Int]()→List()、List[Int](1,2,3)→List(1,2,3)。旧い[]ジェネリック構文は完全に削除。[]は配列/リストリテラルとインデックスアクセスのみに使用。
例
完全な例
// ======== 1. インターフェース定義 ========
// インターフェース = フィールドがすべて関数型の記録型
// インターフェースには self 引数は不要 — インターフェースは「呼び出し側位置を除いた関数シグネチャ」のみを定義する
Drawable: Type = {
draw: (surface: Surface) -> Void,
bounding_box: () -> Rect
}
Serializable: Type = {
serialize: () -> String
}
Transformable: Type = {
translate: (dx: Float, dy: Float) -> Transformable, // インターフェース型を返す、具体的な実装は自身の型を返す
scale: (factor: Float) -> Transformable
}
// ======== 2. 型定義 ========
Point: Type = {
x: Float,
y: Float,
Drawable,
Serializable,
Transformable
}
Rect: Type = {
x: Float,
y: Float,
width: Float,
height: Float,
Drawable,
Serializable,
Transformable
}
// ======== 3. メソッド実装(通常関数 + 明示的束縛)========
// 関数を定義(self は慣習名に過ぎず、キーワードではない)
draw: (p: &Point, surface: Surface) -> Void = {
surface.plot(p.x, p.y)
}
bounding_box: (p: &Point) -> Rect = {
Rect(p.x - 1, p.y - 1, 2, 2)
}
serialize: (p: &Point) -> String = {
"Point(${p.x}, ${p.y})"
}
translate: (p: &Point, dx: Float, dy: Float) -> Point = {
Point(p.x + dx, p.y + dy)
}
scale: (p: &Point, factor: Float) -> Point = {
Point(p.x * factor, p.y * factor)
}
distance: (p1: &Point, p2: &Point) -> Float = {
dx = p1.x - p2.x
dy = p1.y - p2.y
(dx * dx + dy * dy).sqrt()
}
// 明示的束縛 — 束縛後にのみドット呼び出し構文が使える
Point.draw = draw[0]
Point.bounding_box = bounding_box[0]
Point.serialize = serialize[0]
Point.translate = translate[0]
Point.scale = scale[0]
Point.distance = distance[0]
// Rect のメソッドも同様
draw: (r: &Rect, surface: Surface) -> Void = {
surface.draw_rect(r.x, r.y, r.width, r.height)
}
Rect.draw = draw[0]
bounding_box: (r: &Rect) -> Rect = r
Rect.bounding_box = bounding_box[0]
serialize: (r: &Rect) -> String = {
"Rect(${r.x}, ${r.y}, ${r.width}, ${r.height})"
}
Rect.serialize = serialize[0]
translate: (r: &Rect, dx: Float, dy: Float) -> Rect = {
Rect(r.x + dx, r.y + dy, r.width, r.height)
}
Rect.translate = translate[0]
scale: (r: &Rect, factor: Float) -> Rect = {
Rect(r.x * factor, r.y * factor, r.width * factor, r.height * factor)
}
Rect.scale = scale[0]
// ======== 4. 使用 ========
// インスタンス作成
p: Point = Point(1.0, 2.0)
r: Rect = Rect(0.0, 0.0, 10.0, 20.0)
// メソッド呼び出し(糖衣構文)
p.draw(screen)
r.draw(screen)
// 通常メソッド呼び出し(直接呼び出し)
d: Float = distance(p, Point(0.0, 0.0))
// チェーン呼び出し
p2: Point = p.translate(1.0, 1.0).scale(2.0)
// インターフェース代入
drawables: List(Drawable) = [p, r]
for d in drawables {
d.draw(screen)
}
// ジェネリック関数(RFC-023 構文:呼び出し時に型引数を省略、自動推論)
process_all: (items: List(T)) -> Void = {
for item in items {
print(item.serialize())
}
}
process_all([p, r])詳細設計
インターフェースチェックアルゴリズム
fn check_type_implements_interface(
typ: &Type,
iface: &Type
) -> Result<(), TypeError> {
// インターフェースの各フィールド(関数フィールド)について
for (field_name, iface_field) in &iface.fields {
// 型に同名のメソッドがあるか確認
if let Some(method) = typ.methods.get(field_name) {
// メソッドシグネチャが互換か確認
// インターフェースフィールド: (Surface) -> Void
// メソッドシグネチャ: (Point, Surface) -> Void
// 比較:self 引数を除いた後が一致するべき
if !method_signature_matches(method, iface_field.type_) {
return Err(TypeError::MethodSignatureMismatch {
type_name: typ.name,
interface_name: iface.name,
method_name: field_name,
});
}
} else {
return Err(TypeError::MissingMethod {
type_name: typ.name,
interface_name: iface.name,
method_name: field_name,
});
}
}
Ok(())
}インターフェース直接代入とコンパイル時最適化
インターフェース型は直接代入をサポートし、コンパイラは代入の右辺の型に応じて最適な呼び出し戦略を自動選択する:
// 具体型を直接代入 → コンパイル時に具体型確定、ゼロオーバーヘッド呼び出し
d: Drawable = Circle(1)
d.draw(screen) // コンパイル後:直接 circle_draw(screen) を呼び出し、vtable なし
// 関数戻り値 → コンパイル時に具体型確定不可、vtable を使用
d: Drawable = get_shape()
d.draw(screen) // vtable でメソッド検索
// 異種コレクション → vtable を使用
shapes: List(Drawable) = [Circle(1), Rect(2, 3)]
for s in shapes {
s.draw(screen) // vtable でメソッド検索
}コンパイル時最適化戦略:
| シナリオ | 推論結果 | 呼び出し方式 |
|---|---|---|
d: Drawable = Circle(1) | 具体型 Circle | 直接呼び出し(ゼロオーバーヘッド) |
d: Drawable = get_shape() | 不明 | vtable |
shapes: List(Drawable) = [...] | 異種 | vtable |
ルール:
- 右辺が具体型コンストラクタでコンパイル時に確定可能な場合、直接呼び出し IR を生成
- 右辺の型がコンパイル時に確定できない場合、vtable 機構にフォールバック
- vtable は実行時多態の正確性を保証するフォールバック
アヒル型サポート
// 同じメソッドさえ持っていれば、インターフェース型に代入可能
CustomPoint: Type = {
draw: (self: CustomPoint, surface: Surface) -> Void,
x: Float,
y: Float
}
custom: CustomPoint = CustomPoint(
(self: CustomPoint, surface: Surface) => surface.plot(self.x, self.y),
1.0,
2.0
)構文変更
| 以前 | 以後 |
|---|---|
type Point = Point(x: Float, y: Float) | type Point = { x: Float, y: Float } |
type Result(T, E) = ok(T) | err(E) | Result: (T: Type, E: Type) -> Type = { ok: (T) -> Result(T, E), err: (E) -> Result(T, E) } |
impl キーワードが必要 | キーワード不要、インターフェース名は型体後に記述 |
廃止:| バリアント構文
廃止宣言(2026-07-25):
|バリアント構文を正式に廃止し実装から削除。
以下の記述はもはやサポートされない:
type Color = red | green | blue # ❌ 廃止
type Result(T, E) = ok(T) | err(E) # ❌ 廃止
type Option(T) = some(T) | none # ❌ 廃止統一して記録型で和型(sum type)を表現する。記録型のフィールドがすべて関数で、且つすべてがその型自身を返すとき、それは和型である:
Color: Type = {
red: () -> Color,
green: () -> Color,
blue: () -> Color
}
Result: (T: Type, E: Type) -> Type = {
ok: (T) -> Result(T, E),
err: (E) -> Result(T, E)
}
Option: (T: Type) -> Type = {
some: (T) -> Option(T),
none: () -> Option(T)
}設計根拠:
- 特殊ケースの除去:
|は BNF で唯一のname: type = value形式でない構文。削除後、type_expr生成規則は完全に統一され、parser はバリアント型のために独立したパスと先読みバックトラックを維持する必要がなくなる。 - 数学的等価性:Curry-Howard 同型下では、選言 P ⊕ Q に対応する和型は、「フィールドがすべて自身を返す関数の記録型」と等価。両者は同じ意味を表現し、二つの構文を必要としない。
- 破壊性ゼロ:削除前の
|構文は parser で半サポート(引数なしバリアントは解析可能だが、引数型は単態化時に失われる)であり、ユーザーコードの依存は一切ない。 - AST 簡素化:
Type::Variant(Vec<VariantDef>)ノード削除、すべてのバリアント型は統一してType::Structパスを通り、下流の typecheck/mono/formatter の特殊分岐がすべて除去される。
注:和型の意味的属性(バリアント構築、match 網羅性チェック、tagged union メモリレイアウト)はtypecheck 層で
Type::Struct構造から導出され、独立した AST ノードに依存しない。バリアント構築の完全な意味は次節「記録式和型のバリアント構築(権威定義)」を参照。
記録式和型のバリアント構築(権威定義)
本節では「廃止:
|バリアント構文」節の識別基準を完全な意味に展開する(2026-09-25 決定、issue #341 RFC-011b フェーズ 2 の前提)。四つの決定:型限定呼び出し、ゼロペイロードバリアントは関数呼び出し形式、型自足推論、バリアント名は全か無かでの昇格。
判定ルール:全か無か
記録型は以下の両方を満たすとき和型と判定される:
- 型体のすべてのフィールド型が関数型である;
- 各フィールドの戻り型が型引数置換(
Self/ジェネリック実引数)後にその記録型自身である。
判定は全か無かである:判定成立時、すべての関数フィールドが同時にバリアントコンストラクタに昇格する;満たさない場合、その型全体は通常の記録となり(関数フィールドは関数値を持つデータフィールド)、「一部バリアント、一部データ」の混合形態は存在しない。データとバリアント意味を同時に持ちたい場合、データ記録と和型の二つの型としてモデル化し、宣言を統合しない。
呼び出し形式:型限定、裸名なし
バリアントコンストラクタの唯一の呼び出し形式は型限定呼び出し——型値上でバリアントメンバを取得して呼び出す:
r1 = Result(Int, String).ok(5) // Result(Int, String)、ペイロード 5
c = Color.red() // ゼロペイロードバリアント:フィールドシグネチャと一貫した関数呼び出し形式裸名形式は提供しない(ok(5) はコンストラクタ呼び出しではない)。根拠:裸名は文脈の期待型からのバックトラックに依存して所属和型と型実引数を確定するが、本言語では型は明示的に書けなければならず、推論は期待位置から単方向に流れる(RFC-011a「Self は明示的型引数であり、魔法はない」と同じ規律)。限定名形式では型は完全に自足し、文脈を必要としない。将来裸名を導入する場合でも、「文脈上唯一確定可能」時の展開糖としてのみであり、意味基準は本節の限定名である。
推論:型自足
限定名に完全な型実引数が与えられれば、バリアントコンストラクタのシグネチャはフィールドシグネチャを型実引数で置換した結果:
Result(Int, String).ok // : (Int) -> Result(Int, String)ペイロード型チェックは通常の関数呼び出しの実引数チェックそのもの(不一致で E1002 報告)、新規推論ルールはない。ジェネリック和型のコンストラクタは型のインスタンス化と共に自然に単態化され、独立した機構はない。
フィールド昇格:バリアント名はデータフィールドではない
和型と判定された後、バリアント名はデータフィールド空間から削除される——和型値のバリアクト名フィールドアクセスはコンパイル時拒否:
r1.ok // コンパイルエラー:ok は Result のバリアントコンストラクタであり、データフィールドではない根拠:実行時の和型値は tagged union(下記)であり、バリアント宣言表を持たない;フィールドアクセスを許可すれば必ず静かに誤翻訳される。これは RFC-011a §1.2(フィールド/メソッド統一名前空間、衝突エラー)の規律の延長である。構築は型限定(Result.ok)、取出は match 分解(RFC-010b)。
実行時表現と等価性
和型値の実行時表現は tagged union:Enum { 型身份, variant_id, payload }。
- 型身份 は具体和型を運ぶ(グローバルプレースホルダではない)——和型を跨ぐ値は比較不能;
- variant_id は型体内でのバリアントの宣言順で番号付けされ、この順は同時に RFC-010b 網羅性チェックのバリアント集合入力である;
- 等価性(
==/!=):型身份が同じ、variant_id が同じ、payload が値ごとに等価(再帰)。
std 移行
std.result は本節の機構を使って ok / err を定義するように変更され(native result_ok / result_err は退く)、 std のバリアント構築はユーザー和型と同じルールを通り、特権通路はない。Result / Option の型位置での parser 特例と make_result の名前特例の撤去は RFC-011b フェーズ 2 が承接する——? のインターフェース化後 Result は通常の std 和型となる。
論理演算子:and / or / !(権威定義、Zig 式)
定義宣言(2026-08-03):論理演算子の権威的形式はキーワード
and/or+ 記号単項!(SPECsyntax.md§2.2 優先度表と一致)。本設計は Zig に揃える:短絡制御フローにはキーワード、純粋な単項演算には記号。初期実装が C に漂流した&&/||および中間状態のキーワードnotはすべて削除済み。
意味:
| 演算子 | 優先度(SPEC §2.2) | 結合性 | 意味 |
|---|---|---|---|
! | 3(単項前置、密結合) | 右から左 | 論理否定(純粋関数、制御フローなし) |
and | 10 | 左から右 | 短絡論理積 |
or | 10 | 左から右 | 短絡論理和 |
# 短絡評価:and の左が false / or の左が true のとき、右は実行されない
if x != 0 and y / x > 1 { ... } # x == 0 のときゼロ除算にならない
# 密結合:!a == b ≡ (!a) == b(Zig 式;Python の not a == b ≡ not (a == b) の逆)
!3 == 4 # false:(!3) == 4 → false
!(3 == 4) # true
!x != 0 # ≡ (!x) != 0
!list.is_empty(xs) # ≡ !(list.is_empty(xs))、呼び出し後に反転以下の記述はもはやサポートされない(lexer がエラーで対応する記述を提示):
x && y # ❌ 削除済、x and y を使用
x || y # ❌ 削除済、x or y を使用
not x # ❌ 削除済、!x を使用(not は通常の識別子に戻る;!= には影響なし)設計根拠(Zig に揃える、ziglang/zig#272 / #6625):
- 短絡は制御フロー → キーワード;純粋関数は演算 → 記号。
and/orは評価順序を変更し(右側は必要に応じてスキップ)、ifと同じ性質を持つためキーワード;!は既に評価された被演算子に純粋な反転を行い、-+と同じ性質を持つため記号。YaoXiang のエラー伝播は?(§2.11)であり、!と衝突しない。 - 密結合による曖昧性除去:
!は視覚的に被演算子に「密着」し、高い優先度が一目瞭然;キーワードnotは被演算子との間にスペースを強制され、どこに束縛されるか(not a == b)が脳内で曖昧になりやすい。 - 曖昧性解消:
&は二つの役割を担う——借用トークン(&p/&mut p、RFC-009)とビット AND(SPEC §2.2 優先度 8)。さらに&&を導入すれば、一つの記号に三つの意味を持たせることになる。and/or/!により借用、ビット演算、論理の三つの概念が視覚的に完全に分離される。 - 前例:Zig(同じ生態系の現代システム言語)こそが
and/orキーワード +!記号の組み合わせ;Python / Lua / Ada / SQL は全キーワード(notを含む)を使用、C 系は全記号を使用——YaoXiang は Zig のミックスを採用し、両者の利点を得る。 - Curry-Howard との一貫性:型は命題(上記同型節を参照)、精錬型での論理結合は
and/orで書く(例{ 0 <= idx and idx < arr.len })のが命題の自然な表現;!は単項否定記号として ¬ に対応する。
実装:
and/orは IR 層で短絡ジャンプシーケンスに展開され(a and b ≡ if a { b } else { false })、!は単項密結合として解析される(被演算子はBP_UNARY + 1で取得)。回帰テスト:tests/yaoxiang/01-syntax/basics/logical_ops.yx、logical_not.yx。
構文設計説明:名前付き関数は本質的に Lambda の糖衣構文
核心的理解
名前付き関数と Lambda 式は同じものである! 唯一の違いは:名前付き関数は Lambda に名前をつけただけ。
// この二つは本質的に完全に同じ
add: (a: Int, b: Int) -> Int = a + b // 名前付き関数(推奨)
add: (a: Int, b: Int) -> Int = (a, b) => a + b // Lambda 形式(完全に等価)糖衣構文モデル
// 名前付き関数 = Lambda + 名前
name: (Params) -> ReturnType = body
// 本質的には
name: (Params) -> ReturnType = (params) => body要点:シグネチャが完全に引数型を宣言しているとき、Lambda 頭の引数名は冗長になり、省略可能。
引数スコープルール
引数は外側の変数を覆い隠す:シグネチャ内の引数スコープは関数本体を覆い、内部スコープの優先度が高い。
x = 10 // 外側の変数
double: (x: Int) -> Int = x * 2 // ✅ 引数 x が外側の x を覆い隠す、結果は 20注釈位置は柔軟
型注釈は以下のいずれの位置にも書け、少なくとも一箇所に注釈が必要:
| 注釈位置 | 形式 | 説明 |
|---|---|---|
| シグネチャのみ | double: (x: Int) -> Int = x * 2 | ✅ 推奨 |
| Lambda 頭のみ | double = (x: Int) => x * 2 | ✅ 合法 |
| 両方に注釈 | double: (x: Int) -> Int = (x) => x * 2 | ✅ 冗長だが許可 |
完全な例
// ✅ 推奨:シグネチャ完全、Lambda 頭省略
add: (a: Int, b: Int) -> Int = a + b
inc: (x: Int) -> Int = x + 1
main: () -> Void = { print("hi") }
// ✅ 合法:Lambda 頭で型を注釈
double = (x: Int) => x * 2
// ✅ 合法:両方に注釈
double: (x: Int) -> Int = (x) => x * 2設計上の利点
| 特性 | 利点 |
|---|---|
| 簡潔 | シグネチャ完全時、引数名を重複記述不要 |
| 柔軟 | Lambda 形式を保持、好みの方を使用 |
| 一貫 | 変数宣言 x: Int = 42 と統一パターンを維持 |
| 直感的 | name: Type = body が「名前 name、型 Type、値 body」に直接対応 |
トレードオフ
利点
| 利点 | 説明 |
|---|---|
| 極限の統一性 | 一つの構文規則ですべての状況をカバー |
| 理論的にエレガント | 完全に左右対称の name: type = value |
| 新規キーワード不要 | 既存の構文要素を再利用 |
| 実装が容易 | コンパイラは一つの宣言形式のみ処理 |
| 学習が容易 | 一つのパターンを覚えれば全コードが書ける |
| 拡張が容易 | 新機能が自然にこのモデルに溶け込む |
欠点
| 欠点 | 説明 |
|---|---|
| 命名規則 | メソッドは Type.method 命名に従う必要 |
| 冗長 | 完全構文は簡略構文より長いが、推論可能 |
| 学習曲線 | 統一モデルの理解が必要 |
緩和措置
// 1. 明確なエラーメッセージ
// コンパイルエラー例:
// Error: Point does not implement Serializable
// Required method 'serialize: (self: Point) -> String' not found
// Note: Define Point.serialize to implement Serializable
// 2. 型推論
// 型を省略でき、コンパイラが推論
Point.draw = (self: Point, surface: Surface) => surface.plot(self.x, self.y)
// 3. IDE ヒント
// IDE が欠落メソッドを自動提示リスク
| リスク | 影響 | 緩和措置 |
|---|---|---|
| 解析複雑度 | 統一構文が解析複雑度を増す可能性 | 再帰下降パーサを使用 |
| 性能オーバーヘッド | vtable 検索に追加オーバーヘッドの可能性 | コンパイル時単態化最適化 |
隠し要素 🎮:言語の源
✨ Type: Type = Type ✨
// 型の型を定義してみる...
Type: Type = Type警告:これは名状しがたいものである!
╔══════════════════════════════════════════════════════════════╗
║ ║
║ 一生二、二生三、三生万物。 ║
║ 易有太极、是生两仪。 ║
║ ║
║ Type: Type = Type ║
║ 此乃爻象之源、语言之边界。 ║
║ 编译器在此沉默,哲学在此驻足。 ║
║ ║
║ 感谢你触达语言的哲学边界。 ║
║ ║
╚══════════════════════════════════════════════════════════════╝注:コンパイラは
Type: Type = Typeを正しく処理できない(Type0/Type1 宇宙パラドックスを引き起こす)が、この「隠し要素」を意図的に残している——コンパイルを試みると、言語の創始者からの禅的メッセージが返ってくる。これは単なる技術的境界ではなく、YaoXiang による型哲学への敬意である。
付録
構文 BNF
program ::= statement*
statement ::= declaration | expression
# 統一宣言:name: Type = expression
declaration ::= identifier ':' type_expr '=' expression
# 型式
type_expr ::= identifier
| identifier '(' type_expr (',' type_expr)* ')' # 型適用
| '(' type_expr (',' type_expr)* ')' '->' type_expr # 関数型
| '{' type_field* '}' # 記録/インターフェース型
| 'Type' # 元型
type_field ::= identifier ':' type_expr
| identifier # インターフェース制約
# ジェネリック引数:関数型の一部として、e.g. (T: Type, R: Type) -> (...)
# 独立した BNF 規則不要——: Type 引数は通常の関数引数そのもの
# 式
expression ::= literal
| identifier
| identifier '(' expression (',' expression)* ')' # 関数呼び出し / コンストラクタ呼び出し
| '(' expression (',' expression)* ')' # タプル
| expression '.' identifier '(' arguments? ')' # メソッド呼び出し
| lambda
| '{' field ':' expression (',' field ':' expression)* '}'
arguments ::= expression (',' expression)*
lambda ::= '(' parameter_list? ')' '=>' block
block ::= expression | '{' expression* '}'用語集
| 用語 | 定義 |
|---|---|
| 宣言 | name: type = value 形式の代入文 |
| 記録型 | 名前付きフィールドを含む { ... } 型 |
| インターフェース | フィールドがすべて関数型の記録型 |
| ジェネリック型 | Name: (T: Type) -> Type = { ... } と定義される型、型引数を受け取る |
| 名前空間関数 | Type.name 形式の関数、Type 名前空間に属する。暗黙の束縛は伴わない |
| メソッド束縛 | Type.name = func[n]、func の位置 n を呼び出し側として束縛し、obj.name(args) 構文を利用可能にする |
| ジェネリック関数 | (T: Type) 構文を使用する関数、型引数は最初の引数グループ |
| 元型 | Type、言語で唯一の型階層マーカー |
ライフサイクルと帰趣
┌─────────────┐
│ 草案 │ ← 現在の状態
└──────┬──────┘
│
▼
┌─────────────┐
│ 審査中 │ ← オープンコミュニティでの議論とフィードバック
└──────┬──────┘
│
├──────────────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 承認済み │ │ 拒否 │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ accepted/ │ │ rfc/ │
│ (正式設計) │ │ (元の位置) │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 実装へ │ │ 却下理由 │
│ (RFC実装) │ │ を保持 │
└─────────────┘ └─────────────┘