# Culebra 言語仕様 本書はCulebraプログラミング言語の構文と実行時セマンティクスを 定義する規範的ドキュメントです。2つのエンジン — バイトコードVMの executorとそのLLVM lowering(`--jit`とAOT)— は本書の記述どおりに 実装することを要求され、エンジン間または本書との差異はバグです。 実例つき入門は [`handbook.ja.md`](handbook.ja.md)、標準ライブラリの APIリファレンスは [`stdlib.ja.md`](stdlib.ja.md) を参照してください。 ## 目次 1. [概要と設計思想](#1-概要と設計思想) 2. [字句構造](#2-字句構造) 3. [文法](#3-文法) 4. [型](#4-型) 5. [値と同一性](#5-値と同一性) 6. [変数とスコープ](#6-変数とスコープ) 7. [式](#7-式) 8. [文字列と補間](#8-文字列と補間) 9. [配列](#9-配列) 10. [オブジェクト](#10-オブジェクト) * [`class` 構文](#class-構文) * [演算子オーバーロード](#演算子オーバーロード) * [タプル](#タプル) * [集合-set](#集合-set) 11. [関数とクロージャ](#11-関数とクロージャ) * [ジェネレータ (`yield`)](#ジェネレータ-yield) 12. [制御フロー](#12-制御フロー-if-cond-while-for) 13. [パターンマッチ](#13-パターンマッチ) 14. [オプショナル型注釈](#14-オプショナル型注釈) * [Sum type (`enum`)](#sum-type-enum) * [trait と protocol](#trait-と-protocol) 15. [エラー処理](#15-エラー処理) 16. [代数的エフェクト](#16-代数的エフェクト) 17. [メモリモデル](#17-メモリモデル) 18. [組み込み型のメソッド(イテレータプロトコル含む)](#18-組み込み型のメソッド) 19. [コア組み込み関数](#19-コア組み込み関数) 20. [多重ディスパッチ](#20-多重ディスパッチ) 21. [デコレータ](#21-デコレータ) 22. [コマンドラインインタフェース](#22-コマンドラインインタフェース) 23. [既知の制約](#23-既知の制約) 24. [モジュール](#24-モジュール) 25. [付録: VM ↔ JIT の差分](#25-付録-vm--jit-の差分) 26. [付録: conformance test 対応表](#26-付録-conformance-test-対応表) --- ## 1. 概要と設計思想 Culebraは動的型付けスクリプト言語です。設計上の優先事項: * **機能の直交性。** 新しい機能は文法の特別扱いとしてではなく、 既存の機能と組み合わさる形で入ります。 * **1つのフロントエンドと2つのエンジン。** バイトコードVMとLLVM ORC JITは同じパーサ・AST・バイトコードコンパイラを共有する2つの 消費者であるため、新機能は両方で実装できる必要があります。 * **予測可能なメモリ管理。** 両エンジンで参照カウントを採用。 循環参照はサイクルコレクタが回収します。 言語の核は動的型付けです。型注釈は任意で、実行時の境界(関数呼出、 注釈付き代入)でのみ検査されます。Culebraを静的型付け言語にするもの ではありません。 --- ## 2. 字句構造 ### ソースエンコーディング ソースコードはUTF-8。識別子はASCIIのみ。 ### 改行と空白 改行文字: `\n`。空白: スペース、タブ、改行。 CRLF(`\r\n`)で保存されたファイルも受け付ける。パースの前にLFへ正規化するので、 CRLFのcheckoutはLFのものと完全に同じに振る舞う — 三重引用符文字列やraw文字列の 中も含めて(そうしないと`\r`が値の一部になる)。`culebra fmt`はLFで書き出す。 単独の`\r`(後ろに`\n`が続かないもの)は3つめの改行ではなく構文エラー。 旧Macの改行なのか紛れ込んだバイトなのか決まらず、どちらかでプログラムの意味が変わる。 ### コメント * 行コメント: `# ...`または`// ...`、行末またはファイルの終わりまで。 * ブロックコメント: `/* ... */`、ネスト不可。 ### 識別子 IdentInitChar <- [a-zA-Z_] IdentChar <- [a-zA-Z0-9_] IDENTIFIER <- IdentInitChar IdentChar* 識別子は大文字小文字を区別します。 ### キーワード 変数名にできない予約語: nil true false mut debugger return while for in if unless else fn match break continue throw try catch defer class trait enum import export yield これらへの代入は`SyntaxError`です(`let class = 1`)。予約が効くのは 代入ターゲットの位置だけで、パラメータ名・`for`変数・オブジェクトのキー・ プロパティ名はキーワードが始まりえない位置なので、これらの語も受け付けます。 ただし`return`・`throw`・`break`・`continue`の4語は、束縛ではなく**読み 出す**ときだけ例外です。これらは式なので (§12)、式の位置に現れた時点で 制御を移す形式であって変数参照にはなりません。`for break in xs { f(break) }` は`break`を束縛しますが、渡す代わりにループを抜けます。オブジェクトの キーとプロパティ名はどちらも式の位置ではないので影響を受けません (`{break: 1}`も`o.break`もそのまま動きます)。 残りのキーワードは**文脈依存**で、それぞれの構文が始まりうる位置でのみ キーワードとして認識され、それ以外では普通の識別子です: cond nobreak from with effect perform handle static get by `static`と`get`はclass本体内の、`by`はrangeリテラル内 (`0..10 by 2`)のマーカーです。残る7語はそれぞれ固有の形を持つ構文を 導きます — `cond {` … `}`は`test => body`のアームが最低1つ必要、 `nobreak`はループ本体の後、`from`は`import`の後 — なので `let with = 1`も`if handle { … }`も変数を指します。**ループのラベル** (`outer: for … { break outer }`、§12)も同じく文脈依存ですが、語を 1つも予約しません。`while`/`for`の直前と`break`/`continue`の直後で しか認識されないので、使った名前は変数名としてそのまま空いています。 型注釈の型名 (`Nil`, `Bool`, `Long`, `Float`, `String`, `Array`, `Object`, `Function`, `Any`)も予約語ではなく、`:`や`->`の後に現れる場合のみ文脈的に 認識されます。 パーサは代入における`let`もオプショナルな接頭辞として認識します。 ### リテラル * 整数: `NUMBER`は64ビット符号付き整数(`Long`)。10進のほか、 基数プレフィクスでhex `0xFF` / 8進`0o755` / 2進`0b1010`も書ける (プレフィクス文字は大文字小文字不問、桁は基数に従う)。 * 浮動小数点数: `FLOAT <- [0-9]+ '.' [0-9]+ ([eE] [-+]? [0-9]+)? / [0-9]+ [eE] [-+]? [0-9]+`。 小数点(後ろに数字が続く必要あり、末尾の単独ドット`1.`は不可) または指数表記`e`/`E`を含むリテラルは`Float`(IEEE 754 binary64)。 例:`1.0`, `0.5`, `-2.5`(`2.5`に単項マイナス), `1e-5`, `1.5e3`。 * 文字列: `'...'`は **Raw文字列** で、クォート間の文字(バック スラッシュも含む)はすべてリテラル扱い。エスケープも補間もなく、 アポストロフィは含められません(`'`を含むRaw文字列はバック クォート`` `...` ``を使う)。 * 補間文字列: `"...{expr}..."`。`{expr}`は任意の式を埋め込めます。 対応エスケープ: `\n` `\r` `\t` `\\` `\"` `\{`(`\{`で補間を起動 しない素の`{`を埋め込み。`}`は補間終端でしか特別ではないので 生で書いてOK)。未知の`\X`はバックスラッシュ + 文字の2文字 としてそのまま残ります。 * 真偽値: `true`, `false`。 * Nil: `nil`。 **末尾カンマ。** 配列`[1, 2,]`・オブジェクト`{a: 1,}`(短縮形も同じで、 `{x,}`は`{x}`)・タプル`(1, 2,)`・引数リスト`f(1, 2,)`・パラメータリスト `fn g(a, b,)`・ラムダ引数`|a, b,|`・分配パターン`let [a, b,] = …` で、最後の要素の後ろに単一の末尾カンマを置けます(差分が綺麗になり、 隣の要素に触れずに並べ替えできる)。先頭カンマや二重カンマは 依然syntax error。カンマが意味を持つのは`(x,)`だけで、1要素タプルを表します。 ### 演算子・区切り文字 == != <= < >= > # 比較 + - * / % ** # 算術(`**`は冪乗)。`+`は文字列・配列の連結も行う @ # 行列積(`__matmul__`で定義) | & ^ << >> ~ # ビットor / and / xor / シフト / 補数(Long限定) ! # 論理否定 && || # 論理AND/OR(短絡) ?? # nilコアレス(`||`より低い優先順位) ?. ?[ ] # オプショナルチェイン / オプショナル添字 !! # 非nullアサーション(後置) .. ..= # 範囲リテラル(開区間 / 閉区間) = # 代入 += -= *= /= %= **= @= # 複合代入 &= |= ^= <<= >>= # 複合代入(ビット演算) ??= # nilコアレス代入 => # matchアーム区切り -> # 戻り値型 ... # restパターン | # orパターン区切り : # 型注釈 ( ) [ ] { } # グルーピング / コンテナ区切り , ; # 区切り . # メンバアクセス ### 文の区切り `;`または改行で区切ります。文の間の空行は無視されます。 --- ## 3. 文法 完全なPEG文法は単一の真実源で、パーサーはそれをそのまま読み込みます。 トップレベルのルール: PROGRAM <- STATEMENTS STATEMENTS <- (STATEMENT ((';' / newline) STATEMENT?)*)? STATEMENT <- DEBUGGER / RETURN / THROW / YIELD_FROM / YIELD / BREAK / CONTINUE / DEFER / IMPORT_STMT / EXPORT_STMT / EFFECT_FN_DECL / MULTIFN_DECL / ENUM_DECL / CLASS_DECL / TRAIT_DECL / LEXICAL_SCOPE / EXPRESSION EXPRESSION <- DESTRUCTURE_ASSIGN / PLACE_ASSIGN / ASSIGNMENT / TRY / CONDITIONAL ASSIGNMENT <- LET MUTABLE PRIMARY (ARGUMENTS / INDEX / DOT)* TYPE_ANNOTATION? ASSIGN_OP EXPRESSION CALL <- PRIMARY (ARGUMENTS / INDEX / SAFE_DOT / SAFE_INDEX / DOT / NONNULL)* PRIMARY <- WHILE / FOR / IF / MATCH / COND / HANDLE / PERFORM / RETURN / THROW / BREAK / CONTINUE / FUNCTION / LAMBDA / OBJECT / SET / ARRAY / NIL / BOOLEAN / FLOAT / NUMBER / REGEX_LIT / IDENTIFIER / TRIPLE_STRING / STRING / RAW_STRING / INTERPOLATED_STRING / TUPLE / '(' EXPRESSION ')' 文 (STATEMENT) は式の位置に置けない構文 — 各種宣言形式、`yield` / `yield from`、`defer`、およびモジュール形式 — です。それ以外は`if`・ `while`・`for`・`match`・`cond`・`handle`・`perform`を含めてすべて `PRIMARY`であり、これが値として使える理由です (§7)。 制御を移す4形式`return`・`throw`・`break`・`continue`も`PRIMARY`なので、 式が置ける場所ならどこにでも書けます。これらは値を生まず、到達した時点で 制御が移るため、外側の式が受け取るものがそもそも存在しません (§12)。 ### 演算子の優先順位 低い順: 1. `||` 2. `&&` 3. `==`, `!=`, `<`, `<=`, `>`, `>=` 4. 二項`+`, `-` 5. 二項`*`, `/`, `%`, `@`(matmul) 6. 単項`+`, `-`, `!`, `~`(右結合) 7. `**`(右結合)。単項プレフィックスより強く結合(`-2**2 == -4`)する 一方、右辺は単項プレフィックスを受理するので`2 ** -1 == 0.5`、 `2 ** -3 ** 2 == 0.00195…`はいずれもパースされる。 8. 呼出/添字/ドット (`x(...)`, `x[i]`, `x.k`) — 左結合 単項プレフィックスは`*`より強く結合するので、`-a * b`は`(-a) * b` です。数値ではどちらに解釈しても結果は同じですが、`__neg__`と `__mul__`を定義した型ではどちらが先に走るかが観測できます: ```culebra class N { new(v) { self.v = v } __neg__() { println('neg') N(-self.v) } __mul__(o) { println('mul') N(self.v * o.v) } } _ = -N(2) * N(3) # => | # neg # mul ``` `=`は右結合ですが、一般的な式演算子ではなく`ASSIGNMENT`内でのみ 現れます。 ### 式指向構文 多くの構文は値を生む式です: * `if { a } else { b }`は選ばれた分岐の値を返します。 * `match x { ... }`は一致したアーム本体の値(どれも一致しなければ `nil`)を返します。 * `cond { test => a, _ => b }`は最初に真になった`test`のアーム 本体の値を返し、どれも一致しなければ`nil`([`cond`](#cond) 参照)。 * `while`は常に`nil`を返します。 * 文の位置のブロック`{ ... }`は`LEXICAL_SCOPE`で、`nil`を返します。 式の位置では`{`は`OBJECT`リテラルを開始します。 * 連続した式の値は最後の式の値です。 --- ## 4. 型 Culebraの型は厳密に12種類 — `type_of`が返しうる名前がすべてです: | 型 | 説明 | |-------------|--------------------------------------------------------------| | `Nil` | 単一の値`nil` | | `Bool` | `true`または`false` | | `Long` | 64ビット符号付き整数 | | `Float` | IEEE 754 binary64(倍精度浮動小数点) | | `String` | イミュータブルなヒープ文字列 | | `StringView`| 他の文字列のバイト列をzero-copyで借用するイミュータブルなビュー。`slice` / `split` / `view`が返す(§18.1) | | `Array` | ミュータブルな順序付きコレクション | | `Object` | ハッシュ可能なキーと値のミュータブルな対応(Stringキーはshape経由の高速パス) | | `Function` | クロージャ(関数ポインタ + 捕捉セル) | | `Tensor` | N次元数値テンソル。lazy graphをCPU / GPUで評価する([stdlib §8](stdlib.ja.md#8-tensor)) | | `Tuple` | イミュータブルなハッシュ可能シーケンス(`(1, 2)`)。Object / Setのキーに使える | | `Set` | 挿入順を保持する一意ハッシュ可能値の集合(`Set(1, 2, 3)`) | class・enumバリアント・モジュール・イテレータ・stdlib namespaceは すべて`Object`です。`type_of`はモジュール・イテレータ・namespaceには `'Object'`を、classインスタンスにはそのクラス名を、enumバリアントには バリアント名を返します(§19)。 `Long`と`Float`の間では、算術時に自動的に`Float`へ昇格します (§7参照)。比較はこの昇格を通さず値どうしを厳密に比べます — 昇格すると 2^53を超えたところで別々の`Long`が1つの`Float`に重なるためです。 これ以外の型間の暗黙変換はなく、算術・比較・論理演算子が 期待しない型を受け取ると`type error`となります(§15参照)。 `Any`は型注釈でのみ有効で、任意の値に一致します。 **両エンジン対応**:`Float`はバイトコードVMと LLVM ORC JITのどちらでも完全にサポートされています。Longのみの コードパスは`--jit`下でも従来どおりインライン整数コード生成を 維持し、Floatが混ざった場合のみLong↔Float昇格のスローパスに 入る設計で、ビット単位で同一の意味論を保証します。 --- ## 5. 値と同一性 * `Nil`, `Bool`, `Long`, `Float`は値型です。値として比較・コピーされ、 `==`は格納されたデータで比較します。 * `String`, `Array`, `Object`, `Function`は参照型です。変数は ヒープ上のオブジェクトへの参照を保持します。代入や引数渡しは参照を コピーするだけで、オブジェクトはコピーしません。一方の変数を 通じた変更は同じオブジェクトを指すもう一方の変数からも見えます。 `==`の挙動: * `String`: 内容で比較。 * `Array`, `Object`, `Tuple`, `Set`: **値(構造的)等価**(要素を 再帰比較)。要素の対はそれぞれ`==`そのもので比べるので、要素が自分の `__eq__` / `eq`(*演算子オーバーロード*)を持っていれば、どの深さでも それが決めます — `a == b`なら`[a] == [b]`です。ただし**キーとして 照合されるもの**(`Set`のメンバ、`Object`の`String`以外のキー)は 検索時と同じ照合、つまり`hash`と`eq`によります。この段は`__eq__`を 読まず、型もまたがないので、`[1, 7] == [1.0, 7]`が真でも `Set(1, 7) == Set(1.0, 7)`は偽です。`Function`と`Tensor`のみ参照同一性。 (値等価でもArrayはハッシュ不可のまま=キーには使えない。) 型をまたぐ数値等価性:`Long`と`Float`は数値として比較されます。 `1 == 1.0`、`0 == 0.0`は真。`NaN`は自分自身とも等価になりません。 順序比較(`<`, `<=`, `>`, `>=`)の対応: * `Long`, `Float`, `Bool` — 数値順序(真偽値は`false` < `true`、 `Long`と`Float`は昇格を通さず値どうしを厳密に比較するので、 順序と`==`はどの対についても一致します)。 * `String` — 辞書順(バイト比較)。 * `Tuple` — 対ごと。`==`でない最初の対が結果を決め、そういう対が 無ければ2つは等しい(`(1, 2) < (1, 3)`、`(1, 2) <= (1, 2)`)。 長さは同じでなければなりません。短いTupleを小さいとはしないので、 `(1, 2) < (1, 2, 3)`は`type error`です。*タプル*を参照。 * `Nil` — `nil`同士は等価、順序比較は常に`false`。 * クラスが`__lt__`か`cmp`を定義している`Object` — *traitメソッドへのフォールバック*を参照。 (数値ペア以外の)異なる型同士の順序比較は`type error`になります。 `Array`どうし、`Set`どうし、どちらのメソッドも無い`Object`どうしも 同じです。 順序を付けるものは全てこの1つの規則を使います: 4つの演算子、 `sort` / `sorted`、`sort_by` / `sorted_by`のキー、`min` / `max`と `min_by` / `max_by`のキー、派生`cmp`のfield。 --- ## 6. 変数とスコープ ### 宣言と代入 x = 10 # bare代入 let y = 20 # let宣言(イミュータブル) mut z = 30 # mut宣言(ミュータブル) let mut w = 40 # `mut w = 40`と同等 let {name, age} = person # オブジェクト分解束縛(省略形) let {name: nm, age: a} = person # 改名 / 値マッチ let {user: {name}} = req # ネスト分解 let [a, b, ...rest] = xs # 配列分解束縛(rest可) let (x, y) = pair # タプル分解 let mut {x, y} = point # 分解もlet mutで可変に 単純な識別子を左辺とする代入の扱い: * `let`または`mut`がある場合、現在のスコープに新しい束縛を作成 します。**外側の関数からキャプチャされる変数と同名の場合はコンパ イルエラー**になります(後述「シャドウ禁止」参照)。 * `let`/`mut`がない裸の`x = v`はスコープ連鎖を検索します: * どこかのスコープ(外側のクロージャやグローバルを含む)に`x`が 存在すれば、その束縛を再代入します。外側の捕捉変数が更新される 可能性があります(§11)。これが**クロージャベースのオブジェクト が状態を書き換える仕組み**です。 * 見つからなければ、いちばん内側のスコープに新しい(イミュータブルな) 束縛を作成します。代入を書いたブロック、ループ本体、matchの腕、 `if` / `cond` / `?:`の腕がそれにあたります。腕の中で作った束縛は 腕の終わりで消え、`if`の後では見えません。値を外に出すには`if`自体の 値を束縛します(`x = if c { 1 } else { 2 }`。名前が2つなら `(a, b) = if c { (1, 2) } else { (3, 4) }`)。 ### 複合代入 12種類の複合代入演算子があります。いずれも`let`/`mut`と組み合わ せることはできません(既存束縛の更新だけが対象です)。 x += 1 # x = x + 1 x -= y # x = x - y x *= 2 x /= 3 x %= 5 x **= 2 x @= M # 行列積(Tensor / __matmul__ 経由) x &= MASK # ビット演算の5つは演算子と同じくLong専用 x |= FLAG x ^= 1 x <<= 2 x >>= 2 左辺は識別子・配列要素 (`a[i]`)・オブジェクトプロパティ (`o.x`) のいずれでも構いません。配列インデックスとプロパティ名はちょうど 1回評価されます。未定義の名前への複合代入はエラーです。 a[next_idx()] += 1 # next_idx() は1回だけ呼ばれる o.count += delta bogus += 1 # error: compound assignment on undefined name **Tensorとの相互作用(in-place書き込み):** 左辺がTensorで、自前のバッファを所有しており、形が右辺のbroadcast 結果と一致する場合、`+=` `-=` `*=` `/=` `**=`は新しいTensorを 割り当てず既存バッファを直接書き換えます(`%=`はTensorで意味づけ されておらず、`@=`は出力形状が変わるのでin-place対象外)。同じTensorを別名で参照 していると、その別名でも変更が見えます(NumPyと同じセマンティクス)。 mut W = Tensor.randn('f32', 1024, 256) let alias = W W -= grad * lr # Wのバッファを直接書き換え Tensor.eval(alias) # alias.to_array() でも更新後の値が見える `W = W - grad * lr`と書くと毎ステップ新しいTensorを確保するので、 SGDの重み更新のような巨大テンソルのループでは`-=`のほうが大きく 速くなります(in-placeなら1ステップあたりWのサイズ分の確保が 消える)。`W`がビューや評価前のグラフノードの場合はin-place条件を 満たさないので、その場合は新規Tensorを作る通常経路に自動でフォール バックします(観測可能な挙動の差はありません)。 ### `_` シンク(束縛しない捨て値) `_`は「束縛しないsink」です。任意の束縛フォームで右辺を評価 (副作用も走ります)した上で、値は **どこにも保持せず破棄** します。 そのため同じスコープ内で`_`を何度繰り返しても衝突せず、後述の シャドウ禁止規則も`_`には適用されません。 let _ = side_effect() # 値は捨てるが副作用は走る let _ = 1; let _ = 2 # 同一スコープでの繰返しもOK for _ in 0..n { count = count + 1 } # n回ループ、ループ変数なし fn (_, _, x) { x } # 第3引数だけ束縛 try { ... } catch _ { recover() } # 例外値は捨てる let [first, _, third] = [1, 2, 3] # 配列スロットを無視 let [_, ..._, last] = xs # 先頭と中間を捨てる match v { [a, _, c] => a + c } # パターン内のスロットもsink `_`は読み取れません。束縛されていないので後から`_`を参照すると `undefined variable '_'`エラーになります。このsinkルールは `let`/`mut`宣言、`for ... in`、関数パラメータ、`try ... catch`、 分解束縛 / `match`のパターンスロットすべてに一様に適用されます。 (ただし`Object`パターン`{ _, x }`の`_`は特殊で、オブジェクトに リテラルキー`_`が存在することを要求し、sinkにはなりません。捨て たい場合は配列パターンや位置指定パターンを使ってください。) ### シャドウ禁止 関数内で新しい束縛を導入するとき、外側の関数で宣言された同名の変数が 内側の関数を書いた位置から見えている(つまり内側の関数がキャプチャ しうる)場合、エラーになります。このルールは名前を宣言するすべての 形に一様に適用されます: * `let` / `mut`宣言。分割代入パターンが宣言する各名前を含む (`let (a, b) = pair`、`let {x, y} = point`、`let [h, ...t] = xs`) * 関数パラメータ (`fn (name) { ... }`)。パターンパラメータが束縛する 名前を含む * `match`パターン束縛 (`match v { name => ... }`)、`for`の変数、 `catch`の変数 * ネストした`fn`・`class`・`enum`の名前と`import` パラメータのデフォルト値やクラスフィールドの初期化子の中で書かれた 宣言にも適用されます。裸の`x = v`はシャドウしません。見えている`x`に 書き込むか、見えていなければ新しく宣言します。 例: make_bumper = fn () { mut count = 0 bump = fn () { mut count = 10 # エラー: 外側変数 'count' をシャドウできません } incr = fn (count) { # エラー: パラメータがキャプチャ 'count' をシャドウ count + 1 } peek = fn (v) { match v { count: Long => count # エラー: パターンがキャプチャをシャドウ } } } これは「新しい束縛を導入したつもりが、外側にキャプチャされた同名が 既に存在し、外側を書き換える意図が静かに壊れる」という典型的バグを 防止するためのルールです。内側の変数名を変更すれば解決します。 次のケースではシャドウが**許可**されます: * **グローバル・組み込み関数**(`inspect`、`min`、トップレベル変数 など)は常にシャドウ可能。ローカル関数で`mut min = arr[0]`と 書くのに何のコストもかかりません。 * **同関数内のブロックスコープ**(`if` / `cond` / `?:`の腕を含む)内の シャドウは許可されます: fn () { a = 0 { let a = 1; ... } # OK: ブロック限定の新規束縛 if c { let a = 2 } # OK: 腕限定の新規束縛 } * **内側の関数から見えない名前**はキャプチャされないため、内側の関数で 宣言できます。閉じた兄弟ブロック、別の`match`アーム、内側の関数が 書かれていないループ本体などです: fn outer() { { let a = 1 } # この`a`はブロック終了で消える inner = fn () { let a = 2; a } # OK: ここから見える`a`は無い } 外側の関数が内側の関数を囲むスコープで**後から**宣言する名前は、内側の 関数からも見えます(クロージャは変数そのものをキャプチャします)。この 場合は引き続きエラーです: fn outer() { inner = fn () { let a = 2 } # エラー: 下の`a`が`inner`から見える let a = 1 } エラーは`ShadowError: cannot shadow outer variable 'a' (declared in an enclosing function)`です。 制約は**関数境界を越える場合のみ**適用されます — クロージャで キャプチャした状態は裸の`x = v`で 変異させ、新規ローカルは`let`/`mut`で宣言します。 ### 可変性 束縛はデフォルトでイミュータブルです。`let`なしでイミュータブルな 束縛に再代入すると`immutable variable 'x'...`になります。 `mut`は束縛の可変性を指定するだけで、値そのものの可変性とは 無関係です。例えば`let arr = [1,2,3]`でも`arr.push(4)`は合法で、 ポインタ自体が変わらないので問題ありません。 ### スコープ * 関数本体は新しい関数レベルスコープを導入し、パラメータはここに 束縛されます。 * 文の位置のブロック`{ ... }`(`LEXICAL_SCOPE`)は閉じ括弧まで 有効な入れ子スコープを導入します。 * `for`と`while`の本体は**反復ごとに新しいスコープ**を導入します。 本体内で導入した束縛はループ外に漏れず、反復をまたいで残りません (したがって裸の`x = …`は毎反復で新規宣言になり、再代入では ありません)。本体内の`defer`は各反復の終わりに走ります。 * `match`アームはパターンの束縛、ガード、本体を覆うスコープを 導入します。 * `if`の各腕(`then`本体、すべての`else if`、`else`)、`cond`の各腕、 三項演算子`c ? a : b`の各腕は、`{}`ブロックと同じく独自のスコープです。 腕の中で宣言した名前や書いた`fn`は腕の終わりで消え、腕の中の`defer`は 腕が終わるときに発火します。裸の`x = v`は、そこから見える`x`があれば それに書き込み、見える`x`がないときだけ腕の中で宣言します。テストは 腕に含まれません。テストが宣言する名前(`else if (let m = f()) > 0`)は `if`を囲むスコープに属します。init節(`if mut d = f(); …`)は`if`全体を 囲むスコープなので、その名前はすべてのテストとすべての腕に届きます。 ### 代入先 複雑な代入先もサポート: arr[0] = x # インデックス代入 obj.key = v # プロパティ代入(なければ作成) ### Design note: なぜ 3 層別々のシャドウ規則か Culebraはシャドウを3つの軸で独立に扱います: | スコープ関係 | Culebra | 一般的な慣行 | |---|---|---| | 関数境界越え(クロージャキャプチャ) | **エラー** | 警告or許可 | | 同関数内(ブロックスコープ、腕のスコープ) | 許可 | 警告or許可 | | グローバル / 組み込み名 | 許可 | 警告or許可 | 3つの位置に別々のポリシーを当てているのは、「クロージャによる オブジェクト」イディオムにおいて各位置が異なる役割を持つためです: * **キャプチャされた状態 = オブジェクトの状態**。クロージャベース のオブジェクトパターン([`handbook.ja.md` §9.2](handbook.ja.md#92-クロージャベースの別解) 参照)では、外側関数 の可変束縛がオブジェクトのprivateフィールドに相当します。 `mut x = ...`を新規ローカル宣言のつもりで書いてしまうと、 **静かにオブジェクトが壊れる**。これをコンパイル時エラーに するコストは十分見合います。 * **ブロックスコープ = 一時的な計算領域**。同一関数内の `{ ... }`は局所計算のstaging areaで、`let a = transform(a)` のようなrebindingは意図的パターンであってバグではありません。 制限する理由がない。 * **グローバル = 共有の語彙**。`inspect`, `to_string`, `Math`, `IO`などの builtinsやtop-level名はambientな存在と理解されており、 `mut min = arr[0]`のようなローカルは使い勝手の良いイディオム であって混乱の種ではない。renameを強制すると安全性の利得なく 摩擦だけが残ります。 結果として、この規則は設計意図どおりのバグクラス — 「新規ローカル」 と「外側再代入」の取り違え — を捕捉しつつ、シャドウが自然な 2つのシチュエーションには干渉しません。先行技術との対比: --- ## 7. 式 ### 算術 `+`, `-`, `*`, `/`, `%`, `**`は数値(`Long`または`Float`)を2つ 受け取ります。数値以外が混ざると`type error`です。 * **両辺`Long` → `Long`**。整数演算。除算と剰余は0方向への切り 捨て(床方向ではないので、符号によって丸め方向は変わらない)。 `7 / 2 == 3`、 `-7 % 3 == -1`。オーバーフローはラップ(多倍長なし)。 床剰余 — 負のインデックスを負のままにせず`0..n`に巻き戻す方 — が欲しい場合は [`Math.wrap`](stdlib.ja.md#mathwrapx-long-n-long---long) を使います。 * **いずれかが`Float` → `Float`**。`Long`側は`Float`に昇格し、 IEEE 754 binary64で計算します。`1 + 2.0 == 3.0`、 `3 / 2.0 == 1.5`。 0除算 / 剰余は`Long / 0`でも`Float / 0.0`でも `divide by 0 error at L:C`を投げます。 `**`(冪乗)はやや複雑なルールです: * `Long ** 非負Long → Long`(整数冪;オーバーフローはラップ)。 `2 ** 10 == 1024`。 * `Long ** 負Long → Float`。`2 ** -1 == 0.5`。`0 ** -1`は `divide by 0 error`。 * いずれかが`Float` → `Float`(`std::pow`)。 `2.0 ** 0.5 ≈ 1.4142`。 * `**`は右結合で、単項マイナスより強く結合します。右辺は単項 プレフィックスを受理するので`2 ** 3 ** 4 == 2 ** 81`と `2 ** -1 == 0.5`はいずれもそう解釈されます。 ### ビット演算 `|`(or)、`&`(and)、`^`(xor)、`<<` / `>>`(左 / 算術右シフト)、 単項`~`(補数)は **2つの`Long`**(`~`は1つ)に作用します。非`Long` は`type error`。`>>`は符号を保つ算術シフト。シフトも他の`Long`演算 同様にラップします(bignumなし)。シフト量は64を法(下位6ビット)と して扱うため`1 << 64 == 1`、負のシフト量も同様にラップします。 0b1100 & 0b1010 # → 8 12 ^ 10 # → 6 0b1010 | 0b0101 # → 15 1 << 4 # → 16 ~0 # → -1 5 & ~1 # → 4 (低ビットをクリア) READ | WRITE # disjointなフラグビットを合成 優先順位: 比較`<` or `|` `<` xor `^` `<` and `&` `<`シフト`<`加算。 よって`1 << 2 + 1 == 8`(`1 << (2+1)`)、`2 & 3 ^ 1 == 3` (`(2 & 3) ^ 1`)、`1 | 2 == 3`(`(1 | 2) == 3`)。`~`は単項レベル。 **`|`と`|...|` lambda:** bit-OR `|`は通常式が来る場所すべてで使えます (lambda本体`|x| x | 1`も可)。唯一の例外は**パラメータのデフォルト値**で、 そこでのtop-level `|`はパラメータリストの閉じ`|`と曖昧になるため括弧が 必要です: `|x = (A | B)|`。`||`は従来通りlogical-or。 ### 比較 * `==`, `!=`: 任意の値(参照型については §5参照、数値型間の `1 == 1.0`については §5の等価性節参照)。 * `<`, `<=`, `>`, `>=`: 原則として同じ型同士。ただし`Long`と `Float`は数値として比較できます(`1 < 1.5`が有効)。これ以外 の型をまたぐ順序比較は`type error`。順序を持つ型は §5にあります。 * **チェーン**: `a < b < c`は`(a < b) && (b < c)`の意味。中間の `b`は一度だけ評価され、最初に偽になった所で短絡します。 比較演算子は混在可: `0 <= i < n`、`lo < x <= hi`、`a == b == c`。 ### 論理 * `!x`: `x`が真偽値変換可能である必要あり。 * `x && y`: まず`x`を評価、偽なら`x`を返し`y`を評価しない。真 なら`y`を評価して返す。短絡評価。 * `x || y`: まず`x`を評価、真なら`x`を返し`y`を評価しない。偽 なら`y`を評価して返す。短絡評価。 * `x ?? y`: `x`が`nil`でなければ`x`を返し、そうなら`y`を評価 して返す。短絡評価(LHSが非nilならRHSは評価されない)。 `||`より優先度が低い。左結合: `a ?? b ?? c` = `(a ?? b) ?? c`。 ### null 安全アクセス `??`と`T?` Optional型(§14)を補完し、nilの可能性がある値を扱う ための演算子。 * `a?.b` / `a?.m(...)` — **オプショナルチェイン**: `a`が`nil`なら 残りのチェーン全体が`nil`に短絡する(プロパティ参照やメソッド 呼び出しは引数ごとスキップ)。非nilなら`a.b` / `a.m(...)`と同じ。 `a?.b.c`は`.c`も含めて短絡するので、各リンクを守るには`a?.b?.c`。 * `a?[k]` — **オプショナル添字**: nilレシーバは`nil`に短絡、 それ以外は`a[k]`と同様(Array / Tuple / Object)。 * `expr!!` — **非nullアサーション**: `expr`をそのまま通すが、`nil` なら`NilError`。後置なので連鎖可: `a!!.b`, `a!![0]`。 * `a ??= b` — **nilコアレス代入**: `a`が現在`nil`のときだけ`b`を 代入し、それ以外では`b`を評価しない(短絡)。対象は単純変数の他、 `obj.key`・`obj[k]`(classインスタンスの`__index__`/`__setindex__` フォールバック込み)・`arr[i]`にも使える。存在しないObjectキーは 素の`obj.key`読み取り(§10)と同じく`nil`扱いなので、`obj[k] ??= v` はキーが無ければ挿入する — 実行時に挿入されたキーのmutable-by-default ルール(§10)に従い、既存キーがnil値を持つ場合はそのスロットの `mut`フラグに従う。Array要素は自動拡張しない: 範囲外の`i`は引き続き `IndexError`。`FixedArray`/`BoundedArray`/`SharedBuffer`要素や`@packable`のpacked フィールド(packed scalarにnilセンチネルは無い)、`Shared.new`ビュー (常にimmutable)には非対応。 ### 真偽値変換 `Bool`, `Long`, `Float`のみ真偽値変換可能: * `Bool`: そのまま。 * `Long`: `0`は偽、それ以外は真。 * `Float`: `0.0`(および`-0.0`)が偽、それ以外の有限値は真、 `NaN`は真 — 真偽値変換が問うのは「そもそも数値か」であって 「使える数値か」ではありません。 * `Nil`, `String`, `Array`, `Object`, `Function`: **変換不可** — 真偽値 文脈(例: `if s { ... }`)に渡すと`type error`。 (これは意図的に厳格です。必要なら`s != nil`や`!arr.empty()`と 明示的に書いてください。) ### 単項 `+x`は恒等(数値必須)。`-x`は`Long`または`Float`の符号反転。 ### 括弧 `(expr)`はグルーピングで、スコープは導入しません。 ### メソッド呼出 メソッド呼出は`receiver.name(args)`の形です。完全な解決規則 —— メソッド/UFCSのディスパッチ順、組み込みとユーザ定義プロパティの 相互作用 —— は §10「メソッドとUFCS」で定義します。演算子オーバー ロード(`+`, `-`, `*`, `==`, `@`, ...)と`__str__`の定義も §10に あります。 ### 評価順 部分式は**ソース上の左から右へ順に**評価します。これはすべての backendで規範的なルール。JITは中間IRを並べ替えてよいが、観測 可能な副作用順は維持しなければなりません。 * **関数呼出の引数.** `f(p1, p2, **splat, k: kv)`は`p1` → `p2` → `splat` → `kv`の順で評価。positional / `**` splat / keywordが混ざっていてもコールサイトの並び順に従う。 * **配列・オブジェクト・タプルリテラル.** 要素式 / プロパティ式はソース順。`{a: v1, b: v2}`なら`v1` → `v2`。 サイズ指定形式`[n; default]`はcount → defaultの順。 * **2項演算子`+`, `-`, `*`, `/`, `%`, `==`等.** 左辺式 → 右辺式 → 演算。オペランドの値は評価した時点のもので、あとのオペランドが その変数を代入し直しても変わらない(`n = 1`なら`n + (n = 10)`は `11`)。呼び出し先やレシーバーとその後ろの引数も同じ。 * **`||`, `&&`, `??`.** 最初の決定的オペランドで短絡。`||`は最初の truthy、`&&`は最初のfalsy、`??`は最初の非`nil`で停止。 以降のオペランドは評価しない。 * **代入`lval = rhs`.** RHSを先に評価し、その後LHSのtarget chain(`obj[k] = ...`なら最終キー`k`も含めて)を評価して store — 値が先に決まり、その置き場所は後から解決されます。 * **複合代入`lval op= rhs`.** RHSを先に評価。LHS chainは subscriptキーを含めて**1回だけ**評価し、暗黙のreadとstoreの 両方で同じ評価結果を使い回す。キーが再評価されることはない。 * **メソッド呼出.** 受信側式(receiver)を先に評価、その後引数 リストを上記のソース順で評価。 迷ったら一時変数に分解して書いてください — このspecはそれを 不要にするために存在します。 --- ## 8. 文字列と補間 ### Raw 文字列リテラル 'hello' 'C:\path\to\file' # バックスラッシュもそのまま — エスケープ解釈なし 'a\nb' # 4文字: a, \, n, b シングルクォート文字列は **Raw**(生)です。クォート間のすべての 文字を逐字的に取り、エスケープも補間もしません。`'...'`が唯一 含められないのがアポストロフィ`'`で、その場合は下のバッククォート 文字列を使ってください。Raw・補間どちらも複数行にまたがれます (ソースの改行が値に含まれる)。なので`'\d+'`はそのままregex リテラル、`"...\n..."`は複数行テンプレ。 ### バッククォート Raw 文字列リテラル `it's` # シングルクォートを含められる `say "hi"` # ダブルクォートもエスケープ不要 `\d{4}-\d{2}` # 量化子つきregexがそのまま バッククォート文字列(Go流)は **`'...'`と同じRawだが`'`・`"`・ `{`も含められる** — バッククォート間は全バイト逐字、エスケープも 補間もなし、複数行可。含められない唯一のバイトはバッククォート自身。 アポストロフィと波括弧が混在するregex(例: GPT-2 pre-tokenizer `` `'s| ?\p{L}+|\s+(?!\S)` ``)に最適で、`'...'`(`'`不可)でも `"..."`(`{...}`を補間)でも書けないケースを埋めます。 ### Regex リテラル re'\d+' # コンパイル済みRegex。Regex.compile('\d+') と同じ re"\d{4}-\d{2}" # reが本体をRaw化するので {4} は量化子 re`["']\w+` # backtick本体: ' も " も含められる re"hello"i # 末尾フラグ(i / m / s) re"^${word}$" # ${expr} 補間(エスケープ/合成。下記参照) `re'...'` / `re"..."` / `` re`...` ``リテラルは、コンパイル済みの [`Regex`](stdlib.ja.md#14-regex) 値に評価されます — 実体は `Regex.compile(<本体>, <フラグ>)`そのもので、RegexオブジェクトAPI 全部をそのまま連鎖できます: re'\w+'.find(" hello world").value # => "hello" re"hello"i.test("HELLO") # => true **本体はクォートの種類に関わらずRaw** — `re`プレフィックスがエスケープ 解釈を無効化するので、`\d`・`\w`・`{n}`量化子はそのまま通り、閉じ クォートだけが区切りになります。 唯一の構造化された形が **`${expr}`補間** です。通常の`"..."`の素の `{`ではなく`$`を使うのは、regexの`{n}`量化子と衝突しないため です(`$`アンカーが量化されることは実際まずありません): let word = "a.b" re"^${word}$".test("a.b") # => true re"^${word}$".test("axb") # => false — `.`はエスケープされる 補間は **型駆動で、デフォルトでインジェクション安全** です: - **String**(および非Regex値は文字列化される)は *エスケープ* され、 **リテラルとしてマッチ** — 値中のメタ文字は特別な意味を失う; - コンパイル済み **`Regex`** は非キャプチャグループ`(?:src)`として 展開され、量化子や選択がパターンに**合成**される(そのRegexが持つ インラインflagはマッチ全体に適用される点に注意)。 ``` let digits = re"\d+" re"id=${digits};".test("id=123;") # => true — 合成 ``` `{`の直前にリテラルの`$`を置きたいときは`\$`(regexのリテラル ドル用エスケープ)を使います。これは補間も抑制します — `re"\${2}"`は ドル記号2つにマッチ。**完全に動的な**パターンは従来どおり `Regex.compile(...)`で組み立ててください: let n = 4 Regex.compile('\d{' + n.to_s() + '}') 末尾のフラグ文字(`i`大文字小文字無視 / `m`複数行 / `s`ドットが改行に マッチ)は閉じクォートの直後: `re"^b"m`。`/.../ `形式はありません — `/`は除算で、両者の区別にはculebraが意図的に避けるステートフルな字句 解析が要るためです。`re`がregexプレフィックスになるのはクォートが直後に 続くときだけで、それ以外は普通の識別子です(`let re = 5`は問題なし)。 ### 三重引用符文字列 """複数行で "引用符" や 'アポストロフィ' や {補間} を含められる""" `"""..."""`は`"..."`と同じく補間される(`{expr}` / `{x:spec}`、 `\n` / `\{`エスケープも同じ)が、内部のシングル/ダブル引用符は エスケープ不要 — `"""`だけが終端。埋め込みLLM promptや引用符を 含むスニペットに便利。リテラルの開き波括弧は`\{`。 #### ブロック形式(dedent あり) 開きの`"""`の直後が改行のとき、その文字列は *ブロック文字列* になります。 開き`"""`直後の改行と、閉じ`"""`直前の改行は値に含まれず、各行は閉じ `"""`のインデント分だけdedentされます。これにより、周囲のコードに合わせて リテラルをインデントしても、そのインデントが文字列に漏れません。 let html = """ """ # => "\n" dedentはリテラル構築時に1回だけ解決されます(JITは純リテラルのブロックを 単一定数に畳み込みます)。確保済み文字列に対して別途実行時に`trimIndent` 相当のパスを掛けることはありません。閉じデリミタより深い相対インデントは保持 され、空行は空に正規化されます。閉じ`"""`のインデント未満の非空行や、独立 行にない閉じ`"""`は`SyntaxError`です。 開きの行から本文が始まる三重引用符(1行`"""..."""`を含む)は *ブロック文字列 ではなく*、従来どおりdedentなしの生の文字列として扱われます。 ### 補間文字列 "hello {name}" "sum = {a + b}" "nested: {if x > 0 { 'pos' } else { 'neg' }}" "line one\nline two" "literal brace: \{ and tab:\there" `"..."`文字列は、プレーンテキスト部分と`{expr}`部分の連鎖です。 各式は評価され、表示形式に変換(下記)されて周囲の文字列と連結 されます。 エスケープシーケンス(プレーンテキスト部分のみ): | エスケープ | バイト / コードポイント | | ---------- | ------------------------------------------- | | `\n` | 改行 (0x0A) | | `\r` | 復帰 | | `\t` | タブ | | `\\` | バックスラッシュ | | `\"` | ダブルクォート | | `\{` | 素の`{`(補間を起動しない) | | `\xHH` | 16進2桁の生バイト1つ(`0x00`–`0xFF`) | | `\uXXXX` | BMPのUnicodeスカラー値(16進ちょうど4桁)、UTF-8エンコード | | `\UXXXXXXXX` | Unicodeスカラー値(16進ちょうど8桁)、UTF-8エンコード | `\xHH`は生バイトを書きます — `String`はbyte stringなので、不正な UTF-8になるバイト(例`"\xff"`)も生成できます。`\uXXXX`/`\UXXXXXXXX`は コードポイントを書き、どちらも`U+10FFFF`超とサロゲート範囲`U+D800`–`U+DFFF` (Unicodeスカラー値でない)を拒否します。BMPを超える範囲を書けるのは `\U`のみです。`}`は補間終端としてのみ 特別なのでエスケープ不要。未知の`\X`は2文字(`\`と`X`)として そのまま残ります。埋め込みNUL(`\x00` / `\u0000`)は普通のバイトで、 `size()`が数え、すべてのString操作が保持します — `String`はNULで 終端しません。 ### フォーマット指定 補間はコロンの後にフォーマット指定を付けられる — `{expr:spec}`。 specはC++ `std::format`のミニ言語: `[[fill]align][sign][#][0][width][.precision][type]`。 "{pi:.2f}" # → 3.14 (固定小数点、2桁) "{5:.2f}" # → 5.00 (Longもfloat specで変換される) "{n:05}" # → 00042 (ゼロ埋め幅) "{255:#x}" # → 0xff (16進・接頭辞付き) "{name:>10}" # → 幅10で右寄せ "{score:+}" # → +42 (符号明示) 数値はspecの型文字に従う: float型(`f`/`e`/`g`)はLong/Floatを 浮動小数で、整数型(`d`/`x`/`o`/`b`)は整数で整形。その他の値は表示 文字列を整形(幅・寄せが効く)。型に不正なspecは`ValueError`。 `std::format`にそのまま従うので`,`の桁区切りは非対応です (その用途はロケール`L`オプションが担当)。 spec自身も`{expr}`フィールドを持てるので、幅や精度をソースに直書き せず計算できます: "{s:>{w}}" # 幅`w`で右寄せ "{x:.{p}f}" # 小数`p`桁 "{x:{w}.{p}f}" # 1つのspecに両方 フィールドは普通の式で、書かれた位置で(左から右へ、整形対象の値の 後に)評価されます。値は`Long`でなければなりません — 10進表記として specに差し込まれるものであり、それが幅・精度の正体だからです。 他の型はフィールド自身の位置で`TypeError`になります。フィールドは 同じ桁を手で書いたのと**厳密に同じ意味**になるので、幅`0`は「幅なし」 ではなくゼロ埋めフラグとして読まれます: 数値なら問題なし (`"{n:>{0}}"`)、文字列なら`ValueError`で、これは`"{s:>0}"`が元から そうであるのと同じです。補間が使えるところならどこでも使えます: ```culebra let w = 6 inspect(["a", "bb"].map(|s| "{s:>{w}}")) # => [' a', ' bb'] ``` ### 表示変換 `"..."`内の式が`String`以外の場合、表示形式が挿入されます: * `Nil` → `nil` * `Bool` → `true`または`false` * `Long` → 10進表記 * `Array` → `[v1, v2, ...]`(`inspect`形式。内部の文字列は引用符付き) * `Object` → `{key: val, mut key2: val2, ...}`(挿入順) * `Function` → `[function]` `"..."`内の`String`値はそのまま挿入されます(引用符なし)。これが `inspect`と表示が異なる唯一の点です。 循環データ(`a.c = a`等)は再入箇所で`{...}` / `[...]`と表示され、 無限再帰を防ぎます。 ### 連結 `+`演算子は2つの文字列を連結し、新しい`String`を返します。 'foo' + 'bar' # 'foobar' 'a' + 'b' + 'c' # 'abc' `+=`は末尾に追加します(文字列はimmutableなので、連結結果を変数・ 要素・フィールドに再束縛します)。 let mut s = 'a' s += 'b' # sは 'ab' になる 両辺とも`String`または`StringView`である必要があり、結果は常に ownedな`String`です。文字列と非文字列の連結(`'n: ' + 1`)は `TypeError`になります。型を混ぜる場合はinterpolation(`"n: {1}"`)か `to_string`を使います。 配列も同じように連結できます(§9)。他の型はできません。 --- ## 9. 配列 ### 構築 [] # 空配列 [1, 2, 3] # 3要素 [1, 2, 3](5, 0) # 5要素: [1, 2, 3, 0, 0] [](5, nil) # nil 5個の配列 [1](10, 0) # [1, 0, 0, 0, 0, 0, 0, 0, 0, 0] オプションの`(count, default)`指定は、まず`count`個の`default` 要素で埋め、次にリテラルで先頭位置を上書きします。`default`を省略 すると`nil`で埋められます。負の`count`は`ValueError`で、確保できない ほど大きい`count`も`ValueError`(`array size is too large`)です。 **スプレッド。** `...iterable`要素は別コレクションの要素を展開する: [0, ...a, 4] # → [0, , 4] [...a, ...b] # 連結 対象は`Array` / `Tuple` / `Set`(非イテラブルは`TypeError`)。 **連結。** `+`は2つの配列をつないで新しい配列にします。文字列の連結 (§8)と同じ扱いです。スプレッドの方が一般的な形で、`Tuple` / `Set` を対象にでき、リテラルの途中にも差し込めます。一方`+`は素直な2配列の 場合に読みやすく、連鎖もできます。 a + b # 新しい配列。a・bは変わらない a += b # aを連結結果に再束縛する [...a, ...b] # a + bと同じ結果 コピーはshallowで、要素は`slice`と同様に元の配列と共有されます。新し い配列を作らずその場で追加するには`a.extend(b)`(§18.2)を使います。 ### アクセスと変更 arr[i] # インデックスアクセス、負の値は末尾から arr[-1] # 末尾要素 arr[i] = v # 設定。既存要素のみ(自動拡張なし) arr.size() # 要素数(Long) arr.push(x) # 末尾追加、nilを返す `arr[i]`の範囲外アクセスは`index out of range at L:C`を投げます。 ### スライス range添字(`seq[a..b]`, `seq[a..=b]`)は単一要素でなく部分列を返し ます。`..`は終端排他、`..=`は終端包含。両端とも負値可(末尾から数え る)。両端は**省略可**で、開始省略は`0`、終端省略は長さになります: `xs[2..]`は先頭2つを落とし、`xs[..3]`は先頭3つ、`xs[..]`は全体コピー。 範囲外の端は**クランプ**され、開始が終端を超えると空を返すので、 スライスは境界で例外を投げません。端はインデックスなので`Long`で なければならず、`Float`の端を持つrangeでスライスすると`TypeError`です。 Arrayは**浅いコピー**を返します(背骨は元と独立、要素は共有)。参照 セマンティクスの配列をビューでスライスすると別名化で驚くため、コピー が安全な既定です。巨大配列のzero-copy遅延ウィンドウが要るときは イテレータ(`xs.iter().skip(a)`)を使います。Stringは**バイト単位の ビュー**(Go流バイトindex。コードポイント途中で切れても生バイトを 保持)。TupleはTupleを返します。 rangeは**第一級の値**です(`let r = 1..3`)。変数に入れ、関数に渡し、 後で添字に使えます(`xs[r]`)。両端が`Long`の境界付きrangeは反復可能 (`for i in 1..4`)。境界の無いrange(`2..`)は反復終端が無く、反復すると例外。 ```culebra let xs = [10, 20, 30, 40, 50] inspect(xs[1..3]) # => [20, 30] inspect(xs[1..=3]) # => [20, 30, 40] inspect(xs[-3..-1]) # => [30, 40] inspect(xs[2..]) # => [30, 40, 50] inspect(xs[..3]) # => [10, 20, 30] ``` ```culebra let xs = [10, 20, 30, 40, 50] inspect(xs[1..100]) # => [20, 30, 40, 50] inspect(xs[3..1]) # => [] let r = 1..3 inspect(xs[r]) # => [20, 30] ``` ```culebra # 浅いコピー: 元の配列を書き換えてもスライスには影響しない。 mut xs = [10, 20, 30] let s = xs[0..2] xs[0] = 99 inspect(s) # => [10, 20] ``` ```culebra inspect("hello"[1..3]) # => 'el' inspect("hello"[1..=3]) # => 'ell' ``` ### 等価性・順序 配列は**値(構造的)**で比較されます: `[1, 2] == [1, 2]`は真で、入れ子 要素も再帰的に比較。Tuple / Set / Objectも値等価で、 参照同一性は`Function`と`Tensor`のみ。(値等価でもArrayはハッシュ 不可のまま=Object/Setのキーには使えない。)順序演算子は配列に未定義。 --- ## 10. オブジェクト ### 構築 {} # 空オブジェクト {name: 'alice', age: 30} # 2プロパティ {mut counter: 0, name: 'x'} # 明示的なmut {name, age} # 短縮形 — {name: name, age: age} と等価 ソース上のプロパティ順は等価性・アクセスに影響しませんが、表示と 反復には挿入順が保持されます — キーは最初に書かれた順で返ります。 短縮形`{x}`は`{x: x}`と等価で、識別子をキーと値の両方として再利用し、 現在のスコープから`x`を参照します。`mut`も使え(`{mut n}`)、 プロパティを可変にしつつ値は束縛`n`から取ります。 **スプレッド / マージ。** `...obj`メンバは別Objectのエントリを取り込む。 後のキーが勝つので、設定の上書きにも使える: {...defaults, ...overrides} # マージ(overrides優先) {...base, port: 8080} # 1フィールド上書き 取り込んだエントリは可変。非Objectのスプレッドは`TypeError`。 ### 表示規約 デフォルトのフォーマッター(`__str__`を持たないObjectに使われる)は、 その値を作ったクラスの名前を先頭に付けます。`x`, `y`フィールドを持つ `class`構文のインスタンスは`Point {mut x: 3, mut y: 4}`と表示されます。 普通のObjectはクラスを持たないので`{...}`と表示され、`class`という名前の フィールドがあってもそれは普通のプロパティとして一覧に並びます。 ### 列挙 `keys()`・`size()`・`to_string()`・`for k, v in obj`・スプレッド (`{...obj}`)・`JSON.stringify`はすべて同じ集合を、挿入順で列挙します — そのオブジェクト自身のエントリです。`class`構文のインスタンスでは 宣言フィールド(宣言順)→ `self.x = y`で設定されたフィールド、の順に なります。クラス名もメソッドも自身のエントリではなく、全インスタンスが 委譲する1つのクラス毎metaの上にある(§25参照)ため、どちらも列挙には 現れません。したがってインスタンスのスプレッドはフィールドだけを普通の Objectに写し、その結果は何のインスタンスでもありません: class P { new() { self.a = 1 } m() { 2 } } let p = P() p.keys() # ['class', 'a'] p.size() # 2 p.to_string() # 'P {mut a: 1}'(classは接頭辞に持ち上げ) JSON.stringify(p) # '{"class":"P","a":1}' {...p}.keys() # ['class', 'a'] — 普通のObject、メソッド無し `has(key)`だけは例外で、*lookup*の問いに答えるためクラスメソッドも 見えます。ただしdictの組み込みメソッドは見えません: p.has('m') # true — クラスメソッド p.has('size') # false — dictの組み込み、エントリではない データアクセサは自身のエントリのみを見ます: `p.get('m', fallback)`は fallbackを返し、`p['m']`は`KeyError`を投げます。メソッド名への代入 (`p.m = 1`)はメソッドを隠す自身のフィールドを作ります。 ### アクセスと変更 obj.key # 読み取り(存在しなければnil) obj.key = v # 設定(存在しなければ作成、既存にはmut必要) obj[k] # 非Stringキーでの読み取り(下記参照) obj.size() # プロパティ数(Long)、非Stringキーも数える obj.has(key) # Bool — String / Long / Float / Bool / Nil / Tupleを受け付け obj.keys() # 挿入順(interleave)のキー配列 obj.remove(key) # `key`の項目を削除(任意のハッシュ可能キー)。なければno-op `mut`指定なしで既存プロパティに代入すると `immutable property 'key' at L:C`になります。 ドット形式の名前(`obj.key`)は識別子(`[A-Za-z_][A-Za-z0-9_]*`)で、 shape経由の高速パスを通ります。識別子でないキーは下記の添字アクセス パスに入ります。 キーには**文字列リテラル**(`'...'`・backtick・補間なしの`"..."`)も使え、 識別子でないキー(ハイフンや空白を含む)を書く手段になります: let headers = {"User-Agent": "curl", "X-Trace-Id": id} headers["User-Agent"] # 識別子でないキーは [ ] で読む キーはコンパイル時定数でなければならず、**補間入り** `"...{x}..."`はSyntaxError。 動的キーは`o[k] = v`で作ります。 ### 非 String キー Stringキー(`{name: 'alice'}`)に加え、Objectリテラルは`Long`, `Float`, `Bool`, `Nil`, `Tuple`をキーとして受け付けます。 let grid = {(0, 0): 'origin', (1, 0): 'east'} let by_id = {1: 'one', 2: 'two', 3.14: 'pi'} let by_nil = {nil: 'unknown'} 非Stringキーはサイドカーマップに入り、`obj[k]`で読みます。 grid[(0, 0)] # 'origin' by_id[1] # 'one' ハッシュできないキー(Array、Set、Function、`hash()`を持たないObject)は、 キーを引く所(`obj[k]`、`has`、`get`、`get_or_put`、`remove`)ではどこでも、 Objectが何を持っていても`TypeError`になります。クラスの`__index__`は、 読み出しのときには引き続きそうしたキーを受け取ります。 キーの同一性は**型厳密**で、`==`演算子とは別の関係です。 型が違うキーは決して同一キーになりません。よって`1 == 1.0` が`==`演算子では真でも、`1` / `1.0` / `true`は**別々のキー**です。 逆向きの違いもあります。`NaN == NaN`は偽ですが、`NaN`は1つのキーです (同じ`NaN`への2度目の書き込みは上書きになり、読み出しも削除もできます)。 `0.0`と`-0.0`は、`==`で等しいのと同じく1つのキーです。 Setの要素と`distinct()`も同じ規則に従います。 リテラル内で **同じキーを繰り返した**場合は後勝ち(`{1: 'a', 1: 'b'}`は`{1: 'b'}`。 Stringキーを含め、リテラルの重複キーは常にlast-wins)。 Stringキーと非Stringキーは単一の挿入順を共有するので、`to_string` 出力では混在キーのObjectが実際の書き込み順でinterleaveされます。 ### 添字代入 `obj[k] = v`でハッシュ可能な任意のキー — `Long`, `Float`, `Bool`, `Nil`, `Tuple`, enum variant, ランタイム`String` — を書き込めます: mut bag = {} let k = 'alpha' bag[k] = 1 # ランタイムStringキー bag[42] = 'long' bag[(1,2)] = 'tuple' inspect(bag[k]) # 1 inspect(bag[(1,2)]) # 'tuple' Stringキーはshapeベースの`obj.foo`パスと統合されます — どちらの 形式でも同じスロットに到達します: mut o = {} o['x'] = 1 inspect(o.x) # 1 o.y = 2 inspect(o['y']) # 2 既存スロットはどちらの書き込み形式でも`mut`フラグを尊重し、 immutableなスロットへの`obj[k] = v`は`ImmutableError`を投げます。 非Stringキーはサイドカーマップに入ります。複合形式(`obj[k] += v`)は スロットをその場で更新し、キーが既に存在し(無ければ`KeyError`)かつ スロットがmutableであることを要求します。 この書き方で作った Object は辞書であり、辞書として振る舞います。 キーの型を問わず、挿入・読み出し・削除は要素数が増えても O(1) の ままです。Object リテラルやクラスインスタンスが持つ固定スロット 配置は別の表現で、キーが増えてその配置が割に合わなくなると Object はそこから離れます。表現が変わるだけで、振る舞いは 変わりません。 ### メソッドと UFCS メソッド呼出`receiver.name(args)`は次の順序で解決します: 0. `receiver`が組み込みnamespace (`IO`・`Math`等) の場合、解決 されるのはそのメンバーだけです。後述の [namespaceは閉じている](#namespaceは閉じている)を参照。この レシーバではUFCSは働きません。 1. `receiver`が`name`という名前のプロパティ / 組み込みメソッドを 持てばそれを呼ぶ。`Object` / `Array`ではユーザ定義プロパティが 組み込みを上書きできる。`String`は専用のメソッドテーブルのみ。 レシーバのクラスが適合するtraitのデフォルト実装 (§14) もここでの 「そのメソッド」に含まれるので、UFCSに落ちずにこの段階で解決 されます。解決された値が`Function`であれば、呼出の間`self`は `receiver`に束縛されます。 2. 存在しない場合、外側スコープに`name`という名前の自由関数が あり、**その第一引数が`receiver`を受け取れる**なら、`receiver`を 第一引数として`name(receiver, args)`を呼ぶ。これが **Uniform Function Call Syntax (UFCS)** で、D / Nimの流儀に 倣っています。後述の[第一引数が決める](#第一引数が決める)を参照。 3. どちらも該当しなければプロパティ参照は`nil`を返し、続く呼出は 型エラーで失敗します。 ```culebra o = {n: 10, add: fn (x) { x + self.n }} # レシーバ呼び出しは self を束縛するので `add` は o.n を読む: inspect(o.add(5)) # => 15 double = fn (x) { x * 2 } 42.double() # UFCS → double(42) → 84 word_count = fn (s) { s.split(' ').size() } 'hello world'.word_count() # UFCS → 2 # 既存メソッドが常に優先 — ユーザ定義の `size` は Array / Object / # String の組み込み `size` によって隠される。 size = fn (x) { 99 } [1, 2, 3].size() # 3 (builtin)、99 ではない ``` UFCSは **DOTの直後に引数リストがある場合のみ**適用されます。裸の プロパティ参照(`x.name`だけ)ではUFCSは起動しません。UFCS呼出 はレシーバを`self`に**バインドしません** — 意味的には自由関数 呼出で、レシーバは単に第一引数の位置にあります。 組み込みテーブルの外でディスパッチする名前が2つありますが、順序は 同じです。クラスインスタンスに合成される`parameters()` (§10) は step 1のプロパティなので、グローバルな`parameters`が横取りする ことはありません。明示drop`x.drop()` (§17) は逆にstep 2の**下**に います —— 自前の`drop`を持たないレシーバは、それを受け取れる スコープ内の自由関数`drop`に渡り、名前を自分で解決するレシーバ (自前の`drop`を持つハンドル、あるいはそういう候補がそもそも無い 場合)だけがat-most-onceガードに届きます。クラスオブジェクト・enum・名前空間は決して届きません: `C.drop()`はstaticな`drop`の普通の呼び出しです。 #### 第一引数が決める 自由関数がそのレシーバのUFCS候補になるのは、第一引数がレシーバを 受け取れるときだけです。型注釈の無い第一引数は何でも受け取ります。 注釈があれば、普通の引数として渡したときに通るもの (§14) を 受け取ります —— union、レシーバのクラスが適合するtrait、3段階の どのレベルのenumでも同じです。先頭に位置引数を持たない関数 (引数なし、または`*args`だけ)は、失敗しうる宣言が無いので候補の ままです。multimethodでは、第一引数がレシーバを受け取れるoverloadが 1つあれば足ります。どのoverloadが走るかは、その後の全引数に対する 通常のディスパッチ (§20) が決めます。 レシーバを受け取れない関数は、その呼出にとっては存在しないのと 同じです。だからメソッド名の書き間違いが、無関係な関数の引数に ついての苦情に化けることはありません: ```culebra # doctest: skip let g = fn (p: Long) {} class C { new() {} f() { self.g(0) } # C に `g` は無く、自由関数の `g` は Long を取る } C().f() # TypeError: expected Function, got Nil # ('g' is not a method of C) 7.g() # UFCS → g(7) ``` この判定はレシーバだけを見て、どの引数よりも先に行われます。残りの 引数はこれまでどおり、呼ばれた側のバインダが検査します。 #### 組み込みメソッドは位置引数でバインドする 組み込みメソッド(`Array` / `String` / `Set` / `Tuple` / dict / iterator / `Tensor`)は引数を**位置で**バインドします。キーワード 引数として書ける名前は*キーワード専用*パラメータ —— `sorted`の `reverse:`のように、そもそも位置スロットを持たないもの —— だけ です。通常のパラメータ名をキーワードで指定するのは、そのパラメータ が実在しても`TypeError`であり、`**` splatは一切受け付けません: ```culebra # doctest: skip [3, 1, 2].sorted(reverse: true) # → [3, 2, 1] (キーワード専用) 'hello world'.truncate(8) # → 'hello...' 'hello world'.truncate(max: 8) # TypeError: built-in method 'truncate' # does not accept keyword arguments [3, 1].sorted(**{'reverse': true}) # 同じ TypeError ``` この規則は名前ではなく*呼ばれる実体*についてのものです。同名の ユーザ定義メソッドはただの関数なので、`doc.truncate(max: 8)`は 普通にキーワードをバインドします。また名前を**解決するレシーバ** についての話でもあります —— プロパティは引数がバインドされる前に 読まれるので、`[1, 2].take(n: 1)`(`Array`に`take`は無い)は キーワードのエラーではなく通常のメソッド解決失敗になります。 #### 引数リストはバインドより先に走り切る `x.m(a, b)`はプロパティを読み、**すべての**引数を左から右に評価し、 その後でバインドします。したがって引数の型違反も、レシーバが持って いないメソッドも、リスト全体が走り終えた*後で*報告されます: ```culebra # doctest: skip 'abc'.truncate(bad(), loud()) # 両方走ってから `max` の型エラー 'ab'.push(loud()) # loud() が走ってから "expected Function, got Nil # ('push' is not a method of String)" ``` 存在しないメンバーは`nil`として読めるので、レシーバが持たないメソッドは 呼び出しの時点で表に出ます。`nil`を呼んだときと同じ`TypeError`です。 括弧の部分はメソッド呼び出しが足すもので、読もうとした名前と、 レシーバの型(`type_of`の語彙)を示します。 これより早く失敗する唯一の場合がスカラーのレシーバです。`nil`・ `Bool`・`Long`・`Float`はそもそもメンバーを持てないので、 `(5).push(loud())`はプロパティ読みの時点で失敗し、`loud()`は走り ません。 #### namespaceは閉じている 組み込みnamespaceのメンバー集合は固定なので、知らないメンバーは 拡張点ではなくtypoか廃止されたAPIです。参照すると `AttributeError: namespace 'IO' has no member 'zzz'`になり、UFCS が代わりに立つこともありません —— 同名の自由関数`zzz`では埋まり ません: ```culebra # doctest: skip zzz = fn (ns, v) { v } IO.zzz(9) # AttributeError。zzz(IO, 9) ではない Math.to_string() # AttributeError。to_string(Math) ではない ``` 例外はdictの組み込みで、namespaceも`Object`である以上 `IO.keys()`・`Sys.has('script')`など`Object`テーブルの分は今も 答えます。素のobjectは影響を受けません —— `{a: 1}.zzz(9)`は従来 どおりUFCSです。 書き込みも同じ固定集合ルールに従い、`Ns.zzz = v`と`Ns.zzz ??= v`は 幽霊メンバーを生やす代わりに同じ`AttributeError`を投げます (`Ns.zzz += v`は後述の一般的なcompound代入ルールにより既に missing propertyでエラーでした)。**既存**メンバーへの書き込みは そのslotの通常の代入ルールに従います —— stdlib名前空間のメンバーの 多くはimmutableとして登録されているため、結果は無言の上書きでなく `ImmutableError`になるのが通例です: ```culebra # doctest: skip Math.zzz = 1 # AttributeError: namespace 'Math' has no member 'zzz' Math.pi = 3.0 # ImmutableError: immutable property 'pi' ``` **組み込み**メソッドの裸読み(括弧なしの`let m = [1, 2].map`)は 第一級値になりません — `TypeError: built-in method 'map' cannot be used as a value (call it, or wrap it in a lambda)`を投げます。この rejectが働くのは**そのレシーバ自身の組み込みテーブルに名前がある 場合だけ**で、レシーバが単に持っていない組み込み「名」のプロパティ は他のmissと同じく`nil`を返します(`{a: 1}.map`や、`join`が インスタンスメソッドであるクラスオブジェクトへの`C.join`など)。 ユーザ定義メソッドの裸読みは束縛済みの第一級値になります(後述の `self`を参照)。 `self`は2段階で解決されます。レシーバを伴う呼出はその呼出の間 `self`を束縛し、この動的束縛が常に優先されます。レシーバなしで 到達した本体 —— 素の`f(x)`、UFCS呼出、`map`などの組込みに渡した 関数、`defer`ブロック —— では、字句的に外側の関数の`self`へと 必要なだけ外向きにフォールバックします。メソッド内で定義した ネストの`fn`・ラムダ・返したクロージャは、したがってメソッドの レシーバを見続けます。レシーバも外側の`self`もどちらも存在しない 場合に限り、他の未束縛の名前と同じく`NameError: undefined variable 'self'`になります。送出されるのは本体に入った時点では なく、`self`を読んだその箇所です。 関数値のプロパティを**値として**読む(`let m = o.f`、呼出括弧 なし)と、`o`を`self`として抱えた新しいラッパ = **束縛メソッド** が返ります。束縛は恒久的で、ラッパを別のオブジェクトに付けて メソッドとして呼んでも元のレシーバで実行されます。また読み取りの たびに新しいラッパが作られるので`o.f == o.f`は`false`です。 これはオブジェクトリテラルのプロパティ・クラスメソッド・static・ コンストラクタ(`let mk = C.new`)に一様に適用されます。本体内の 暗黙の再帰ハンドル`fn`は「その呼出が経由した値」を指すため、 メソッドとして呼ばれたフレームの`fn`は束縛ラッパです —— `fn(...)`での再帰や`fn`の返却は現在のレシーバを保持します。 **JIT**: UFCSは`--jit`でもサポートされます。解決は実行時に行われ、 レシーバが同名プロパティを持っていればメソッド経路が勝ち、そうで なければ自由関数として解決し、その第一引数がレシーバを受け取れる ならレシーバを第一引数として呼び出します。 ### `class` 構文 クロージャベースのオブジェクトが正統なOOイディオムですが、軽量な 代替として`class`形式があり、同じランタイム表現に脱糖されます: class Car { new(mpr) { self.miles = 0; self.mpr = mpr } run(n) { self.miles = self.miles + self.mpr * n } total() { self.miles } } c = Car.new(5); c.run(3); inspect(c.total()) inspect(type_of(c)) # 'Car' — それを作ったクラス セマンティクス: * 宣言は`Car`を`new`プロパティのみを持つ`Object`に束縛します。 * `Car.new(...)`はコンストラクタが設定したフィールドを持つ新しい `Object`を返します。コンストラクタ本体の実行中は`self`がその新 オブジェクトに束縛されます。クラス名もメソッドもインスタンスには コピーされません — どちらも全インスタンスが委譲する1つのクラス毎 metaの上にあり、`c.run`はそこを経由して解決され、`type_of(c)`は そこから名前を読み、`c.keys()`はフィールドのみを返します (*列挙*参照)。Objectはフィールドを書くことでクラスを名乗ることは できません — `type_of`・`match`・クラス型の引数・`==`はどれもmetaに 問い合わせます。 * クラスを呼ぶとコンストラクタが呼ばれます: `Car(5)`は`Car.new(5)`で、 キーワード引数もそのまま渡ります。この規則は値の書かれ方ではなく 値が持つ`new`に対して定義されているので、ライブラリ側の コンストラクタにも同じく及びます — `Vector2(1, 2)`、 `Scene.Image(4, 4)`、`Channel()` — docsに載っている`.new(...)`で この形が使えないものはありません。及ばないのは`new`キーを持つ ただの`Object`で、こちらは呼び出し不可のままです。fieldに名前を 付けただけでdictを呼び出し可能にはできません。 * コンストラクタ/メソッド内で`self.x = y`で作られるフィールドは **デフォルトで可変**です(通常の`o.x = y`は不変プロパティを作る のと異なります)。メソッドが頻繁にインスタンス状態を書き換える クラスの慣習に合わせています。 * `new`メソッドは省略可能で、省略時は無引数・フィールドを1つも 持たないインスタンスを返します。 * コンストラクタ本体内の`self`は不変です。`self = newObj`への 代入は`ImmutableError`を投げます。コンストラクタは常に最初に 確保されたインスタンスを返し、 明示的な`return value`は`value`を捨てます。識別子差し替えが 必要なfactoryは`static`メソッド(下記)またはクラス外の関数 として書いてください。 * メソッドの引数と戻り値には`fn`と同じ型注釈を書け、検査のされ方も 同じです(*戻り値*を参照)。`area() -> Float { ... }`は本体の末尾で 抜けるときも`return`で抜けるときも戻り値を検査し、 `c.area.return_type`は`'Float'`を返します。`static`と`get`の メンバにも書けます。例外は`new`で、結果は常にインスタンスなので `new(...) -> T`は`SyntaxError`です。 * `static`修飾子を付けたメソッドはクラスオブジェクト自身に登録 され(インスタンスには付かない)、factory・定数的なヘルパ・ 名前空間としての用途に使えます。`Shape.create(...)`形式で 通常のプロパティ参照と同じく解決され、receiverはクラスオブジェクト そのものなのでstatic本体内で`self`は使えません: class Shape { new () { self.kind = 'unknown' } area () { 0 } static circle (r) { let s = Shape.new() s.kind = 'circle'; s.radius = r; s } static square (side) { let s = Shape.new() s.kind = 'square'; s.side = side; s } } let c = Shape.circle(4) # 静的factory let s = Shape.square(3) inspect(c.kind) # 'circle' staticメソッドはインスタンス経由では参照できません(`c.circle(...)` はinstanceに`circle`プロパティが無いため`TypeError`を投げる): staticはクラス経由でのみ到達でき、インスタンス経由では届きません。 * `static NAME = EXPRESSION`でクラスレベルの不変定数(static field)を 宣言できます。class宣言時にeagerに評価され、staticメソッドと同じ クラスオブジェクトのプロパティとして登録されます: class Circle { new (r) { self.radius = r } static PI = 3.14 static MAX = 100 area () { self.radius * self.radius * Circle.PI } } inspect(Circle.PI) # 3.14 inspect(Circle.MAX) # 100 値の式は任意(`static SUM = [1,2,3].sum()`等)で、class宣言時の 外側スコープで評価されます。値の式の中の宣言はその式だけのもので、 式の外へは出ません(`t = 0; class K { static s = (let t = 5) + 1 }` の後も`t`は`0`)。この時点でclass自身の名前はまだ束縛されていません。 値がそれを読むと、直接でも呼び出した関数の中でも (`static T = Circle.PI * 2`)、宣言がまだ走っていない名前を読んだ ときと同じ`NameError: undefined variable 'Circle'`になります。 staticに入れた関数(`static unit = || Circle.new(1)`)は後で走るので、 classを名指せます。staticメソッドと同じく、static fieldは immutable(`Circle.PI = 2`は`ImmutableError`を投げる)かつ インスタンス経由では参照できません。 `static NAME: Type = EXPRESSION`は値の型を注釈し、型つきの`let`と 同じくclass宣言時に1度検査されます(`static PI: Long = 3.14`は `TypeError: type error: static field 'PI' expects Long`を投げる)。 static fieldは必ず値を持つので、`static NAME: Type`だけを書くと ロード時の`SyntaxError`になります。 * `NAME = EXPRESSION` / `NAME: Type` / `NAME: Type = EXPRESSION`は **インスタンスfield** を宣言します — `new`本体の前に初期化される per-instanceの可変状態です。型注釈は言語の他の場所と同様optional: class Player { score = 0 name: String tags = [] best = self.score + 10 new (name) { self.name = name } } let p = Player.new('rocci') inspect(p.score) # 0 inspect(p.best) # 10 p.tags.push('bird') # Arrayは各インスタンス独立 初期化子の意味論: - **インスタンス毎**: 初期化子式は`C.new(...)`の呼び出しごとに 評価されます — `tags: Array = []`は各インスタンスに独立した Arrayを与えます — インスタンス間で共有されるものはありません。 - **宣言順・`self`参照可**: 初期化子は自分より上で宣言されたfield を読め、メソッドも呼べます(上の`best`)。*下で* 宣言されたfield を読むと`nil` — 宣言順に意味があります。 - **引数バインド後・`new`本体の前**: 初期化子はバインド完了後に 走ります — ctor呼び出しのarity/型/デフォルトのエラーが先に発火し、 field副作用は起きません。`new`本体からは全宣言fieldが見えます。 コンストラクタのパラメータは初期化子からは*見えません*(初期化子が 閉じ込めるのはclassの定義スコープであって、コンストラクタ呼び出し ではありません)。ctor引数をfieldに入れるには本体で`self.x = a`と 明示するか、field parameter(`new(.x)`、下記)を使います。 - **初期化子はそれぞれ独立したスコープ**: 初期化子の中の宣言 (`area = (let side = 3) * side`)はその式だけのもので、次のfieldの 初期化子からは見えません。 - 初期化子なしのtyped field(上の`name: String`)は型のゼロ値に なります: `0` / `0.0` / `''` / `false`。参照型(`Array`, `Object` 等)は`nil`です。untyped形は常に初期化子を伴います(ゼロ値を 推論する型がないため)。 - 宣言fieldは、コンストラクタ内の`self.x = y`で作ったfieldと 全く同じ可変インスタンス状態です。例外は`@value`クラス(§21)で、 そのインスタンスは`new`が返った時点で凍結します。 宣言型は**書き込みのたびに検査されます**。パラメータや`let x: T`と 同じruntime-checkモデル(§14)で、そのfieldは自分の型の値だけを受け 取ります。**スカラ**(`Float`・`Long`・`Bool`、および§21の固定幅の綴り `Float32`・`Int32`・`Byte`…。いずれもこの3つのどれかを表します)は 自分の値だけを受け取ります。それ以外の注釈(クラス、enum、trait、 union、オプショナル`T?`、`Array`・`Object`・`Function`)はパラメータと 同じ規則で検査しますが、`nil`は常に通ります。`nil`はそうしたfieldの 上のゼロ値なので、`Car`と宣言したfieldは`Car?`と同じく`nil`を受け取り ます。検査は値がfieldに届く経路すべてを覆います: 宣言自身の初期化子、 リテラルの名前、計算したキー、コンストラクタ自身の`self.x`、nativeの builder。これによりfieldの型を読む価値が生まれます — `Float`のfieldは `Float`を、`Car`のfieldは`Car`(か`nil`)を持つので、クラスが分かる コードは問い合わせずにfieldの型を知ることができます。検査されるfieldは `remove`できません(`ImmutableError`)。消えうるfieldは何も約束しない からです。 ```culebra class Car { speed: Float new(.speed) {} } class Garage { car: Car } let g = Garage() inspect(g.car) # => nil g.car = Car(1.5) inspect(g.car.speed) # => 1.5 g.car = 'a string' # !! field 'car' expects Car ``` **`String`は検査されません**。`String`を許す注釈(`String?`・ `String | Long`)、何でも許す注釈(`Any?`)、注釈のないfieldも同じ です。`String`には第二の綴り があります: `StringView`(§18.1)も文字列値で、`slice`・`split`・ `lines`・文字列に対する`for`が返すのはこちらです。したがって `name: String`のfieldは`StringView`を(そして何でも)受け取り、その読み は値に型を問い合わせます。 `@packable`クラス(§21)はこれに加えて、fieldの宣言型を固定バイト レイアウトの計算に読み、`@value`クラスは全fieldがスカラまたは別の `@value`クラスかの検査に読むため、どちらもfieldはtyped必須です (どちらのクラス内でも`x = 7`はSyntaxError)。`@packable`のfieldで型が スカラ以外のものはバイトレイアウトに属します。packed viewへの書き込みは それが検査しますが、通常のインスタンスへの書き込みは検査されません。 * `new(.x)`は **field parameter** を宣言します: `new`のパラメータ`x` であり、同時にその引数をfield `x`に格納するので、本体にその代入を 書きません。`.x: T`と`.x = v`は他のパラメータと同じ形で型と デフォルトを与えます。`.x?`は省略可能にし、型とデフォルトの両方を fieldの宣言から取ります: class Cartridge { prg_rom: Array chr_rom: Array chr_ram = self.chr_rom.empty() ? repeat(8192, 0) : [] mapper_id = 0 has_battery = false new(.prg_rom, .chr_rom, *, .mapper_id?, .has_battery?) {} } let c = Cartridge([0x4E, 0x45, 0x53], [], mapper_id: 4) inspect(c.mapper_id) # 4 inspect(c.has_battery) # false(宣言のデフォルト) inspect(c.chr_ram.size()) # 8192(初期化子がchr_romを読んだ) - `x`という名前の普通のパラメータです: キーワードでは`x`として渡し、 arity/型のエラーでも`x`と報告され、後続のデフォルトや本体からも `x`として読めます。位置/キーワード渡し、`*`、並び順の規則(必須 パラメータは省略可能なものの後に置けない)はそのまま適用され、 `.x?`は省略可能側に数えます。 - 引数は宣言順のそのfieldの位置で、引数のバインド後・`new`本体の前に 格納されます。したがってそのfieldより*下で* 宣言された初期化子は 引数が格納した値を読め(上の`chr_ram`)、*上で* 宣言されたものは 従来どおり`nil`を読みます。格納は他のすべての格納と同じ検査付きの 書き込みで、宣言型はそれが受け取らない値を拒みます。 - 呼び出し側が`.x?`を省略すると、宣言自身の値(初期化子、または型の ゼロ値)が入り、本体のパラメータ`x`もその値として読めます。`nil`を 渡すのは値を渡すことであって、省略ではありません。 - 宣言済みのfieldを名指す場合: パラメータが`.x?`でない限り、宣言は 初期化子を持てません(さもないとパラメータが常に値を供給し、 初期化子が走ることがない)。パラメータ側の型は宣言の型と一致 しなければなりません。`.x?`は自分の型もデフォルトも持ちません。 - 宣言されていないfieldを名指す場合: `.x`と`.x: T`はそのfieldを **宣言** します。クラスのfield一覧では最初の`new`の位置に入り (各オーバーロードが宣言するfieldはすべてそこに、名指した順で 並びます)、型は書いたとおり(型なしなら`nil`で始まり、ゼロ値も 検査もありません)。複数の`new`オーバーロードが同じfieldをこの形で 宣言してもよく、型の書き方は一致が必要です(型なしの`.x`も含めて)。 名前は本体の宣言と同じ一意性の規則に従います。`@value`や `@packable`クラスではそのfieldもfield規則に従います(型が必須)。 `.x?`にはデフォルトを取る宣言がないのでSyntaxErrorです。 - この形を受け付けるのは`new`のパラメータリストだけで、他の場所では SyntaxErrorです。 * `get NAME () { ... }`は **getter**(引数なしメソッド)を宣言します。 括弧なしのプロパティ読み取りで呼び出されます: class Circle { new (r) { self.radius = r } get area () { self.radius * self.radius * 3.14 } } let c = Circle.new(4) inspect(c.area) # 50.24 — fieldのように読める inspect(c.area()) # 50.24 — 呼び出し表記も動く getterは他のメソッド同様`self`を読みますがpropertyとして振る舞う ので、fluent chainの括弧が消えます(`p.parent().name()`ではなく `p.parent.name`)。getterは純粋・全域・O(1) の導出(値の本質的な性質) に限り、I/Oや失敗しうる処理は通常メソッドに残します。こうして括弧の 不在が「副作用なし」を表します。getterは 引数を取れません(`get f (x)`は構文エラー)。`get`はcontextualで、 `get`という名のメンバー(`get () { ... }`)は通常メソッドのままです。 `obj.name`と`obj.name()`の両表記はVM・JIT・AOTで同一に発火します。 * 両エンジンともクラス宣言をコンパイルします。 インスタンス生成は軽いランタイム呼出: `new`自体は通常のJIT closureで、capturesにメソッドclosure群とユーザの`new`本体 closureを持ち、ランタイムヘルパーが新オブジェクトに結び付けます。 * `drop`等のwell-knownメソッドも通常のメソッドと同じ形で書けます (§17)。JITではメソッドはクラス共有メタオブジェクト経由のプロト 委譲で持たれますが、auto-dropルックアップはproto chainを辿るため `class C { drop() { ... } }`も期待通り発火し、`self`はインスタンス に束縛されます。 ### 演算子オーバーロード 任意の`Object`(`class`構文で作られたものでもプレーンなobject リテラルでも)は、特殊メソッドを定義することで算術・比較演算に 参加できます。ディスパッチは実行時: 左オペランドが対応する特殊 メソッドを持つ`Object`ならそのメソッドが右オペランドを唯一の 引数として呼ばれ、そうでなければ組み込みの数値パスが動きます。 **両エンジン**が同じ特殊メソッドプロトコルを通るので、 `Object`の算術はJITでも動きます。`class`構文で作ったインスタンス も、普通の`Object`にメソッドを持たせたものなので同様に扱えます。 | 演算子 | 特殊メソッド | 備考 | |---------------|--------------|----------------------------------------| | `a + b` | `__add__` | | | `a - b` | `__sub__` | | | `a * b` | `__mul__` | | | `a / b` | `__div__` | | | `a % b` | `__mod__` | | | `a ** b` | `__pow__` | | | `a @ b` | `__matmul__` | 行列積(PEP 465)。優先度は`*`と同じ。組み込み数値での意味なし — `__matmul__`未定義なら`type error`。 | | `-a` | `__neg__` | `a`に対する0引数メソッド | | `a == b` | `__eq__` | `!=`は否定から導出 | | `a < b` | `__lt__` | `>=`は否定から導出 | | `a <= b` | `__le__` | 未定義で`__lt__`があれば`a < b or a == b`として導出。`>`は否定から導出 | 例: class Vec { new(x, y) { self.x = x; self.y = y } __add__(r) { Vec.new(self.x + r.x, self.y + r.y) } __mul__(r) { Vec.new(self.x * r, self.y * r) } # スカラー __eq__(r) { self.x == r.x && self.y == r.y } } a = Vec.new(1, 2) b = Vec.new(3, 4) c = (a + b) * 2 # Vec(8, 12) **traitメソッドへのフォールバック。** クラスが明示的な`__eq__` / `__lt__` dunderを定義していない場合、比較演算子は`Eq` / `Comparable` traitのメソッドにフォールバックします。`==` / `!=`はクラスが書いた `eq(other)`を、`<` / `<=` / `>` / `>=`は`cmp(other)`(`Comparable`の 正準メソッド)を経由します。これにより手書きの`eq` / `cmp`クラスが、 `__eq__` / `__lt__`を書かずに`a == b` / `a < b`をそのまま使えます。 優先順位は **明示dunder > traitメソッド > 既定**(`==`は構造比較、 順序比較はどちらも無い`Object`で`type error`)。書かれた`eq`を経由する ことで、演算子は同じ`eq`を使う`Object` / `Set`のキー等価と一致します。 **派生した**`eq`は書かれたものではなく「インスタンスはfieldで一致する」 と言っているだけなので、そのclassの`==`は構造比較 — 既定 — になり、 enumのvariantと同じ扱いです(*methodの自動導出*を参照)。 この優先順位は演算子だけのものではなく、「等しい」を意味する比較の すべてに当てはまります: `==`で比べる2つのコンテナの要素どうし、そして `contains`と`index_of`による探索(`Array`・`Tuple`・イテレータ)です。 ```culebra class Account { new(id, note) { self.id = id; self.note = note } eq(o) { self.id == o.id } hash() { self.id } } let a = Account(7, 'as read from disk') let b = Account(7, 'as typed in') inspect(a == b) # => true inspect([a] == [b]) # => true inspect({owner: a} == {owner: b}) # => true inspect([a].contains(b)) # => true inspect([a].index_of(b)) # => 0 ``` **添字アクセス。** クラスインスタンスに`__index__(key)`と `__setindex__(key, value)`を定義すると、`obj[k]`と`obj[k] = v`が それらに委譲され、ユーザのコレクションラッパが組み込み同然に添字 できます。オブジェクトが直接プロパティとして持たないキーに対して 発火します(名前フィールド`obj["field"]`はフィールドを読みます)。 ユーザ型のスライス(`obj[a..b]`)は`__index__`に委譲されません。 キーはプログラムが書いた値のまま届きます: 部分文字列(`obj[s[0..2]]`)は `StringView`のままです。両backendで同一にディスパッチされます。 ```culebra class Grid { new() { self.d = [10, 20, 30] } __index__(i) { self.d[i] } __setindex__(i, v) { self.d[i] = v } } let g = Grid.new() g[1] = 99 inspect(g[1]) # => 99 ``` **呼び出し (`__call__`)。** クラスインスタンスに`__call__(*args)`を 定義すると`obj(args)`で呼び出せます — `__index__`の双子です。 `obj(x)`は`obj.__call__(x)`とまったく同じで、インスタンスが`self` に束縛されます。これにより層を合成する値の`model(x)`イディオム (`__call__`が下位層の`__call__`を呼ぶモデル)が書けます。添字フック と同様にクラスインスタンスでのみ発火するので、`__call__`キーを持つだけ のプレーンdictは通常の値のままです。両backendで同一にディスパッチ されます。 ```culebra class Adder { new(b) { self.b = b } __call__(x) { self.b + x } } let add3 = Adder.new(3) inspect(add3(10)) # => 13 inspect(add3.__call__(10)) # => 13 ``` `__call__`を持つクラスは`Function`型を構造的に満たすので、callable インスタンスは第一級の関数値です — 高階ビルトイン (`map` / `filter` / `reduce` / `sort_by`) に渡しても、`Function`注釈の任意のパラメータに 束縛しても、その`__call__`経由で呼ばれます。`Function`の型パターン(§13) にも一致します: ```culebra class Scale { new(k) { self.k = k } __call__(x) { x * self.k } } inspect([1, 2, 3].map(Scale.new(10))) # => [10, 20, 30] fn apply_twice(f: Function, x) { f(f(x)) } inspect(apply_twice(Scale.new(2), 5)) # => 20 let picked = match Scale.new(3) { f: Function => f(4), _ => 'not callable', } inspect(picked) # => 12 ``` `__call__`は呼び出しの全形式(位置引数・`*args`・キーワード引数)を 受け取れるので、`obj(x: 1)`は通常のメソッド呼び出し同様に`x`を `__call__`のパラメータに束縛します。 ### カスタム文字列表現 (`__str__`) `Object`に0引数の`__str__`メソッドを定義すると、値自身が `inspect` / `print` / `println`、文字列補間 (`"{x}"`)、`to_string(x)` に対して独自の表示形式を提供できます。戻り値は`String`である必要があり、 それ以外は型エラー。`__str__`を持たない`Object`は従来通りの デフォルト表示 (`{key: value, ...}`) になります。 class Matrix { new(r, c) { self.rows = r; self.cols = c } __str__() { "Matrix {self.rows}x{self.cols}" } } m = Matrix.new(2, 3) inspect(m) # Matrix 2x3 inspect("shape: {m}") # 'shape: Matrix 2x3' to_string(m) # 'Matrix 2x3' ディスパッチはトップレベルのみ: ネストされた構造内で`__str__`が 含まれる場合 (`inspect([m])`など)、外側フォーマッターのデフォルト 再帰走査は各要素に対して依然として`str()`を使うため、カスタム 形式は表示フックに直接渡されたオペランドにのみ現れます。より深い カスタマイズは具体的な必要性が出てから検討。 `__str__`本体では`inspect(self)`や`"{self}"`といった再帰呼出は **避けてください** — 組み込みの再帰ガードがないため、コールスタック を使い切るまでループします。最終文字列はプロパティを直接参照 (`self.x`) して組み立ててください。 ### 自動反射 可換演算子 (`+`, `*`, `==`) については、LHSが対応する特殊メソッドを 持たず (`Long`/`Float`など)、かつRHSが対応する特殊メソッドを持つ `Object`の場合、呼び出しが反射されます: `lhs op rhs` → `rhs.__op__(lhs)`。RHS側のメソッドはスカラーを引数として受け取り、 処理することが期待されます (典型的には引数の型を`match`で分岐): class Vec { ... __mul__(r) { match r { n: Long => Vec.new(self.x * n, self.y * n), # スカラー _ => Vec.new(self.x * r.x, self.y * r.y) # 要素ごと } } } a = 2 * Vec.new(3, 4) # 反射 → Vec.__mul__(2) → Vec(6, 8) 非可換演算子 (`-`, `/`, `%`, `**`, `@`, `<`, `<=`) は反射**しません** — `rhs.__op__(lhs)`は意味的に間違った結果になるためです。これらは LHSが対応する特殊メソッドを持つ`Object`である必要があります。 ### 自動合成される `parameters()` すべてのclassインスタンスは、引数なしの`parameters()`メソッドを 自動的に得ます。これは自身のフィールドから到達可能なすべてのclass インスタンス値をフラットな`Array`として返します。ウォーカーは `Array`の要素と、`class:`タグを持たない通常の`Object`の値を 再帰的に降下し、出会ったclassインスタンスはleafとして収集して それ以上は降下しません。スキップされるもの: `class:`タグそのもの、 および名前が`_`で始まるフィールド (private/キャッシュ用とみなす)。 反復順序は挿入順で、両バックエンドで揃います。 class Value { new(x) { self.x = x } } class GPT { new() { self.layers = range(2).map(|_| { W: range(4).map(|_| range(4).map(|_| Value.new(0.0)).collect() ).collect() }).collect() self.wte = range(8).map(|_| Value.new(0.0)).collect() } } let model = GPT.new() model.parameters().size() # 40 — すべてのValueがleafとして収集 classインスタンスはleaf扱いで、ウォーカーはそこで停止し、内部の フィールドへは降下しません。列挙にとって透過的にしたい中間グルーピン グには通常のObject dict (`class:`タグなし) を使い、実際にパラメータ として扱いたい葉だけをclassインスタンスにする、というのが基本パター ンです。 ユーザー定義の`parameters`メソッド (同名のプロパティ) があれば、 それが合成版より優先されます — フィールドwalkが意図する列挙と 異なる場合に有用です。両エンジンは同じウォーカーで合成しており、 出力順序も一致します。 --- ## タプル `Tuple`はイミュータブルで固定長のシーケンス — `Array`のハッシュ可能 版です。リテラル構文は括弧 + 少なくとも1つのカンマで書きます。 カンマなしだと括弧式と曖昧になるためです。 let pair = (3, 4) let single = (1,) # 末尾カンマで1要素tuple let triple = (1, 'two', 3.14) 要素アクセスは`Array`と同じ添字形式: pair[0] # 3 triple[-1] # 3.14 等価性は要素単位(Arrayも値等価なので入れ子も再帰比較): (1, 2) == (1, 2) # true ([1], 2) == ([1], 2) # true (内側Arrayも値等価) 順序は対ごとに決まります。`==`でない最初の対が、その対に対する`<`の 規則で結果を決め、そういう対が無ければ2つは等しい: ```culebra inspect((1, 2) < (1, 3)) # => true inspect((1, 9) < (2, 0)) # => true inspect((1, 2) <= (1, 2)) # => true ``` 順序を付けるのは結果を決める対だけなので、`<`が拒む対はそこでだけ 同じ`type error`になります。`(1, 'a') < (1, 2)`は例外 (`cannot compare String and Long`)、`(1, 'a') < (2, 0)`は`true`。 2つのTupleの長さは同じでなければなりません。短いほうを小さいとは しません: ```culebra (1, 2) < (1, 2, 3) # !! cannot compare Tuple and Tuple (lengths 2 and 3) ``` これでTupleは、複数の項目で一度に並べるためのキーになります: ```culebra let notes = [{bar: 2, beat: 0.0}, {bar: 1, beat: 2.5}, {bar: 1, beat: 1.0}] let ordered = notes.sorted_by(|n| (n.bar, n.beat)) inspect(ordered.map(|n| n.beat)) # => [1.0, 2.5, 0.0] inspect(notes.min_by(|n| (n.bar, n.beat)).beat) # => 1.0 ``` `reverse: true`は順序全体を逆にします。位置ごとに向きを変える指定は ありません。`Array`は順序を持ちません。 要素がハッシュ可能ならTupleもハッシュ可能なので、ObjectやSetの キーに使えます: let grid = {(0, 0): 'origin', (1, 0): 'east'} let visited = Set((0, 0), (1, 1)) # TupleのSet `for x in t { ... }`で要素を順に列挙できます。両バックエンドで 同一のセマンティクスです。 組み込みメソッド: | メソッド | 説明 | |-------------|---------------------------------------------------| | `size()` | 要素数 (`Long`) | | `empty()` | `Bool` — `size()`が0か | | `presence()`| 非空ならタプルをそのまま、空なら`nil` | | `contains(x)`| `Bool` — `x`が要素に含まれているか | | `to_array()`| 同じ要素を持つ新しい`Array` | | `iter()` | 要素を添字順に返すイテレータ | ### Destructuring `let (a, b) = t`でTupleを名前付きスロットに分解できます。要素数 が一致しない場合は`ValueError`を投げます。Tupleパターンは `match`のarmでも使え、入れ子もできます: let (x, y, z) = (10, 'hi', 3.14) let (p, (q, r)) = (1, (2, 3)) # 入れ子 let kind = match (1, 2) { (0, 0) => 'origin', (a, b) => 'other', } **並列代入 / swap。** `let`を外すと、新規宣言でなく**既存変数**へ 再代入する。RHSは束縛前に全評価されるので、swapやrotateに一時変数 が要らない: mut a = 1 mut b = 2 (a, b) = (b, a) # swap → a == 2, b == 1 (x, y, z) = (y, z, x) # rotate 各対象は単独で書いたときとまったく同じ振る舞いをする。既存の束縛は 再代入され(immutableなら`ImmutableError`)、可視でない名前は 素の`x = v`と同じく宣言される。配列・オブジェクトパターンも可: `[p, q, _] = xs`、`{g, h} = rec`。 パターンの照合はどの対象に書き込むより前に終わるので、形の不一致 (`ValueError`)では1つも書き込まれない — `[a, 5] = [9, 6]`は`a`に`9`を 入れてから`5`で失敗するのではなく、`a`を変えずに終わる。 `ImmutableError`はこれとは別で、その対象への書き込み時に送出される ため、左側の対象は既に新しい値を持つ。 **添字・プロパティを対象にする。** 対象には添字やプロパティのチェーンも 書ける。要素のswapにも一時変数が要らない: (p[i], p[j]) = (p[j], p[i]) # 2要素のswap (m[0][1], m[1][0]) = (m[1][0], m[0][1]) (self.front, self.back) = (self.back, self.front) (a, o.count) = f() # 名前とチェーンの混在も可 チェーン対象の書き込み規則は単独形(`p[i] = v`、`o.a = v`)と同一で、 クラスインスタンスの`__setindex__`も、immutableな**束縛**が保持する mutableなコンテナへの`xs[0] = v`が許されることも、そのまま成り立つ。 RHSは1度だけ評価され、`Array`または`Tuple`で、対象と同数の要素を 持たなければならない。それ以外は`ValueError`。要素はどの対象への書き込み よりも先に読まれるので、対象がRHS自身を指しても安全であり (`(p[1], p[0]) = p`)、要素数不一致のときは1つも書き込まれない。 その後、対象は左から順に書かれ、各対象の受け手と添字はその時点で評価 される。 対象はフラットな並びに限る。ネストしたパターンや`...rest`は上記の パターン形の機能であり、チェーンとは混ざらない。呼び出しは対象になれない (`f() = v`は`SyntaxError`)。 --- ## 集合 (Set) `Set`は挿入順を保持する一意ハッシュ可能値の集合です。`Set(...)`が引数から 作ります(引数はいくつでも可)。`Set.new(...)`も同じ呼び出しです。波括弧は 常にObjectのもので(名前だけを並べた`{a, b}`はObjectの短縮形)、Setに リテラルはありません。表示も、作る呼び出しの形になります: let s = Set(1, 2, 3) let one = Set(x) # 1要素。名前も値と同じに書ける let none = Set() # 空。`{}`は空Object let mixed = Set(1, 'two', 3.14, true) inspect(s) # => Set(1, 2, 3) `{1, 2}`はそう案内する`SyntaxError`です。すでにあるコレクションからは `to_set()`で作ります(`[x, y].to_set()`、`it.to_set()`)。 重複要素は構築時に潰されます。メンバーの同一性はObjectキーと同じ規則で、 数値としての値ではなく型を区別します。したがって`Set(1, 1.0, true)`は3要素 とも残ります(この3つがObjectキーとしても別々のキーになるのと同じ)。 ハッシュできない要素は、引く所(`contains`、`remove`)ではSetの要素数に 関係なく`TypeError`になります。 等価性は順序を無視: Set(1, 2, 3) == Set(3, 2, 1) # true 組み込みメソッド: | メソッド | 説明 | |--------------|----------------------------------------------------| | `size()` | 要素数 (`Long`) | | `empty()` | `Bool` — `size()`が0か | | `presence()` | 非空ならセットをそのまま、空なら`nil` | | `contains(x)`| `Bool` — `x`が含まれているか | | `union(b)` | このセットと`b`の合併を新しい`Set`で返す | | `intersect(b)`| 両方に含まれる要素を新しい`Set`で返す | | `diff(b)` | このセットにあって`b`にない要素を新しい`Set`で返す | | `sym_diff(b)`| どちらか一方にのみある要素を新しい`Set`で返す | | `subset(b)` | `Bool` — 自身の全要素が`b`に含まれているか | | `superset(b)`| `Bool` — `b`の全要素が自身に含まれているか | | `to_array()` | 挿入順のメンバーを持つ新しい`Array` | | `iter()` | 挿入順にメンバーを返すイテレータ | | `add(x)` | `x`を挿入。新規追加なら`true` | | `remove(x)` | `x`を削除。存在していれば`true` | 集合演算のメソッドは左オペランドの挿入順を残存要素に対して保ちます。 `.add(x)` / `.remove(x)`はuser定義Objectメソッド (例: `Calculator.add(1)`) と名前衝突しうるので、呼び出し点で実行時タグ振 り分け: Setレシーバはset primitiveへ、Objectレシーバはuser プロパティへ。VMとJIT(およびAOT)で動作。 Set操作はメソッド形のみ: `union`, `intersect`, `diff`, `sym_diff`。 `|` / `&` / `-` / `^`の演算子形は提供しません。lambdaパラメータの 閉じ記号`|`と曖昧になり (stateless PEGでは区別不能)、四つまとめ てメソッドに統一しました。演算子は基本的にmath型 (Long, Float, Tensor、`__add__`等でoverloadしたuser numeric class) 専用ですが、 唯一の例外として`+`は文字列 (§8) と配列 (§9) の連結も行います。 `Set`に`+`はありません。`to_array()`してからつなぐか`union`を使い ます。 --- ## 11. 関数とクロージャ ### 関数リテラル fn () { nil } fn (x) { x + 1 } fn (mut x) { x = x + 1; x } fn (a: Long, b: Long) -> Long { a + b } fn (name, greeting = 'hello') { "{greeting}, {name}" } 関数は第一級の値で、再利用可能なコードを定義する唯一の方法です。 リテラルは他の値と同じように束縛でき (`name = fn (...) { ... }`)、 宣言形の`fn name(...) { ... }`も使えます。 [ジェネレータ](#ジェネレータ-yield)になれるのも [多重ディスパッチ](#20-多重ディスパッチ)に参加できるのも宣言形だけで、 `fn.name`にソース上の名前が付くのも宣言形だけです。 ### ラムダ構文 `|x| ...` 高階関数に短い関数を渡すときの軽量構文: add = |x, y| x + y # 式ボディ sq = |x| x * x noop = || 42 # 引数なし scale = |x, y| { # ブロックボディ let factor = 10 x * y * factor } abs_v = |x| if x < 0 { -x } else { x } # if/while/for/match/try # は式なのでボディに置ける xs.map(|x| x * 2) # functorとして渡せる ボディは通常は単一式です。複数の文、途中の`let`、副作用が必要な 場合は、波括弧でブロックを書けます: clamp = |v, lo, hi| { mut x = v if x < lo { x = lo } if x > hi { x = hi } x } どちらの形も`fn (...) { ... }`と完全に同じセマンティクス: * 囲みスコープから変数をキャプチャする。 * パラメータの`mut` / 型注釈 / デフォルト値はすべて使える: `|mut x = 10| x + 1`は`fn (mut x = 10) x + 1`と同じ。 * `fn`による自己参照も名前付き関数と同じく使える。 * 戻り値型注釈は持てない(ラムダは短く保つため。注釈を付けたい関数は `fn`を使う)。 `|`区切りはパターンor(`match`アーム内のみ)や論理`||` (式の開始位置には現れない)とは競合しません。 ### パラメータ * `mut`付きパラメータは関数本体内で束縛が可変になります。なしでは 再代入で`immutable variable`になります。 * オプショナル型注釈は関数エントリで検査されます(§14)。 * パラメータは`name = expr`の形でデフォルト値を持てます。呼び出し側 が引数を省略した場合、デフォルト式は**呼び出しごとに**、関数の 定義環境に「パラメータより先にフレームが作る束縛」——レシーバ `self`、再帰ハンドル`fn`、先行パラメータ——を重ねた環境で評価され ます。したがって`fn (a, b = a + 1)`も`m(k = self.n)`も書けます。 後続のパラメータはこの環境に含まれず、デフォルト式の中のクロージャ からも見えません。トップレベルに`b`があれば、`fn (x = b, b = 5)`も `fn (x = || b, b = 5)`もその`b`を読みます。デフォルト式の中の宣言は その式だけのもので、本体からは見えません。 コンストラクタではインスタンスは既に存在しますがフィールド初期化子 は未実行なので、そこでの`self`のフィールドは`nil`です。 デフォルト付きパラメータは必須パラメータの後に置く必要があります。 * パラメータは**分解パターン**にできます — `fn ({x, y})`、`fn ([a, b])`、 `fn ((k, v))`、ラムダ形`|{a, b}|`。引数はエントリでパターンに マッチされ、名前が本体に束縛されます。形が合わなければ`ValueError`。 入れ子可(`fn ({user: {name}})`)、通常パラメータと混在可 (`fn (factor, {x, y})`)。 fn dist({x, y}) { x * x + y * y } dist({x: 3, y: 4}) # → 25 * 最後のパラメータを`*name`と書くと**位置可変長キャッチオール**になり、 通常パラメータを超えた位置引数をすべて`Array`に集めます(無ければ空配列)。 必ず最後のパラメータでなければなりません。 fn f(first, *rest) { [first, rest] } f(1, 2, 3) # → [1, [2, 3]] f(1) # → [1, []] `*args`を宣言すると**可変長ディスパッチ**にも参加します。正規パラメータ数 以上の任意の呼び出しにマッチしますが、固定arityのオーバーロードが同点では 常に勝ち、可変長候補同士では正規パラメータが多い方がよりspecificです。 fn h(x: Long) { "exact" } fn h(*xs) { "variadic" } h(1) # → 'exact' (固定arityが勝つ) h(1, 2) # → 'variadic' ### キーワード引数と `**` splat 呼び出し側で引数を名前指定(`name: value`)で渡したり、Objectを kwargsとして展開(`**obj`)できます: let f = fn (x, y = 10, z = 100) { x + y + z } f(1, 2, 3) # 位置引数 f(1, z: 5) # kwargs(yはdefault) f(z: 3, y: 2, x: 1) # 順不同 let opts = {y: 7, z: 8} f(1, **opts) # Objectをkwargsに展開 f(1, **opts, z: 100) # 明示kwargがsplatより優先 f(1, **{y: 2}, **{y: 5}) # 複数splat: 後勝ち `**rest`キャッチオール: 宣言したパラメータに吸収されなかった kwargsをObjectとして受け取ります。位置はパラメータリストの末尾 である必要があります。 let route = fn (path, **opts) { route_internal(path, opts.method, opts.headers, opts.body) } route('/users', method: 'GET') # opts = {method: 'GET'} route('/users', method: 'POST', body: '…') # opts = {method, body} キーワード専用パラメータ: パラメータリスト中の単独の`*`以降の パラメータは名前付きでのみ渡せます。 let g = fn (x, *, y, z = 10) { x + y + z } g(1, y: 2) # 13 g(1, y: 2, z: 3) # 6 g(1, 2) # TypeError: takes 1 positional argument but 2 # given ('y' is keyword-only: write y: ...) g(1) # ArityError: missing required argument 'y' ルール: * 位置引数はkwarg/splatより前に置きます。逆順(`f(x: 1, 2)`)は `SyntaxError`。 * 同じ名前を明示kwargで2回書くことはできません。 * 同じ名前を位置引数とkwargの両方で渡すこともできません。 * 未知のkwarg名は`TypeError`。ただし呼び出し先が`**rest` キャッチオールを宣言していれば、そこに吸収されます。 * `**` splatの右辺は`String`キーのみを持つ`Object`でなければ なりません。 * デフォルト値は呼び出しごとに再評価されます。可変なデフォルト (`fn f(xs = [])`)は毎回新しい値になり、呼び出しをまたいで 溜まっていくことはありません。 デフォルト値のあるパラメータは、キーワードで飛ばせます。 `(x, y = 10, z = 20)`に`f(1, z: 3)`と渡すと、`y`はデフォルト値に なります。 **標準ライブラリの関数。** 名前空間の関数や組み込みのグローバル関数 (`Math.clamp`、`JSON.stringify`、`Canvas.rect`、`to_string`)も、 同じ規則でキーワード引数と`**` splatを受け付けます。使える名前は リファレンスの見出しに書かれた引数名で、必須の引数も名前で渡せます: Math.clamp(x: 5, lo: 0, hi: 3) # → 3 Canvas.rect(**{x: 1, y: 1, w: 6, h: 6, color: c}) Math.clamp(x: 5, lo: 0) # ArityError: missing required argument 'hi' `*args`で引数をまとめて受け取る関数(`Math.max`)には名前の付いた 引数がないので、キーワード引数は`TypeError`になります。組み込みの 値型のメソッドは位置で受け取ります(「組み込みメソッドは位置引数で バインドする」を参照)。 コンパイル時エラー(例: `positional argument follows keyword argument`)はどのエンジンでもコンパイル段階で検出され、 `try/catch`ではキャッチできません。 ### 戻り値 * 本体はブロック。ブロックの最後の式が関数の戻り値になります。 * `return expr`で早期復帰、`return`(式なし)は`nil`を返します。 * 戻り値型`-> T`が宣言されている場合、自然な落ち抜けでも明示的な `return`でも、関数を離れる前に`T`で検査されます。 ### 再帰: `fn` 関数本体では`fn`が現在実行中の関数値を指します。名前を付けずに 再帰できます: fib = fn (x) { if x < 2 { x } else { fn(x - 1) + fn(x - 2) } } ### メソッド: `self` `self`はメソッド呼出の期間中だけ束縛されます。メソッド呼出の外で `self`を使うとスコープに存在しないため`undefined variable 'self'` になります。 ### クロージャ 関数リテラルはレキシカル環境を参照で捕捉します。 * 内側の関数で変数を読むと、見つかるまでスコープ連鎖を辿ります。 * 内側で`x = v`と書いた場合、外側に`x`があれば外側の束縛を 更新し、なければ新しいローカルを作ります。 * 捕捉された可変変数(例: 外側の`mut x`)は共有されます。捕捉した 全てのクロージャと外側スコープ自身から更新が見えます — 捕捉は 値ではなく参照で行われます。 例: make_counter = fn () { mut n = 0 fn () { n = n + 1; n } } c1 = make_counter() c2 = make_counter() inspect(c1()) # 1 inspect(c1()) # 2 inspect(c2()) # 1、独立したカウンタ JITでは捕捉された可変変数はヒープの**セル**に配置され、複数の クロージャが同じスロットを共有できます。§17参照。 ### ジェネレータ (`yield`) 本体に`yield`を含む関数は**ジェネレータ関数**です。`fn`宣言、`fn`式、 クラスのメソッド、traitの既定メソッドのいずれでもかまいません。呼び出しても 本体は実行されず、イテレータ (§18.5) が返ります。本体は消費側が値を 引き出すたびに、`yield` 1個分ずつ進みます。 ```culebra fn counter() { yield 1 yield 2 yield 3 } inspect(counter().collect()) # => [1, 2, 3] ``` `yield expr`は式ではなく**文**です。消費側に値を渡すだけで自身は何にも 評価されないため、`let x = yield 1`は構文エラーになります。値は ジェネレータから出ていく一方向のみで、`send()`に相当するものはなく、 中断中の本体に値を渡して再開することはできません。 `yield from expr`は任意の反復可能オブジェクト — `Array`、`String`、 遅延チェーン、別のジェネレータ — に委譲し、その全要素を送り出してから 委譲元の本体を再開します: ```culebra fn inner() { yield 'x' } fn mixed() { yield from [1, 2] yield from inner() yield 'done' } inspect(mixed().collect()) # => [1, 2, 'x', 'done'] ``` 委譲を使う主な理由である再帰的な走査もこれで組み立てられます: ```culebra fn walk(node) { yield node.value for kid in node.kids { yield from walk(kid) } } let leaf = {value: 3, kids: []} inspect(walk({value: 1, kids: [{value: 2, kids: [leaf]}]}).collect()) # => [1, 2, 3] ``` **ジェネレータオブジェクト。** 呼び出しが返すのはイテレータです。 `iter` / `has_next` / `next` / `dispose`を備えるので、`for`-in、遅延 コンビネータ群 (§18.5)、その他プロトコルを駆動するあらゆる消費側から 使えます。中断できることで、終わりのないソースも実用的になります: ```culebra fn nat() { mut i = 0 while true { yield i i += 1 } } inspect(nat().map(|x| x * x).take(5).collect()) # => [0, 1, 4, 9, 16] ``` 呼び出しは、どの呼び出しとも同じくパラメータを束縛します。デフォルト値の 評価と宣言した型の検査はそこで行われます。本体が動き出すのは最初の `has_next()`で、本体を次の`yield`まで進め、その値を`next()`のために 持っておきます。ジェネレータは**一度きり**で、汲み尽くしたらそのまま 尽きた状態を保ち、同じオブジェクトを再び反復しても何も出てきません。 新しく回すには関数を呼び直します。 どの関数が作ったものでも、ジェネレータの`type_of`は`'Generator'`で、 表示は`Generator {}`です。中断している本体が持っているものは プロパティには現れません。2つのジェネレータが等しいのは、同じ オブジェクトであるときだけです。ジェネレータは作ったisolateに属し、 Sendableではありません(stdlibの`Isolate`)。 ```culebra fn count(n) { for i in 0..n { yield i } } let g = count(3) inspect(type_of(g)) # => 'Generator' inspect(g == g) # => true inspect(g == count(3)) # => false ``` **本体の中では**、`while`・`for`・`if`・`{ }`の本体の任意の深さに `yield`を置け、`break` / `continue` / `return`は通常の関数と同じように 働きます(`return`は生成を終了します)。`defer`は通常の関数と同じく、 そのスコープを抜けるときに実行されます (§15)。ループ本体の`defer`は 反復ごとに、本体直下の`defer`は本体が終わったときに走ります。本体の中の `for`-inも同じ規則で反復子を閉じ、`drop`を持つローカル (§17) は そのスコープを抜けるときにdropされます。 ```culebra fn rows() { for id in [1, 2] { defer { inspect("close {id}") } yield id } } for r in rows() { inspect(r) } # => | # 1 # 'close 1' # 2 # 'close 2' ``` yieldするクラスのメソッドは、オブジェクト自身を反復可能にする手段です。 オブジェクトに対する`for`は`iter()`が返すものを歩き、メソッドのジェネレータは どのメソッドとも同じように`self`を読みます。`static`メソッド、getter、 オブジェクトのプロパティとして書いた関数も同じように働きます(最後のものは、 呼ばれたオブジェクトが`self`になります)。 ```culebra class Bag { new(.items) {} iter() { for x in self.items { yield x } } } for x in Bag.new([1, 2]) { inspect(x) } # => | # 1 # 2 ``` `yield`は`try`の本体にも`catch`の本体にも置けます。再開した後に走るコードの throwは、中断しなかった関数と同じように、その`yield`がある`try`が捕まえます。 そこで中断している間にジェネレータをcloseすると、`catch`は走らずに領域を 抜け、保留中のdeferが走ります。 ```culebra fn guarded(n) { try { yield 'start' if n > 0 { throw "bad {n}" } yield 'ok' } catch e { yield "caught {e}" } yield 'end' } inspect(guarded(0).collect()) # => ['start', 'ok', 'end'] inspect(guarded(2).collect()) # => ['start', 'caught bad 2', 'end'] ``` 本体の中で宣言した`fn name`は、どの関数の中でもそうであるように束縛されます。 書かれたスコープの中にあり、それを読むクロージャから見え、周りの`yield`が 何をしていても本体の変数を読めます。 本体の中の`self`は、そこで宣言した関数の中と同じ意味です。メソッドの中なら そのメソッドのreceiverで(`yield`をまたいでも、メソッドが戻った後でも 保たれます)、receiverが無い場所では`NameError`です。本体の中でオブジェクトの プロパティとして書いた関数の`self`は、その関数が呼ばれたオブジェクトです (§10)。 ```culebra class Shelf { new() { self.books = ['a', 'b'] } titles() { fn each() { for b in self.books { yield b } } return each() } } inspect(Shelf.new().titles().collect()) # => ['a', 'b'] ``` ジェネレータは中断したまま止まることもあります。消費側が`break`・ `return`・例外で`for`-inを抜けたとき、終端メソッドが途中で終わったとき (§18.5)、`dispose()`を呼んだとき、ジェネレータへの最後の参照が消えた ときです。その時点で残っている`defer`が内側から順に実行され、スコープに 残っているローカルが解放されます。後始末は本体が最後まで到達したか どうかに左右されません。`has_next()`が`next()`のために持っていた値も このとき解放されます。 ```culebra fn two() { defer { inspect('closed') } yield 1 yield 2 } for v in two() { inspect(v) break } # => | # 1 # 'closed' ``` 本体の外へ出た例外もジェネレータを終わらせます。例外が抜けていく途中で 本体の残っている`defer`が走り、例外は次の値を求めた側に届き、 ジェネレータはそれ以降尽きた状態になります: ```culebra fn broken() { defer { inspect('closed') } yield 1 throw 'boom' } let b = broken() inspect(b.next()) inspect(try { b.next() } catch e { "caught {e}" }) inspect(b.has_next()) # => | # 1 # 'closed' # 'caught boom' # false ``` **制約。** * `yield`は`defer`ブロックの中には置けません。deferはスコープを抜けるとき に走り、そこにはジェネレータをcloseするときも含まれるので、再び中断する 本体はcloseできないからです。パーサが`SyntaxError: yield cannot appear inside a defer block`で拒否します。スコープが終わる前にyieldしてください。 * `yield`は関数かメソッドの本体になければなりません。ファイルの トップレベル、`|...|`ラムダ(本体は式です)、コンストラクタ`new`、 `drop`の中ではパース時に拒否されます: SyntaxError: yield can only appear inside the body of a function or a method (not `new` or `drop`): the top level of a file, a `|...|` lambda, a constructor and `drop` cannot be generators. Move the yield into a named fn and call it instead. この検査はファイルのロード時に走るので、どのバックエンドでも同じ プログラムを同じ位置で拒否します。 * 本体が走っている間のジェネレータは再開できません。本体の中から 自分自身の`has_next()`や`next()`を呼ぶと`ValueError: generator already running`になります。 * 本体の中の名前は、普通の関数の中と同じ意味を持ちます。宣言は それぞれ自分の変数を作り、その変数はスコープの間だけ生きます。 ブロック・腕・ループ本体の`let`は外側の同名の変数とは別の変数で、 ループは回ごとに自分の変数を持ちます。`mut`なしで宣言した変数への 代入は`ImmutableError`になり、ロード時の名前の検査 (§6) も本体に 対して同じように働きます。`effect fn`の本体と`handle`本体も同じ規則に 従います (§16)。 * 本体の裸の`perform`には、本体を再開した呼び出しを囲んでいるハンドラが 応えます。本体の中に自己完結した`handle { ... }`式や`effect fn`の宣言を 書くのも動作します (§16)。 同一のプログラムはVM・JIT・AOTバイナリのいずれでも同じ値を生成します。 --- ## 12. 制御フロー (`if`, `cond`, `while`, `for`) ### `if` if cond { then_block } if cond { then_block } else { else_block } if c1 { b1 } else if c2 { b2 } else { b3 } if init; cond { … } `cond`は真偽値変換可能(`Bool`または`Long`)である必要があります。 `if`は式で、値は取られた分岐の最後の式。取られた分岐がない(`else` なしで`if`が偽)場合は`nil`。 `while`と同様に、`if`も省略可能な **init節**(条件の前に`;`で 区切って置く宣言)を受け取れます。これは`if` / `else if` / `else` チェーン全体にスコープを閉じ込めます([`while`](#while) と同じ形で、 C++17の`if (init; cond)`に相当): ```culebra # doctest: skip if mut d = compute(n); d > threshold { use(d) # ここで d が見える … } else { fallback(d) # … ここでも見えるが、チェーンの外では見えない } ``` 各束縛は宣言(`let` / `mut`)でなければならず、素の`if x = 0; …`は `SyntaxError`です。複数束縛は`,`で (`if mut a = f(), mut b = g(); …`)。 各腕(`then`本体、すべての`else if`、`else`)は`{}`ブロックと同じく独自の スコープです(§6 スコープ)。腕の中で宣言した名前は`if`の後では見えません。 取り出すには`if`の値を束縛します: ```culebra fn pick(c) { x = if c { 1 } else { 2 } x } inspect(pick(true)) # => 1 inspect(pick(false)) # => 2 ``` `if`の後で腕の名前を読む(`if c { x = 1 } else { x = 2 }`の後の`x`)と、 読込時の`NameError`です。 ### 文修飾子(`if` / `unless`) stmt if cond stmt unless cond 末尾に付く`if` / `unless`(Rubyの文修飾子)は、`cond`が真(`if`) または偽(`unless`)のときだけ`stmt`を実行します — `unless`は `if !cond`とまったく同じです。どちらのバックエンドに渡る前に通常の `if cond { stmt }` / `if !cond { stmt }`へ脱糖されるので、条件が 成立しないときの値は本体なし`if`と同じく`nil`です: ```culebra mut i = 0 while i < 10 { i = i + 1 break if i > 5 continue unless i % 2 == 0 println(i) } # => 2 # => 4 ``` 修飾子は任意の式ではなく**文**全体に付きます — `f(1 if true)`は Rubyと同様`SyntaxError`です。値が意味を持つのは、修飾された文が ブロックや関数本体の最後の文であるときだけです: ```culebra fn grade(v) { return "small" if v < 10 return "big" unless v < 100 "medium" } inspect(grade(5)) # => 'small' inspect(grade(500)) # => 'big' ``` 宣言(`let`、`mut`、宣言する分割代入、`fn`、`class`、`enum`、 `effect fn`)と`defer`には修飾子を付けられません。修飾された 文は脱糖後の`if`の腕で、腕はそれ自身がスコープです(§6 スコープ)。 宣言は腕と一緒に終わり、`defer`はその場で走ることになるため、どちらも `SyntaxError`です。裸の`x = v if c`はスコープ内の`x`への書き込みなので 書けます: let x = 1 if c # !! SyntaxError defer { close() } if c # !! SyntaxError x = 1 if c # OK: スコープ内の`x`に書き込む let y = if c { 1 } else { 0 } # `if`の値を宣言する defer { if c { close() } } # 条件は中に書く `unless`が予約されるのは代入ターゲットの位置だけで、他の [hard 予約語](#キーワード)と同様パラメータ名・オブジェクトのキー・ プロパティ名としては引き続き使えます。 ### `cond` cond { test => body, ..., _ => default } 主語のない多分岐条件式(Elixirの`cond` / Kotlinの引数なし `when`に相当): 上から順にアームを試し、最初に`test`が真になった アームの値が式全体の値になります。`_`は常に一致するワイルドカードで、 慣習的に最後に置きます — `_`に一致した後のアームは実行されません。 どのアームも一致しなければ値は`nil`(`match`が一致しなかった場合と 同じ)。 ```culebra fn grade(n) { cond { n >= 90 => 'A', n >= 80 => 'B', _ => 'C', } } inspect(grade(85)) # => 'B' ``` `test`の真偽値変換規則は`if`と同じ(`Bool`または`Long`、ゼロは 偽)で、呼び出し・`&&`/`||`・比較など任意の式を書けます。アーム本体は `match`のアームと同じ[式かブロックか](#アーム本体)の規則に従います: 素の式、またはブロックの最後の文の値を返し独自スコープを持つブレース ブロックのどちらかです。式かブロックかにかかわらず、すべての腕が独自の スコープで、三項演算子`c ? a : b`の各腕も同じです(§6 スコープ)。 マッチ対象が特にない場合、`match true { _ if … => ... }`のような ガード連鎖の代わりに`cond`を使ってください — ダミーの対象への マッチではなく、優先順位付きの条件リストとして読めます。 ### `while` while cond { body } while init; cond { body } `while`は文で、値は`nil`。ループ本体内で`break` / `continue`が 使えます。本体は反復ごとに新しいスコープです(`for`と同じ。 [スコープ](#スコープ)参照)。 条件の前に、`;`で区切って**init節**(カンマ区切りの宣言リスト)を 置けます。これはループにスコープを閉じ込めた変数を束縛するので、 カウンタが外側スコープに漏れません: while mut i = 0; i < len { out.push(i) i = i + 1 } # ここでは`i`は見えない init変数は反復をまたいで生存し(本体の`i = i + 2`は再代入)、 ループがどの経路(正常終了・`break`・例外)で抜けても破棄されます。 複数束縛は`,`で: while mut i = 0, mut j = xs.size() - 1; i < j { i = i + 1; j = j - 1 } 各束縛は宣言(`let`または`mut`)でなければならず、素の `while x = 0; …`は`SyntaxError`です(ループにスコープを閉じ込める のではなく外側の`x`を再代入してしまうため)。分割代入も束縛でき ます: `while mut (a, b) = pair; …`。 ### `for` ... `in` for var in iterable { body } `iterable.iter()`を一度呼んで得たイテレータを`has_next()` / `next()`で駆動します。`has_next()`が`true`の間、`next()`が 返す要素を`var`として反復ごとに新しいスコープに束縛します(§18.5)。 ```culebra for x in [1, 2, 3] { inspect(x) } for k, v in {b: 2, a: 1} { inspect("{k}={v}") } # (key, value) ペアを挿入順 for i in 0..10 { inspect(i) } # 排他範囲(0..9) for i in 0..=10 { inspect(i) } # 包含範囲(0..10) ``` rangeの値`a..b`(排他)と`a..=b`(包含)は`range`と同じ遅延整数列を反復します。 両端が`Long`で揃ったrangeは反復でき、スライス用の開いたrange(`xs[2..]`)は 反復の終わりがないので、反復すると例外になります。 端点には`Float`も使えます。その場合rangeは列ではなく数値の区間で、作る・表示する・ 比較することはできますが、反復・スライス・`grid`への引き渡しはその場で `TypeError`(`expected Long, got Float`)になります。数値でない端点は、rangeを 作った時点で`TypeError`です。 ```culebra inspect(0.0..0.86) # => 0.0..0.86 inspect(..0.5) # => ..0.5 inspect(1..3 == 1.0..3.0) # => true ``` rangeは`by `節で1以外の刻み幅を指定できます(`step`を負に すれば降順も可)。刻み幅は常に`Long`です: ```culebra for i in 0..10 by 2 { inspect(i) } # 0, 2, 4, 6, 8 for i in 10..0 by -2 { inspect(i) } # 10, 8, 6, 4, 2 ``` `step`は`0`にできません(rangeを反復した時点で`ValueError`)。 スライス(`xs[a..b by n]`)は`step`を無視します — 反復にのみ影響します。 **計算した値から組み立てる** — `Range(start, end, inclusive = false, step = 1)`。リテラルは「どちら側が開いているか」「両端を含むか」を構文で 書きますが、実行時に決めることはできません。コンストラクタはその2つを値で 受け取り、端に`nil`を渡すと開いた形(`a..`、`..b`、`..`)になります。 刻み幅が`0`ならリテラルと同じく`ValueError`です。 ```culebra inspect(Range(1, 5)) # => 1..5 inspect(Range(1, 5, true)) # => 1..=5 inspect(Range(1, 9, false, 2)) # => 1..9 by 2 inspect(Range(2, nil)) # => 2.. inspect(Range(1, 5) == 1..5) # => true ``` rangeとは`a..b`か`Range(...)`が作ったもののことで、同じ形をまとった値の ことではありません。同じキーを持つObjectはただのObjectで、スライス・反復・ `grid`への引き渡しはrangeとして扱われず例外になります。 **所属判定** — `r.contains(x)`は区間として判定します: `start <= x`かつ `x < end`(`..=`なら`x <= end`)で、開いた端は何も制限しません。`Long`と `Float`は`<`と同じように比べます。数値でない引数と`NaN`はどのrangeにも 含まれず、引数が原因で例外になることはありません。`by`の刻み幅が1以外の rangeは`ValueError`です。 ```culebra inspect((0..10).contains(5.5)) # => true inspect((0.0..0.86).contains(0.86)) # => false inspect((..=0.5).contains(0.5)) # => true inspect((0..10).contains('a')) # => false ``` **ループ変数の分解。** `var`はパターンにでき、各要素の形にマッチさせる (不一致は`ValueError`)。括弧なしのカンマ区切りはタプルパターンの糖衣で、 `for k, v in xs`は`for (k, v) in xs`と同じ: ```culebra # doctest: skip for [a, b] in [[1, 2], [3, 4]] { inspect(a + b) } # 配列パターン for (k, v) in [(1, 'a'), (2, 'b')] { inspect(k) } # タプルパターン for k, v in {a: 1, b: 2} { inspect("{k}={v}") } # 括弧なし == (k, v) for i, v in xs.enumerate() { ... } # (index, value) タプル ``` イテレータプロトコル(§18.5)により、対象は`iter`メソッドを持つ `Object`(サブ型の`Array`含む)、またはすでにイテレータとして 振る舞う`has_next` / `next`メソッドを持つオブジェクトである必要が あります。それ以外の型は`type error`。 `Set`の反復は、イテレータ生成時点の要素のスナップショットを歩きます: ループ本体がSetを変更しても、歩みには影響しません。`Object`が スナップショットするのはキーだけで、削除したキーはスキップされ、値は liveに読まれます(§18.5参照)。`Array`は1ステップごとにliveに 読まれます。 `for`は文で、値は`nil`。`var`にシャドウ規則が適用されます — 外側関数でキャプチャ済みの名前をシャドウしそうな場合、スクリプトは 拒否されます(§6参照)。 **JIT**: `--jit`では`Array` / `Object`(`(key, value)`ペアを挿入順)/ `String`(UTF-8スカラー単位)に対する直接反復として`for` / `break` / `continue`が動作します。加えて、独自の`iter` プロパティを持つObject(ユーザー定義iterator・`range`・ `String.code_points()` / `.graphemes()`・iteratorメソッドチェーン) はiteratorプロトコルを実行時に駆動します — Array/String/keysの 高速パスはそのまま維持。 ### `break` と `continue` break # 最内ループを抜ける continue # 次のiterationへ `for`か`while`の内部でのみ有効。ループ外で使うと実行前に検査され `SyntaxError`になります。`break` / `continue`は値を運びません (ループの値は`nil`のまま)。 どちらも**式**なので (§3)、専用の文の位置を必要としません。`match`の アーム(`')' => break`)、`cond`のアーム、三項演算子の両側にそのまま 置けます — アームの形は[アーム本体](#アーム本体)を、大きな式の途中に 置いたときの意味は[制御を移す式](#制御を移す式)を参照。 **ラベル。** `while`と`for`にはラベルを付けられ、`break` / `continue` はそのラベルを指定できます。指定すると、最内ループではなく**その ループ**を抜ける(あるいは次のiterationへ進める)ことになります: label: while cond { body } label: for var in iterable { body } break label # `label`のループを抜ける continue label # `label`のループの次のiterationへ ```culebra let grid = [[1, 2], [3, 4], [5, 6]] mut found = nil search: for row in grid { for cell in row { if cell == 4 { found = cell break search } } } inspect(found) # => 4 ``` ラベルは`break` / `continue`と同じ行に置く必要があります。そのため、 ラベルなしの`break`の次の行が識別子で始まっていても、2つの文のまま です。ラベルが届くのは、それが書かれた関数の中のループだけです — 入れ子の`fn`や`defer`ブロックの中の`break outer`は、そこでの ラベルなしの`break`と同じく`SyntaxError`になります。囲むループの どれも持たないラベルを指定した場合と、囲むループが既に持っている ラベルを付け直した場合も、実行前に検査されて`SyntaxError`です。 ラベルは束縛ではありません。同じ名前は変数名としてそのまま使えますし、 並んだ別の入れ子で同じラベルを使い回せます。 ラベルで入れ子を抜けるのは、1つずつ抜けるのと同じ抜け方です — 途中の ループはそれぞれの`defer`ブロックを(内側から)実行し、イテレータを 閉じてから、ジャンプ先に着きます。 ### `nobreak`(loop-else) while cond { body } nobreak { … } for var in iterable { body } nobreak { … } `while` / `for`の後に置ける省略可能な`nobreak { … }`ブロックは、 **ループが正常に終了したとき**(条件がfalseになった、または イテレータが尽きた)**のみ**実行され、**`break`で抜けたときは スキップ**されます(`return`やthrowで抜けたときも通り抜けて実行 されません)。条件を直接表す`nobreak`(breakしなかった ときに走る)という名前にしています。値は運びません(ループは`nil` のまま)。 代表的な用途は検索で、フラグ変数なしの「見つからなかった」分岐に なります: ```culebra fn find(xs, target) { for x in xs { if x == target { return "found" } } nobreak { return "not found" # ループが一度も break しなかったときだけ到達 } } ``` `while`のinit節は`nobreak`ブロック内でも見えます(カウンタが ループ後の後処理まで生存する)が、`for`のループ変数は見えません (反復ごとのスコープで既に消えている)。`nobreak`ブロック内の `break` / `continue`は、このループが既に終了しているため、**外側の ループ**に属します(ラベル付きでも同じで、他の場所と同じように入れ子を 抜けます)。逆に、本体からのラベル付き`break`で抜けられたループは、 自身の`nobreak`ブロックを実行しません — breakで抜けられたからです。 `nobreak`は文脈依存キーワードで、この末尾位置でのみ認識されるので、 他の場所では通常の識別子として使えます。 ### `return` 関数本体内でのみ有効。指定した値(または`nil`)で関数を抜けます。 関数の外の`return`は`SyntaxError`(実行前に検査)。スクリプトは 最後の文で終了し、早期終了は`Sys.exit`を使う。 `break`・`continue`と同じく`return`も式なので、`_ => return x`は 正当な`match`アームです。 ### 制御を移す式 `return`・`throw`・`break`・`continue`には共通の性質があります — どれも値を生みません。到達した時点で制御が他所へ移るので、それが 置かれた式は最後まで評価されません。4つが文でなく式である理由も そこにあります。外側の式に渡すべき値が存在しない以上、型が合わない 位置というものが無いからです: ```culebra # doctest: skip let width = "v" + match n { # nが4のとき`+`は完了しない 4 => break, _ => "x", } ``` 知っておくべき帰結: * 制御が移る前に評価済みのオペランドは破棄されます。 `f(a(), match n { 0 => break, _ => b() })`では`break`到達時点で `a()`は既に実行済みですが、`f`の呼び出しは起こりません。 * `defer`ブロックは文で書いた場合とまったく同じく、内側から順に 抜ける途中で実行されます (§15)。 * `break` / `continue`が束縛されるのは最内の**ループ**であって、 それが書かれている`match`や`cond`ではありません。これらは式で あって制御フローのスコープではないため、「matchを抜ける」という 解釈自体が存在せず、混同の余地がありません。 * `break`に囲むループがあるか、`return`に囲む関数があるかの検査は 実行前に走り、式の位置でも文の位置と同じく`SyntaxError`です。 式になったからといって結合が強くなるわけではありません。`return`の オペランドはどちらの位置でも式全体なので、`return 1 + 2`は`match`の アームでも文の先頭でも`3`を返します。 ### `debugger` プログラムを`--debug`付きで実行した場合、`debugger`は実行を一時 停止して簡単なCLIデバッガに入ります。`--debug`なしではno-opです。 --- ## 13. パターンマッチ match subject { pattern1 (if guard1)? => body1, pattern2 (if guard2)? => body2, ... } `match`は式です。アームは上から順に試され、パターンが一致(ガードが あれば真)する最初のアームの本体が実行され、その値が`match`の値に なります。どれも一致しない場合は`nil`。 `while`や`if`と同様に、`match`も省略可能な **init節**(subjectの 前に`;`で区切って置く宣言)を受け取れます。これはsubjectと全アーム にスコープを閉じ込めます([`while`](#while) と同じ形で、C++17の `if (init; cond)`に相当): match mut x = compute(n); x { 0 => "zero", v if v > x - 1 => v, # init変数`x`はガードでも … _ => x # … アーム本体でも見えるが、matchの外では見えない } 各束縛は宣言(`let` / `mut`)でなければならず、素の`match x = 0; …` は`SyntaxError`です。複数束縛は`,`で (`match mut a = f(), mut b = g(); a + b { … }`)。init変数はmatchを どの経路で抜けても(一致したアーム・非一致の素通り・例外)ドロップ されます。 ### パターン | 形式 | 一致条件 | |------------------|-------------------------------------------| | `0`, `'x'`, `"x"`, `nil`, `true` | リテラル等価(文字列パターンは`'...'`・backtick・補間なしの`"..."`) | | `-1`, `-2.5` | 負の数値リテラル。`1`と同じく型まで一致する等価 | | `lo..hi`, `lo..=hi`, `..hi`, `lo..` | 区間に入る`Long`または`Float`(`Range#contains`と同じ判定)。端点は数値リテラル | | `name` | 任意の値、`name`に束縛 | | `_` | 任意の値、束縛なし | | `name: Type` | 指定の型の値、`name`に束縛 | | `Type` | 指定の型の値、束縛なし | | `p1 \| p2 \| p3` | どれかが一致(alternativeは束縛不可) | | `[p1, p2, ...]` | 同じ長さの`Array` | | `[p1, ...rest]` | `n−1`以上の長さの`Array`。`rest`は残りの新しい`Array` | | `[a, ...m, z]` | 中間位置のrestも可 | | `{k1, k2}` | 指定キーを持つ`Object`。各`ki`に`obj.ki`を束縛(省略形) | | `{k1: p1, k2: p2}` | `k1`が`p1`に、`k2`が`p2`に一致する`Object`。入れ子可(`{user: {name}}`)。省略形と混在可 | | `{}` | 任意の`Object`(キー無視) | | `(p1, p2, ...)` | 同じ要素数の`Tuple`。要素ごとに部分パターン | | `Ok(p1, ...)` | enum constructor: variant `Ok`にマッチしpositional payloadを部分パターンに分解。修飾形`Result.Ok(p)`は親enumが`Result`であることも要求する — 同名variantを持つ2つのenumはこれで区別する。「Sum type」参照 | ### セマンティクス * リテラルパターンはコンパイル時定数でなければなりません。**補間入り** `"...{x}..."`はSyntaxError — 実行時の値と照合するにはガードを使います (`s if s == x => ...`)。 * 範囲パターンは、`(lo..hi).contains(x)`が受け付ける数値に一致します。 `Long`と`Float`は`<`と同じように比べるので、リテラルパターンと違って 対象がどちらの型かは問いません: `match 5.5 { 0..10 => … }`は一致し、 `match 1.0 { 1 => … }`は一致しません。数値でない対象と`NaN`は例外に ならず次のアームへ進みます。端点は数値リテラル(負号可)で、`by`は 書けません。`5.. =>`は空白を空けて書きます(`5..=>`は`..=`と読まれます)。 ```culebra kind = fn (x) { match x { -1 => 'minus one', ..0 => 'negative', 0..=9 => 'digit', _ => 'other', } } inspect(kind(-1)) # => 'minus one' inspect(kind(-0.5)) # => 'negative' inspect(kind(3.0)) # => 'digit' inspect(kind(9.5)) # => 'other' inspect(kind('a')) # => 'other' ``` * パターンは左から右、サブパターンは深さ優先で評価されます。 * パターンが導入した束縛`name`はそのアームのガードと本体で有効。 * 裸の名前は先頭の1文字で読み分けます。型名の書き方(§14)と同じで、 大文字で始まる名前は型を指し、それ以外の名前は束縛です。`Dog => …`は `d: Dog => …`から束縛を除いたもので、型名は注釈に書ける形のまま 書けます(`Long?`、`Array`、`Shape.Point`)。複数の型は`|`で 並べます(`Long | Float => …`)。関数型は束縛つきの形で書きます (`f: fn(Long) -> Long => …`)。どの型のものでもない名前は何にも 一致しません。値として読まれることは無いので、定数との比較はガードで 書きます(`n if n == LIMIT => …`)。大文字で始まる名前に束縛したい ときは型を付けて`N: Any`と書きます。 ```culebra class Dog { new(.name) {} } kind = fn (v) { match v { Dog => 'a dog', Long | Float => 'a number', other => "something else: {other}", } } inspect(kind(Dog('Rex'))) # => 'a dog' inspect(kind(2.5)) # => 'a number' inspect(kind('s')) # => 'something else: s' ``` * この読み方は、値を検査するパターン(`match`のアームと`catch`節、§15)の ものです。パターンが宣言や代入をする場所では、大文字か小文字かに かかわらずすべての名前が書き込み先です: `let [N, M] = pair`、 `(A, B) = (B, A)`、`for`の変数、パターン引数。 * `|`(or)サブパターンは束縛できません。alternative内の名前はその枝を 通った経路にしか存在しないため、束縛を書くと`SyntaxError`です (`a | _`、`5 | a`、`Ok(x) | Err(x)`)。`|`の中にはリテラル・型名・`_`を 置き、束縛の形ごとにアーム(もしくはパターン)を分けます。 * `Array`パターンは`...rest`がなければ長さの厳密一致を要求します。 `Object`パターンはキーの網羅性を要求しません(余分なキーは無視)。 * rest配列は新しく確保された`Array`で、要素は浅くコピーされます。 rest自体の変更は元の配列に影響しませんが、要素が参照型ならその 先の可変オブジェクトは共有されたままです。 * `match`の対象は1度だけ評価されます。 ### アーム本体 アーム本体は通常は単一の式です。複数の文を実行したいときは波括弧の **ブロック**を書きます — アームの値はブロックの最後の文の値になります: let kind = match tok { '+' => { let p = prec(tok); register(p); "op" }, _ => "atom", } ブロックアームは独立したスコープです。パターン束縛やブロック内の`let` はそのアーム内だけで有効で、`defer`はアームの波括弧が閉じるときに発火 します(LIFO、アームの値が消費される前)。ブロックアーム内の`return` / `break` / `continue`は通常どおり振る舞い、抜ける際にそのアームの 保留中の`defer`も実行します。 制御を移すだけのアームにブロックは不要です。制御を移す4形式はすべて式 だからです (§12): match tok { ')' => break, '!' => throw "unexpected", ' ' => continue, _ => tok, } アームのパーサはまず式として解釈を試みるので、波括弧がそのまま有効な リテラルなら、ブロックではなくリテラルの意味を保ちます: match x { 0 => {}, # 空のObject 1 => {a: v}, # Objectリテラル 2 => {p, q}, # Objectの短縮形: {p: p, q: q} _ => { f(); g() } # ブロック: f() を実行しg() を実行、g() の値を返す } 注意点が一つ、Objectの**省略形**から派生します: 単一の裸の識別子を 包んだ`{ v }`はObject `{v: v}`であって、`v`を返すブロックでは ありません。それには`_ => v`(波括弧なし)か、ブロックを強制する `{ v; }`と書きます。単独の`{ break }` / `{ continue }`も省略形として 読まれ、`break`という名前の変数は作れないのでそのアームは`NameError` になります — 意図どおりの裸の`_ => break`と書いてください。 ### 網羅性 静的な網羅性検査は行いません。どのアームにも一致しない場合は`nil` が得られます。気になる場合は`_ => nil`や型パターンでのフォール バックを明示してください。 --- ## 14. オプショナル型注釈 Culebraは動的型付けで、型注釈は任意です。注釈は以下3つの実行時 ポイントでのみ検査されます: 1. 関数エントリで、各パラメータ値がその注釈と検査される。 2. 関数戻り時(落ち抜けでも`return`でも)、結果が宣言された 戻り値型で検査される。 3. 注釈付き代入で、RHSが注釈と検査される。 ### 構文 let x: Long = 10 let mut s: String = 'hi' fn (a: Long, b: Long) -> Long { a + b } ### 有効な型名 Nil Bool Long Float String StringView Array Tuple Set Object Function Tensor Any `Any`は常に一致します。未知の型名は検査失敗で`type error`と なります。`class C { ... }`で宣言したクラス名も有効な注釈で、 そのインスタンスを受け入れます。enum名はそのenumの全variantを、 variant名はどのenumのものでも同名variantを、修飾形 `Enum.Variant`はそのenumのvariantだけを受け入れます — 「Sum type」参照。 エラーのkind(`TypeError`、`IOError`など、§15)も型名で、そのkindを 持つObjectを受け入れます。 組み込み型の名前(上の一覧のうち`Any`以外のすべて)は、常にその型を 指します。同じ名前のクラス・enum・variant・traitは、プログラムのもので もライブラリのもの(`FST.Set`)でも、その名前の注釈を満たしません。 プログラムの`class Long`のインスタンスは`Long`ではありません。`Long`型の フィールドの検査と同じ答えで、multimethodもそれを理由にoverloadを選び ません。そうしたインスタンスは`Object`か、注釈なしで受け取れます。 `Function`は`__call__`メソッドを持つインスタンスも受け入れます。 constructorパターンでは、そうしたvariantやクラスもその名前で指せます。 `Long(n) =>`は`enum Json { Long(Long), ... }`のvariant `Long`に一致します。 型名は大文字で始まります。`class point`と宣言したクラスを指す注釈は 構文エラーです。`match`のパターンで型と束縛を見分けるのも、この先頭の 1文字です(§13)。 ### Union 型 注釈に`|`で複数候補を並べると、**いずれか** に一致すれば検査 成功します。 fn show(x: Long | Float) -> String { to_string(x) } show(1) # → '1' show(2.5) # → '2.5' show("hi") # !! type error fn lookup(k: String) -> Long | Nil { if k == "answer" { 42 } else { nil } } Union注釈は単一型注釈が使える場所ならどこでも使えます — 引数、 戻り値、`let` / `let mut`宣言: let id: Long | String = "u-42" let mut count: Long | Nil = nil count = 0 # OK: 再代入では再検査は行われません `|`周りの空白は許容 (`Long|Float`, `Long | Float`)。クラス名と プリミティブの組み合わせも可 (`Square | Circle`, `String | Nil`)。 単一候補の注釈は従来通りの動作 — `Long`と`Long|`は等価ではあり ません (後者はパースエラー)。 Union内の`Object`は **クラスインスタンスのcatch-all** (`class:`タグを持つ値) であって、すべての値のcatch-allではあり ません。プリミティブはそこを素通りします。`Long | Object`はLongと 任意のクラスインスタンスを受け取りますが、Stringは依然fail。 全部受けたい場合は`Any`を使ってください。 候補が **未宣言のクラス名** の場合は、誰もそのクラスのインスタンス ではない通常のクラス注釈と同じ扱い — その候補は何にも一致しないので 実質dead altです。他の候補は通常通りmatchします。 多重ディスパッチ ([§20](#20-多重ディスパッチ)) はUnion型注釈を 理解し、各候補でscoreして最良一致を選びます — 例: `fn area(s: Square | Circle)`はSquare / Circleの双方で dispatch可能。また、bare concrete typeは同型を含むUnionよりも specific扱い: `fn pick(x: Long)`と`fn pick(x: Long | Float)`を 両方定義すると、Long引数はconcrete `pick`に、Float引数はUnion 版にroutedされます。 ### Optional 型 (`T?`) 型名末尾の`?`は`T | Nil`の糖衣 — nilを受理しつつ、非nil値には 基底型を強制します。 fn id(x: Long?) -> Long? { x } id(5) # → 5 id(nil) # → nil id("s") # !! 拒否(Longでもnilでもない) `?`は任意の型名に付けられ、Generic outer (`Array?`) や `let` / 引数 / 戻り値注釈 (`let a: String? = nil`) でも使えます。 内省ではUnion形に正規化される: `x: Long?`の`fn.params[0].type`は `"Long | Nil"`。上記のnull安全演算子(`?.`, `?[]`, `!!`, `??`, `??=`)と組み合わせて使います。 ### 関数型 (`fn(T) -> U`) 素の`Function`型は任意のcallableを受理します。高階引数の *形* — 何を取り何を返すか — を記述するには関数型`fn(T1, T2) -> R` を書きます。 fn apply(f: fn(Long) -> Long, x: Long) -> Long { f(x) } apply(|n| n * 2, 21) # → 42 fn make_adder(n: Long) -> fn(Long) -> Long { |x| x + n } let add5 = make_adder(5) add5(37) # → 42 引数リストは空 (`fn() -> String`)、複数型 (`fn(Long, Long) -> Long`)、 ネスト (`fn(fn(Long) -> Long) -> Long`) も可。callableなクラス インスタンス(`__call__`メソッドを持つもの)も、クロージャと同様に 関数型を満たします。 class Doubler { new() {} __call__(n) { n * 2 } } apply(Doubler.new(), 21) # → 42 Genericの要素型と同じく、引数型・戻り型は **MVPではdocumentation** — ランタイムは値がcallableかどうかだけを検査し、引数の数や実際に 受け取る型は検査しません。非callableは拒否されます。 apply(99, 21) # !! type error: ... expects fn(Long) -> Long 戻り型は単一の型なので、`->`の後のtop-levelな`|`は外側のUnionに 属します: `fn(A) -> B | C`は`(fn(A) -> B) | C`(*Bを返す関数* と *C* のUnion)と解釈されます。オプショナルな **戻り値** は戻り型に`?`を 付けます — `fn(A) -> B?`は結果が`B | Nil`の関数で、関数自体は依然 必須です。 fn run(f: fn(Long) -> Long?) -> Long { f(0) ?? -1 } run(|n| nil) # → -1 run(nil) # !! type error(関数が必須) 多重ディスパッチ ([§20](#20-多重ディスパッチ)) は関数型引数を`Function` と同様に扱います: クロージャや`__call__`インスタンスは `fn(...) -> ...` overloadに、具体型 (`Long`) は自身のoverloadに ルートされます。 ### Generic 型 型名に角括弧で型パラメータを付けられます: `Array`, `Array>`, `Array`。ネスト + 内側Union の組み合わせも可能。組み込みのコンテナ型は現状`Array`のみで、 それ以外のGeneric外側名 (`Box`, `Pair`等) はuser定義 classから (後述「Genericクラス宣言」)。 fn first(xs: Array) -> Long { xs[0] } fn lookup(k: String) -> Array | Nil { ... } **要素レベルのruntimeチェックは行いません** — 要素型まで検証すると 呼び出しのたびにコレクションを走査することになります。境界で チェックされるのは外側の型 のみ。型パラメータはドキュメントと多重ディスパッチのtie-breakの ために存在します。 fn first(xs: Array) { xs[0] } first([1, "two", 3]) # OK — 要素型は強制されない `<>`内の空白は許容され、canonical形に正規化されます: `Array < Long >`, `Array< Long >`, `Array`はすべて `fn.params[i].type`で`"Array"`として現れます。 多重ディスパッチtie-break: Generic引数付きのparamは同じ外側型 だけのbare形よりもspecific。`fn show(xs: Array)`と `fn show(xs: Array)`を両方定義すると、Array引数はGeneric 版にroutedされます。 #### クラス宣言の位置 クラスはトップレベル構築物。`class C { ... }`を **他のクラスの body直下に書くことは禁止** — 宣言はtop level、もしくはfn / lambda / lexical-scope (`{ ... }`) の中 (新しいscopeを開く) のみ 許可。mental modelをflatに保つため。namespaceは`import` / `export`、inner helperは自由関数 / UFCSで。 class Bad { m() { class Inner { ... } # !! SyntaxError } } # OK — fn bodyはscope境界 fn make_inner() { class Inner { ... } Inner.new() } #### Generic クラス宣言 クラスも型パラメータを宣言できます: class Box { new (v: T) { self.v = v } } class Pair { new (k, v) { self.k = k; self.v = v } } 制約なしの型パラメータはドキュメント — メソッドのシグネチャを 読みやすくするためのもので、runtimeは`Any`として扱います。 **Bound付き** (``) は強制されます (後述「Generic Bound」)。classは外側名 (`Box`, `Pair`) でbindされ、インスタンス は外側のclass tagを持ちます: let b = Box.new(42) type_of(b) # → 'Box' Genericクラスを型注釈で使う構文は組み込みGeneric型と同じ: fn unbox(b: Box) -> Long { b.v } #### 既知の制限 * 外側Generic名が同じoverload (`fn show(xs: Array)`と `fn show(xs: Array)`) は両方score 4でdispatchされるため、 Array引数の呼び出しは曖昧 (DispatchError)。要素レベルcheck (Phase 2b) で自然に解消。 * 内省での型パラメータ: `fn.params[i].type`は宣言通り (`T`, `Array`) を返し、読みやすさを優先する。ただしruntime型checkは 外側 / `Any`しか見ない。内省で得た`T`を本物の型として後段dispatch に使わないこと。 #### 予定 (Phase 2b) * expression位置のtype args (`Box.new(42)`) — parserは 現状`<...>`を型注釈contextのみで使用。 * opt-in要素runtime check。 * class body内class宣言 (現状はSyntaxError、上記「クラス宣言の 位置」参照)。 ### nil と注釈(null safety ポリシー) culebraは`nil`をuniversalに保ちつつ、注釈でruntime null safetyを opt-inできます。3段階: * **注釈なし** — `nil`含め何でも可(デフォルトの動的挙動)。 * **`T`(非optional)** — `nil`を**拒否**。`x: Long`引数は`nil`を 受けず(dispatch/type error)、`let x: Long = nil`はtype error。 * **`T?`(optional)** — `nil`を**許容**しつつ、非nil値には基底型を 強制(`x: Long?`はLongかnil、Stringは不可)。 これは **runtime** ポリシーで、静的null検査器は持ちません(設計上の 判断)。null安全演算子が対になります: `?.` / `?[]`で nil可能値を辿り、`!!`で非nilをアサート(`NilError`)、`??` / `??=` でフォールバックを供給/保存。 `late` / `lateinit`キーワードはありません: 静的言語のこの機能は 「遅延初期化を*静的*非null検査器に納得させる」ためのもので、culebra にはその検査器が無いからです。nullableフィールド + 使用箇所`!!`、 または`??`ガードで代替します。遅延計算・メモ化フィールドのイディオム はnilチェック付きアクセサメソッド —`self._data ??= load()`が直接 使えます(`??=`は単純変数だけでなく`obj.key`ターゲットにも対応): class Cache { new() { self._data = nil } data() { self._data ??= load() self._data } } ### 検査されない箇所 * 算術・論理演算子は自身のオペランド型を検査しますが、中間値の 注釈は追加の検査にはなりません。 * オブジェクトのプロパティ値に注釈は付けられません — クラス宣言の外で オブジェクトが生やしたプロパティに制約はありません。クラスで宣言した fieldだけが例外で、その注釈は書き込みのたびに検査されます(§10)。 ただし`String`(と、それを許す注釈)は検査されません。 * 配列要素の型は追跡されません。 * `mut x: T`の注釈、および`mut`パラメータの注釈は、宣言時または呼び出し 時の一度のみの検査で、後続の再代入では再チェックしません。 注釈は主に**ドキュメントと境界検査**のための機能であり、型システム ではありません。 ### プリミティブ型のパラメータが確定させること プリミティブ型名(`Long`・`Float`・`Bool`・`String`・`Array`・`Object`・ `Set`・`Tuple`)で注釈されたパラメータは入口で検査されるので、本体での その名前の読みは既に注釈を満たしています。コンパイラはこれを前提として 扱います: そうしたパラメータへの組み込みメソッド呼び出しは、その名前を 共有する複数の型のどれを持っているかを調べる代わりに、型が名指す1つの 受け手に解決されます。`Long`や`Float`のパラメータの算術は、オペランドの 検査を落とします。答えはどちらでも同一で、注釈は問い合わせを省くだけです。 2つの注釈は何も確定させません。`mut`パラメータは再代入でき、それは 再チェックされません(上記)。`Function`は構造的に満たされ(`__call__`を 持つクラスのインスタンスが通る)、1種類の値を名指しません。クラス名は クラスを確定させますが表現は確定させません。それが読み手に何を許すかは §10が扱います。 ```culebra fn width(s: String) { s.size() } inspect(width('hello')) # => 5 # 確定させているのは入口の検査であり、それ以外は従来どおり拒否されます。 # 型付きパラメータは多重定義のシグネチャなので、拒否はDispatchErrorです。 # !! DispatchError width(42) ``` ```culebra # `mut`パラメータは後続の読みについて何も約束しません: 再代入は # 再チェックされないので、本体は渡されたものをそのまま見ます fn swapped(mut s: String) { s = 42 type_of(s) } inspect(swapped('x')) # => 'Long' ``` ### クラス型の名前が読み手に許す仮定 宣言クラスの分かる名前 — クラスを名指す注釈を持つパラメータか`let`、 そのクラス自身のメンバ内の`self`、クラス型fieldを辿った次の段 — は束縛 される場所で検査され(クラス型fieldはそれ自身の書き込み検査(§10)で 検査されるので、次の段はそのクラスのインスタンスか`nil`です)、その クラスのスカラ宣言fieldは書き込みのたびに検査されます。したがってその 名前を通じた*そうした*fieldの読みは、何が入っているかを既に知って います。コンパイラはこれを使います: `p: Point`で`Point`が`x: Float`を 宣言していれば、`p.x`は`Float`の読みとしてコンパイルされ、値への問い 合わせは行われません。それ以外の型で宣言されたfieldは、その型が1つの tagを決めないので、従来どおりの読みになります。`mut`の束縛はこの約束を 受け取りません — 再代入は再チェックされないので、宣言を読み取る根拠が ありません。 入口の検査が見るのは、値が何によって作られたかであって、どう見えるかでは ありません。クラスの名前はインスタンスがたどるmetaにあり、普通のObjectは fieldを持つことでそれを手に入れることはできません。ですからクラス名を 名乗るObjectは、fieldを読む前に、渡された場所で拒否されます。 読み側にも検査は残っています — 入口の検査を通っていない受け手のためで、 他のオブジェクトに移されたメソッド値が`self.x`を読む場合がそれです。 そこではfieldと型を名指し、違う種類の値を答えることはありません。 同じ理由で`remove`は検査されるfieldを拒否します — 消えうるfieldは契約に なりません。`String`で宣言されたfieldと注釈のないfieldは、何も約束されて いないので削除できます。 入口の検査が比べるのはクラスの名前です。プログラムが1つの名前を複数のクラスに 付けている場合(トップレベルに1つと関数の中にもう1つ、あるいはstdlibの `Vector2`と並べて自前の`Vector2`)、そのどれのインスタンスも検査を通ります。 ですからその名前が確定させるのは、それらのクラス全部が同じ型で宣言している fieldだけで、ほかのfieldは通常どおりに読まれます。enum、enumのvariant、 trait、組み込みの型、stdlibの通常のクラス、エラーのkind(§15)のいずれかも 同じ名前を持っている場合、その名前は何も確定させません。クラス自身のメンバが`self`を読むときは、どちらの場合も 自分の宣言に従います。 ```culebra class Point { x: Float new(x) { self.x = x } } fn scaled(p: Point) { p.x * 2.0 } inspect(scaled(Point(1.5))) # => 3.0 # クラス名を名乗るだけのObjectはそのクラスではないので、引数に束縛されません。 # !! DispatchError scaled({class: 'Point', x: 'not a float'}) ``` ### Sum type (`enum`) `enum`はsum type — variantの固定集合を宣言する。各variantは 任意のpositional payloadを持てる。Generic param対応。 enum Result { Ok(T), Err(E), } enum Color { Red, Green, Blue } # nullary variant enum Shape { Circle(Float), Rect(Float, Float), Origin } 各variantは **variant-as-class** にlowerされる: enum名が名前空間 としてbindされ、構築はそれを経由する。payload variantは constructor、nullary variantはsingleton値。 let r = Result.Ok(5) # payload variant — constructor呼び出し let c = Color.Red # nullary variant — singleton値 let s = Shape.Rect(3.0, 4.0) Result.Ok # constructorは第一級の値 variant instanceはvariant名と親enum名の両方でタグ付けされ、 型注釈 / パターンは両レベルでマッチする: match r { Ok(x) => x, # constructor pattern、payloadを束縛 Err(e) => -1, } match v { x: Ok => "Ok variant", # variant型パターン x: Result => "some Result", # enum型パターン (任意のvariant) _ => "other", } 束縛が要らなければ型名だけを書きます: `Ok => …`、`Result => …`。 variant名だけを書くと、それはvariantの名前であってenumの名前では ない。2つのenumがどちらも`Ok`を宣言していれば、`Ok(p)`も`x: Ok`も 両方を受ける。enumまで絞るには修飾する — パターンなら `Result.Ok(p)`、型なら`x: Result.Ok`で、どちらもvariantと親enumの 両方を検査する。enum名だけ書けば (`x: Result`) そのenumの全variant を受ける。 payloadはpositionalで`_0`, `_1`, … (`r._0`) で参照可能だが、 constructor patternがidiomaticなアクセサ。 variantは書いたとおりの形で表示される。補間・`println`・`to_string()`・ `inspect`のどれでも同じ。`type_of`はvariant名を返し、`class_of`は enumオブジェクトそのものを返すので、`class_of(v) == Shape`で variantがどのenumに属するかを問える: "{Color.Red}" # 'Color.Red' inspect(Shape.Rect(3.0, 4.0)) # Shape.Rect(3.0, 4.0) type_of(Shape.Origin) # 'Origin' class_of(Shape.Origin) == Shape # true variantは構造上`Eq`かつ`Hashable` — その同一性はenum・variant・ 各payload fieldの組 — なので、`@derive`なしでそのまま`Object` / `Set`のkeyや`hash(v)`の引数になる。payload variantがkeyになれる のはpayloadがhashableなとき に限る: `Shape.Rect(2.0, 3.0)`はなれるが、`Result.Ok([1, 2])`は Array自身の`unhashable type` TypeErrorを投げる。別enumの同名 variantは別のkey。 mut hits = {} hits[Color.Red] = 1 hits[Shape.Rect(2.0, 3.0)] = 6 Set(Color.Red, Color.Green, Color.Red).size() # → 2 #### 注意・制限 * canonicalなsum-typeスタイルは無型param + `match`: `fn area(s) { match s { Circle(r) => ..., ... } }`。 * 多重dispatchは3つの粒度のどれでもキーにできる。 `fn f(x: Result.Ok)`はそのenumの`Ok`だけ、`fn f(x: Ok)`はどのenum のものでも`Ok`、`fn f(x: Result)`はそのenumの全variantを受ける。 宣言されているうちいちばん細かいものが選ばれ、enum名は同じvariant を並べた`Ok | Err`のunionより優先される。 * enum内メソッド (free fn + UFCSで代替) / named payload field / 明示discriminant値 / 静的exhaustiveness検査は無し (非マッチ `match`は`nil`)。 * nullary variantは型パターンでマッチ — `Origin`、値を束縛するなら `o: Origin` (parensなしconstructor patternは無い)。 ### trait と protocol `trait`は「契約」 — ある名前で扱うために値が備えるべきmethod集合 の宣言。dispatch / Generic Boundでこの契約名を使える。 trait Greeter { hello() -> String } **構造的conformance**: クラスがtraitのrequired methodを全て (name + arity一致で) 持てば、自動的にtraitにconform — `impl Foo for Bar` block不要。conformするかどうかはclassが実際に持つ メソッドで決まります。 class Bob { new(name) { self.name = name } hello() { "hi, {self.name}" } } fn greet(x: Greeter) { IO.inspect(x.hello()) } greet(Bob.new("Alice")) # → "hi, Alice" required methodを欠くクラスはdispatch段階でreject (DispatchError)。 #### Default method trait methodにbodyを付けるとdefault実装になり、適合クラスは 自動継承。同名methodをクラスに書けばoverride: trait Counter { current() -> Long next() -> Long { self.current() + 1 } # default } class Zero { new() {} current() { 0 } } Zero.new().next() # → 1 (default) class Five { new() {} current() { 5 } next() { 99 } # override } Five.new().next() # → 99 defaultは適合クラスが継承したmethodなので、クラス自身のmethodと 同じ順位に立つ: これを上回るのはownプロパティか宣言されたmethod だけで、runtimeがオブジェクトに供給するもの — `Object`の組み込み method(`size`/`keys`等)・合成された`parameters()`・duck typing のiterator method群 — にはdefaultが勝つ。 2つのtraitがそれぞれ同名のdefaultを持つこともある。両方に適合し、その名前を 自分では定義していないクラスには継承すべきdefaultが1つに決まらないので、 それに触れると`DispatchError`(`ambiguous default`)になる。クラスにその methodを定義すれば解決する: trait Left { left_req() side() { 'left' } } trait Right { right_req() side() { 'right' } } class Both { left_req() { 1 } right_req() { 2 } } Both().side() # DispatchError: ambiguous default `side`: ... trait内でmethod名は1度だけ宣言する。同名を2つ書くと`SyntaxError`: contractもdefault bodyも名前をkeyに持つので、trait側にはそれらを まとめるoverload setが無い(クラスにはある — 「Method overloading」 参照)。 trait宣言は`class`と同じく実行された時点で効く: 通らない分岐の中の traitは何も登録せず、同名の再宣言は前のcontractとdefaultをまとめて 置き換える。 #### Built-in trait runtimeはpreambleとして8つの基本traitをship — `import`不要 で使える: | Trait | Required | Default | |---|---|---| | `Stringer` | `to_s() -> String` | — | | `Eq` | `eq(other) -> Bool` | `neq` | | `Comparable` | `cmp(other) -> Long` | `lt`, `le`, `gt`, `ge` | | `StringLike` | `to_string_view() -> StringView` | — | | `Hashable` | `hash() -> Long` | — | | `Iterator` | `has_next() -> Bool`, `next() -> Any` | — | | `Iterable` | `iter() -> Iterator` | — | | `Dir` | `read(path) -> String`, `list_dir(path) -> Array`, `exists(path) -> Bool`, `is_file(path) -> Bool`, `is_dir(path) -> Bool` | `files`, `glob`, `size`, `copy_to` | Objectリテラルはクラスを持たないので、どんなメンバーがあってもこれらのどれにも conformしない。 `cmp`だけ書けば6関係methodが揃い、`eq`だけ書けば`neq`が 入り、`to_s`だけ書けばStringerの使える場所で受け取れる。 `StringLike`はAPI境界で「バイト列として読める文字列」を受け取る 契約 — `String`と`StringView`の両方がconformする。 `Hashable`は`Object` / `Set`のkey挿入時にチェックされる契約 — user classは`hash()` (戻り値`Long`) と`eq(other)`を定義すれば keyとして使える。enum variantはどちらも無しで`Eq`と`Hashable`に conformする — 「Sum type」参照。 keyのhashは、keyがObjectやSetに入るときに計算して、keyと一緒に 保存する。そのためkeyにするclassは2つの約束を守る必要がある。 `eq`で等しいkey同士は同じ`hash()`を返すこと、そして格納している 間はhashが変わらないこと。格納した後は保存したhashを使うので、 反復・`{...obj}`でのコピー・`==`・表示・Setの演算 (`union` / `intersect` / `diff` / `sym_diff` / `subset` / `superset`) は、 格納済みのkeyの`hash()`を呼ばない。格納中のkeyを書き換えてhashが 変わった場合、entryは残るので`size()`・`keys()`・`for`には現れる。 一方、そのkeyで引く (`obj[k]` / `has` / `contains` / `remove`) と hashを計算し直すので、見つからない: ```culebra class Id { new(n) { self.n = n } hash() { self.n } eq(o) { self.n == o.n } } let k = Id.new(1) mut d = {} d[k] = 'one' k.n = 2 inspect(d.size()) # => 1 inspect(d.has(k)) # => false ``` `Iterator` + `Iterable`はfor-in protocolをformal化。 `iter() -> Iterator` / `has_next() -> Bool` / `next() -> Any`を持つclassはiterableで、全pipeline method (`map` / `filter` / `take` / `collect` / ...) で使える。 `has_next()`はidempotent (連続呼び出しで次値を消費しない) — runtime wrapperがlookaheadを1つcacheする。`next()`は`nil`を含む 任意値をyield可能 (nil terminator設計だとnilの区別ができない)、 終端判定は`has_next()`とpairで行うのが契約。 組み込みのprimitive (Long / String / Array等) もハードコード された対応表でtraitにconformする — class wrapper不要: | Primitive | Stringer | Eq | Comparable | StringLike | Hashable | Iterable | |---|:---:|:---:|:---:|:---:|:---:|:---:| | Nil / Bool | ✓ | ✓ (Bool) | ✓ (Boolのみ) | — | ✓ | — | | Long / Float | ✓ | ✓ | ✓ | — | ✓ | — | | String / StringView | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Tuple | ✓ | ✓ | ✓ | — | ✓ | ✓ | | Array / Set | ✓ | ✓ | — | — | — | ✓ | | Object (bare literal) | ✓ | ✓ | — | — | — | ✓ | | Tensor | ✓ | ✓ | — | — | — | — | | Function | ✓ | — | — | — | — | — | `fn show(x: Stringer) { to_string(x) }`は`show(42)`や `show([1, 2, 3])`をclass wrapperなしで受ける。ただしtrait method呼出 (`x.to_s()`) はclass instanceでのみresolve — primitive にはmethod dispatchがないので、traitは`fn`境界経由 (`fn show(x: Stringer)`) で扱い、`.method()` syntaxは使えない。 `Hashable`も同様: primitiveは`hash(v)` global builtin (`to_string` の対) 経由、class instanceは`x.hash()`直接呼出も使える。user classをObject / Set keyにする場合は対応する`eq(other)`の同時 定義が必要 (container内のequalityとhashの一貫性のため)。 #### Generic Bound 型パラメータにtrait制約をインラインで付けられる。free関数・ classの両方で使え、 **呼び出し境界で強制**される (boundにconform しない値はそのparamの引数にならない): class Money { new(amount) { self.amount = amount } cmp(other) { self.amount - other.amount } # Comparableにconform } fn pick_max(a: T, b: T) { if a.gt(b) { a } else { b } # gtはComparableのdefault } pick_max(Money.new(10), Money.new(25)).amount # → 25 pick_max([1], [2]) # !! no matching method Boundは宣言時に **bound traitへlower** され、通常のtrait conformance機構をそのまま再利用する (compile-time checkなし、 culebraの型checkはruntime)。帰結: * **dispatch特異性**はtraitレベル: 具象overload > bounded > 無制約`` catch-all。 fn rank(x: Long) { "concrete" } fn rank(x: T) { "bounded" } fn rank(x: T) { "unbounded" } rank(5) # → "concrete" rank(1.5) # → "bounded" (FloatはComparable) rank([1]) # → "unbounded" (Arrayはどちらにも非適合) * **lenient単一化**: 同じ型パラメータが複数位置に出る (`a: T, b: T`) とき、boundは *各位置で個別に* 検査され、繰り返しの`T` は単一のconcrete型に強制されない。`(a: T, b: T)`の関数は `(1, 2.0)`を受理する (両方`Comparable`)。 * boundはconformanceを強制するが、後述の **primitive method surface制限**は残る: `a.gt(b)`は`a`がobjectのときのみ dispatchされる。primitive引数ではbodyでnativeな`<` / `>` を使う。 無制約``は任意の引数を受ける (`Any`にlowerされる) — paramに 名前を付け、より特異なoverloadに負けるための形。 **複合bound** ``も使える — argは **全パート**にconform する必要がある (Unionのany-ofに対するall-of)。spacingは寛容 (``も可)。 fn both(x: T) { x } both(5) # OK — Longは両方にconform both([1, 2]) # !! ArrayはStringerだがHashableでない -> reject #### 型パラメータのスコープ 型パラメータはシグネチャだけでなく、宣言の本体全体で有効です。本体の `let`の注釈、入れ子の関数やlambda、入れ子のクラスのフィールド、`match`の 型パターンは、どれも`T`をその型パラメータとして読み、シグネチャと同じように lowerします(制約なしの`T`は`Any`、boundがあればそのboundとして検査)。 `T`という名前のものが他にあっても変わりません。`T?`はどこでも`T | Nil`です。 入れ子の宣言が自分の`T`を持てば、外側の`T`を隠します。 ```culebra fn pair_up(a: T) { let keep = fn (b: T) -> T { b } let first: T = a [first, keep('x')] } inspect(pair_up(1)) # => [1, 'x'] ``` #### dispatch tie-break trait paramのスコアはObjectと具象の中間。同じ関数名で`x: Pri` と`x: Stringer`のoverloadがあれば、argがPriなら具象が勝つ。 trait pathのみmatchする場合はtrait版が選ばれる。 #### trait 継承 traitは`:`の後にsupertraitを宣言できる: `trait Ord: Eq`、 `trait Both: Eq, Show`。supertraitのmethodはsubtraitにflatten される — `Ord`にconformするには`Eq`と`Ord`の**両方**のrequired methodが要る。推移的に効き (`Done: Ord`は`Eq`も引き込む)、 supertraitの **default** methodもsubtraitで使える。 trait Eq { eq(other) -> Bool } trait Ord: Eq { cmp(other) -> Long } # Ord conformにはeq + cmp fn sorted(xs: Array) { ... } supertraitは継承するtraitより前に宣言する必要がある (宣言順で flattenを解決)。primitive型のconformanceは名前ベース (hard-coded table) なので、trait継承は **構造的 (class) conformanceのみ**に 効く — primitiveがsupertrait経由でuser traitを満たすことはない。 #### method の自動導出 (`@derive`) `@derive(...)` classデコレータは標準的なconformance methodを自動 生成する。data classが`eq` / `hash` / `to_s` / `cmp`のboilerplate を手書きしなくて済む。導出可能なのは4つで、各々1 methodに対応: | derive | 生成method | 挙動 | |---|---|---| | `Eq` | `eq(other)` | `other`が同じclassで同じdata fieldを持ち、それぞれがキーとして等しいときtrue | | `Hash` | `hash()` | class名と各data fieldのhashを合成 | | `Show` | `to_s()` | `"ClassName(f1, f2, ...)"` (各fieldの値repr) | | `Comparable` | `cmp(other)` | data fieldを宣言順に辞書式比較 | @derive(Eq, Hash, Show, Comparable) class Point { new(x, y) { self.x = x; self.y = y } } let a = Point.new(1, 2) let b = Point.new(1, 2) a.eq(b) # → true a.hash() == b.hash() # → true a.to_s() # → "Point(1, 2)" a.cmp(Point.new(1, 3)) # → -1 `@derive`はtraitの **required** methodだけを供給するので、trait defaultも自動で付く: `Eq`導出で`neq`、`Comparable`導出で `lt` / `le` / `gt` / `ge`が揃う。`Eq` + `Hash`を導出すると `Object` / `Set`のkeyとして使えるようになる。 生成methodは **reflective** — 呼び出し時にinstance自身のdata fieldを走査する (methodと内部class tagは除外) ので、field変更に 追従する。詳細: * **user定義が優先**。classが既に宣言している名前の派生methodは skipされるので、大半をderiveしつつ1つだけ手書きできる (`@derive(Eq, Hash)` + 独自`to_s`など)。 * **`@derive`はcompiler認識**で関数ではない — `Eq` / `Hash` / `Show` / `Comparable`のみ受理 (それ以外はSyntaxError)。同じclass 上の通常デコレータとも併用できる。 * **class専用。** enumのvariantは構造上`Eq`かつ`Hashable`なので、 `enum`への`@derive`はSyntaxError。 * **Nominal**: 派生`eq`は同じclass tagを要求するので、同一field を持つ別class同士は等しくならない。 * **派生`eq`は等価を宣言しない**。「インスタンスはfieldで一致する」と 言うだけで、enumのvariantと同じ扱いになる。キーとしては型が厳密で (`1`と`1.0`は別のキー)、だから`eq`は`hash`と一致でき、classが キーになれる。`==`では構造比較 — fieldを`==`で比べるので`Long`と `Float`が値として混ざる — になり、`eq`を持たないclassとまったく同じ 答えを返す。キーにするために`Eq`を導出しても`==`は狭まらない。2つが 食い違うのは、fieldのキーとしての同一性が`==`と違うところだけ。混在 した対(`P(1) == P(1.0)`は真だが`P(1)`と`P(1.0)`は別のキーのまま)と、 `NaN`(`P(Math.nan) == P(Math.nan)`は偽だが2つは1つのキー)。classが自分で書いた `eq`や`__eq__`は宣言された等価で、`==`はこれまでどおりそれに従う。 * **`cmp`の順序**は各field対を`<`とまったく同じ規則で比較する。 等しい (`==`) fieldは次へ進み、最初に等しくなかった対が結果を決める。 だからfieldがTupleでも、自分の`cmp` / `__lt__`を持つクラスの インスタンスでも、単独のときと同じ順序になる。`<`が順序を付けられない 対 — Array、どちらのメソッドも無いObject、型違い — はそこでも同じ `cannot compare` TypeErrorになる。 * **派生`cmp`は`==`と一致する**。どちらもfieldを`==`で辿るので、 `cmp(other)`が`0`になるのは`==`が真のときちょうど。`==`と食い違うのは キーの関係である`eq`のほうで、理由は上のとおり。 * **`eq`と`cmp`は`other`を取り**、`hash`と`to_s`は引数を取らない。 引数なしで呼べば通常のmissing required argumentのArityError。 #### 制限 (Phase 4 MVP) * `impl Foo for Bar` blockは未対応 — 構造的のみ。 (Phase 4+ で 明示宣言を再検討) * **演算子はtrait defaultを呼ばない**: `<` / `<=` / `>` / `>=`は `__lt__` / `__le__`、次に`cmp`を、`==`は`__eq__`、次に宣言された `eq`を使う(*traitメソッドへのフォールバック*)。Comparableの default `lt` / `le` — classが自分で上書きしたものも — は `x.lt(y)`の形で呼んだときだけ動く。 * **複数trait同名defaultの選択は非決定**: 2つのtraitが 同名default (`to_s`等) を持ちinstanceが両方にconformするとき、 dispatchは内部hash mapのiteration順で勝者決定。同名default を持つtraitの併用は避ける。 * **自己再帰trait defaultはuser責任**: `trait X { foo() { self.foo() } }`でconform classが`foo`をoverrideしないと stack overflow。depth guard未実装。 * **JIT trait default closureのrefcount**: 各default methodは per-Runtime tableに +1 referenceで保持。trait再宣言でdisplaced bodyはreleaseされ、残りはRuntime破棄時にreleaseされるため、 long-running JIT hostで多session再compileしても蓄積しない。 * **多重dispatch overhead**: trait引数を持たない関数でも、呼び出し 毎にtrait_registry × argsのwalkが発生 (cache後は安いが、 warmup 3 × n_argsのprobeコストがホットループで露呈)。Phase 4+ でtrait参照のないmultifnをskipする最適化を検討。 * **JIT trait-default fallbackはICキャッシュ無し**: trait default 経由のproperty accessは毎回default tableをwalk。`instance. default()`ホットループでそのコストを払い続ける。Phase 4+ で trait-default slot用ICを追加予定。 * **AOT trait再宣言**: methodごとにruntime upsertで登録するため、 preambleの`Eq { eq, neq }`の後にuserが`trait Eq { eq(other) }` を再宣言しても`neq`が残る。REPL / hot-reload環境ではtraitを 完全に書き直すかrestart推奨。 * traitもクラスと同様top-levelのみ宣言可能。 --- ## 15. エラー処理 ### `throw` / `try` / `catch` Culebraは`throw`で例外を発生させ、`try`/`catch`で受けます。 `throw`できる値は**任意のCulebra値**です。 throw 'something bad' throw {kind: 'io', msg: 'file not found', path: p} try { risky() } catch e { inspect("error: {e}") } try { fetch(url) } catch ApiError { retry() } # ApiErrorだけ catch e: Timeout if e.after > 30 { nil } # 型つきの束縛とガード catch {kind: 'IOError', message} { log(message) } セマンティクス: * `throw expr`は`expr`を評価し、その値を直近の`try`まで伝播 させます。**関数境界を越え**、何階層でも透過します * `try { A } catch name { B }`は新しいスコープで`A`を評価し、 `A`内で`throw`が起きたら別スコープで`name`にthrown値を 束縛して`B`を評価します。`try`/`catch`全体は**式**として、 実行されたブロックの最後の値を返します * `catch`の後ろはパターンで、`match`のアームと同じ読み方をします (§13)。`if`のガードも付けられます。したがって`catch e`と`catch _`は 投げられた値をすべて受け、検査をする節 — 型名(`catch ApiError`、 `catch e: ApiError`)、コンストラクタ(`catch NotFound(path)`)、 Objectの形(`catch {kind: 'IOError'}`)、複数の候補 (`catch ApiError | Timeout`) — は一致した値だけを受けます * パターンが`{`で始まる節は、パターン(またはガード)の終わる行に ブロックを書きます。ブロックも`{`で始まるためです * `try`には`catch`節を1つ以上書けます。上から順に試し、値を受けた 最初の節だけが実行されます。どの節も受けなければ、例外は発生した ときのまま外側の`try`へ伝播します。投げられた値はその`throw`の位置で、 実行時エラーはkind・message・位置を保って、割り込みは割り込みのまま です。外側で受ける節には、断った節のガードが見たのと同じ値が渡ります。 実行時エラーの場合も、同じエラーObjectです。 この時点で`try`本体のスコープは終わっています。本体の`defer`は 最初の節を試す前に実行済みです * 節を試している最中に発生した例外(ガードが投げた場合など)は、 処理中の例外を置き換え、ほかの例外と同じようにその`try`を抜けます ```culebra class ApiError { new(.status) {} } fetch = fn (status) { try { if status == 0 { 1 / 0 } throw ApiError(status) } catch e: ApiError if e.status == 404 { 'not found' } catch ApiError { 'http error' } catch {kind: 'ZeroDivisionError'} { 'no status' } } inspect(fetch(404)) # => 'not found' inspect(fetch(500)) # => 'http error' inspect(fetch(0)) # => 'no status' inspect(try { try { throw 'other' } catch ApiError { 'http' } } catch e { e }) # => 'other' ``` * トップレベルまで到達した未catchの`throw`は `uncaught: ... at LINE:COL.`(`throw`自身の位置)と表示され、 プログラムは終了コード1で終了します。例外はcatchした実行時エラーを そのまま投げ直した場合(`catch e { throw e }`)で、そのエラー自身として `Kind: message at LINE:COL.`の形で、エラーが起きた位置で表示されます * `throw`は`return`と別: `return`は自分の関数だけを抜け、 `throw`は外側の関数も貫通します * `throw`は式なので (§12)、ブロックで包まずに`match`のアーム全体に できます (`_ => throw "unexpected"`) ### `defer` `defer { BLOCK }`は、**囲みの字句スコープ**を抜ける時に`BLOCK` を実行するよう登録します。複数登録時はLIFO順。正常終了・早期 `return`・`throw`の**いずれの経路でも**必ず実行されます。 fn work() { { f = open_tmp() defer { f.close() } # この内側ブロックを抜ける時に発火 process(f) } # ← ここでf.close() が走る more_work() } ブロックスコープ(関数スコープではなく)であることの意味: * `if` / `cond` / `?:`の腕もスコープなので、腕の中の`defer`は囲みスコープ ではなく腕が終わるときに発火する * ループ本体の`defer`は**各反復**で発火 — 反復ごとのクリーン アップ要件に合う * 条件ブロック内の`defer`は、そのブロックが実際に実行された 時のみ発火 * 関数全体の終了時に発火させたければ、関数本体の先頭に`defer`を 書く(関数本体自体がブロック) * トップレベルの`defer`はプログラム終了時に発火 — importされた モジュールのトップレベルの`defer`も同じで、エントリモジュールのものの 後に走る defer本体内の`return`は**defer閉包のみ**を抜けます(外側の 関数を抜けません)。defer本体内の`throw`はdeferを中断して 通常の例外として伝播します。ただし伝播するのは、同じスコープでまだ 走っていないdeferが後入れ先出しの順のまま全部走った後です — 後から登録されたdeferが何をしても、deferは必ず走ります。複数のdeferが `throw`した場合は、最後に投げられた例外が伝播します。そのスコープは 例外が通り抜ける他のスコープと同じように抜けます — 束縛は解放され、 外側のスコープは自分のdeferを順に走らせます。 ```culebra fn close_all() { defer { inspect('file closed') } defer { throw 'flush failed' } inspect('body') } inspect(try { close_all() } catch e { e }) # => 'body' # => 'file closed' # => 'flush failed' ``` そのスコープが別の例外ですでに巻き戻し中だった場合、deferの`throw`が 元の例外を**置き換えます**。元の例外は捨てられ、巻き戻しはそのスコープ から外へ新しい例外で続き、外側の`try`で捕まえられます。`try`本体自身の deferは、正常終了でも`throw`での終了でも、その`catch`の届かない所で 走ります — そこから投げられた例外を受け取るのは1つ外側の`try`です: ```culebra fn close_fails() { defer { throw 'close failed' } throw 'read failed' } inspect(try { close_fails() } catch e { e }) # => 'close failed' ``` ### 割り込み(Ctrl+C) Ctrl+Cは協調的でcatch可能な`Interrupted`を発生させます。プロセスを 即座にkillすることはありません: try { serve() # 長時間ループ } catch e { inspect("shutting down: {e.kind}") # → Interrupted cleanup() } * 実行中の計算は次のループ反復または文の境界で停止し`Interrupted` をthrowします。tightなループ(`while true {}`でも)も中断可能で、 `IO.stdin().read()` / `IO.input`(stdin待ち)、ブロッキングな`Http` リクエスト(接続・応答待ち・本体転送)、ブロッキングな`Proc.run` / `Proc.all` / `Proc.race`(子プロセスはkillされる)も同様に、1回の 押下で待機を中断できます(強制終了の2回目を待つ必要はありません)。 * 通常の例外と同じくunwindするので、抜ける途中で`defer`が走ります。 * `catch`すれば実行は通常どおり再開します。割り込みはone-shotなので、 サーバやREPLはCtrl+Cを「現在のリクエストをキャンセル」として扱い 処理を続けられます。 * 未捕捉でトップに達した場合はトップレベルの`defer`が走り、慣例の ステータス`130`(128 + SIGINT)で終了します。 * 1回目が未処理のまま(safepointに到達しないwedgedなプログラム)の 2回目のCtrl+Cは、デフォルト動作で強制終了します。 REPLではCtrl+Cは実行中の評価を中断してプロンプトに戻ります(セッション を終了させません。`Interrupted`をread-evalループがcatchします)。wedged な評価中の2回目の押下は従来どおり強制終了します。 Ctrl+Cをthrowでなく「値」として受け取りたい場合(長時間サービスのgraceful shutdownパターン)は、`Signal.notify`でチャネルを登録します(stdlibガイドの *Signal* セクション参照)。 挙動はVM / JIT / AOTバイナリで同一です。`Interrupted`は ソース位置を持ちません(`line`/`col`は`0`)— 割り込みは非同期で、 特定の式に紐づかないためです。 ### Scope guard パターン コールバック内部から「呼び出し側スコープでcleanupしたい」など、 自分で`defer`を置けない場面では、小さなヘルパーを自前で定義すれば 十分です。言語には組み込みません。 make_guard = fn () { mut fns = [] { add: fn (f) { fns.push(f) }, run: fn () { mut i = fns.size() - 1 while i >= 0 { fns[i](); i = i - 1 } fns = [] } } } process = fn (items) { g = make_guard() defer { g.run() } mut f = nil items.for_each(fn (item) { if f == nil { f = open('out') g.add(fn () { f.close() }) } write(f, item) }) } ### ランタイムエラー 内部ランタイムエラー(型エラー、ゼロ除算、プロパティ不在など)は ユーザの`try`/`catch`に **構造化Error Object** として届きます: | プロパティ | 型 | 内容 | |---|---|---| | `kind` | `String` | `'TypeError'`などのエラー種別。下記参照。 | | `message` | `String` | 人間可読な説明。位置は含みません(`line`と`col`で読みます)。 | | `line` | `Long` | 該当ASTノードの1起点行番号。不明時は`0`。 | | `col` | `Long` | 同じく桁番号。不明時は`0`。 | 標準ライブラリの中で起きたエラー(Culebraで書かれたライブラリの関数や、 それが呼ぶネイティブ関数が投げたもの)は、そのライブラリを呼び出した ユーザのコードの位置を報告します。`line`と`col`がライブラリのソースを 指すことはありません。ライブラリから呼ばれたユーザの関数の中で起きた エラーは、その関数の中の位置のままです。 kindは型名でもあり、型名としてそのkindのエラーを受けます。 `catch`節、`match`のアーム、型注釈、multimethodのディスパッチの いずれでも同じです: ```culebra nth_ratio = fn (xs, i, d) { try { xs[i] / d } catch ZeroDivisionError { 0 } catch e: TypeError { "not a number, line {e.line}" } } inspect(nth_ratio([6], 0, 3)) # => 2 inspect(nth_ratio([6], 0, 0)) # => 0 inspect(nth_ratio([6], 0, nil)) # => 'not a number, line 3' inspect(try { nth_ratio([6], 5, 3) } catch e { e.kind }) # => 'IndexError' ``` あるkindのエラーとは、そのkindを持つObjectのことです。言語と標準 ライブラリが発生させるエラーからランタイムが作るObjectも、プログラムが `{kind: 'ValueError', message: ...}`として投げたObjectも同じです。 クラスのインスタンスは、`kind`というフィールドに何を持っていても、 そのクラスのものです。 両者の違いは、ほかに何を持つかです。実行時エラーは発生した位置の `line`と`col`を持ちます(標準ライブラリが発生させたものは、ライブラリを 呼んだ位置です)。誰も捕まえなければ`Kind: message at L:C.`の形で報告 されます。投げられたObjectは書かれたキーだけを持ち、`throw`の位置で `uncaught: {...}`として報告されます。 型名として使えるkindは、言語と標準ライブラリが発生させるものです。 下の表のkindすべてと、ライブラリの各namespaceのkind(`HttpError`、 `NetError`、`RegexError`、`ArgParseError`など)がこれに当たります。 自分で決めたkindは型名ではありません(`catch MyError`はクラスを指します)。 形で受けます: `catch {kind: 'MyError'}`。 kindと同じ名前でクラスを宣言すると、その名前は両方のものになります。 `catch e: IOError`はそのクラスのインスタンスも、そのkindのエラーも受け、 名前はクラスのfieldについて何も確定させません(§14)。multimethodの ディスパッチ(§20)では、kindの名前を書いた引数が、そのkindのエラーに 対して`Object`の引数より上になります。その名前を含むunionや`T?`は unionとしての順位で、`Object`より下です。 エラーを手にしているコードは`e.kind`でも分岐できます: try { let x = arr[100] } catch e { if e.kind == 'IndexError' { inspect("out of range at line {e.line}") } else { throw e } } 標準の`kind`一覧。各kindについて発生条件とcatch可能性を 明記します。すべてのkindは`e.kind` / `e.message` / `e.line` / `e.col`をVM・JIT・AOT全backendで同一にpopulatedします (注記がある場合を除く)。 | Kind | 発生条件 | catch可能 | |---|---|---| | `TypeError` | 演算子・比較における型不一致;非callableの呼出;`: T`型注釈失敗;`to_long`/`to_float`の非変換可能値;`__str__`がString以外を返す;`*` splatの非Array / `**` splatの非Object;組み込み引数チェックの失敗;同一パラメータへpositional + keyword重複;duplicate keyword;`*`区切りが許す数を超えたpositional(`takes N positional arguments but M given`) | はい | | `ZeroDivisionError` | 整数`/`, `%`, 負指数の`**`で除算になるケース;浮動小数`/`または`%`のRHS == 0 | はい | | `NameError` | 未定義識別子の読み取り;compound代入(`x += rhs`)の`x`が未定義;REPL global検索のmiss。スコープ規則(§6)で見える宣言がどれも解決しない読み取り(builtinを除く)は評価前に検出(コンパイル時エラー参照);宣言は見えるが未実行(またはスキップ)の読み取りはruntimeエラーのまま | はい¹ | | `ImmutableError` | `let`(非`mut`)束縛への代入;immutable ObjectプロパティまたはDictエントリへの代入;コンストラクタ本体内の`self`再代入 | はい | | `KeyError` | Dictの存在しないキーsubscript;Objectの存在しないキーsubscript | はい | | `IndexError` | Array / String / Tensorの範囲外index;Tensor slice範囲外;Tensor reduction axis範囲外 | はい | | `ValueError` | Destructure pattern mismatch;Tensorのshape / dtype不一致;`[].min()`等空コレクションへのreduce;不正な数値文字列;JSON parse失敗;1000段を超えてネストした値のwalk(下記「値ネストの上限」参照) | はい | | `AttributeError` | compound代入(`o.x += ...`)で`x`が存在しない;組み込み名前空間が持たないメンバーの読み取り・書き込み(`=`/`??=`)(名前空間は閉じている — クラスやプレーンなdictは自由に読み書きできる) | はい | | `ArityError` | 必須引数の欠如 — 関数やclass constructorへのpositional不足;**組み込み**・名前空間関数が受け取れる数を超えたpositional。**ユーザ定義**関数への余分なpositionalはエラーではなく`__ARGS__`(§19)に溢れる | はい | | `DispatchError` | 多重ディスパッチ(§20)でマッチなし、またはspecificity同点 | はい | | `AssertionError` | matcherの失敗 (`assert_true` / `assert_eq`等) もしくはユーザ`throw {kind: "AssertionError", ...}`。比較系matcherは両辺をmessageに含めます | はい | | `SyntaxError` | AST loweringで検出される構造エラー:`**rest`が末尾でない、`*`区切りの重複、デフォルト値後に非デフォルト、`let`付きcompound、ループ外の`break` / `continue`等。該当functionの宣言評価時に発火 | はい | | `ShadowError` | §6のシャドウ解析でキャプチャ済み外側名と衝突する束縛を検出。ユーザの`try`が走る前に発火 | **いいえ**(eval前のanalyzer) | | `IOError` | `FS` / `File`等stdlibファイル操作の失敗(パス不在・権限・クローズ済みハンドル);`Tensor.from_csv`失敗 | はい | | `ProcessError` | `Proc.run`の起動失敗(実行ファイルが存在しない等)、または`check: true`での非0終了 / シグナル死 | はい | | `SendError` | Sendableでない値をisolate境界(`Isolate.spawn` / `tx.send`)で渡した — ネイティブハンドル、`Tensor`、`mut`を捕獲したクロージャ、循環参照 | はい | | `ChannelError` | 全endpointが消えた(closed)channelへの`tx.send` | はい | | `ParallelError` | `Parallel.map` / `Parallel.each`の要素が例外を投げた。失敗要素のindexと原因を保持(fail-fast) | はい | | `DropContractError` | `drop` / `iter` / `has_next` / `next`プロパティに引数を取るFunctionを束縛した | はい | | `RecursionError` | 関数呼び出しの深さが固定上限1000フレームを超えた。ユーザ関数の入口(fn・lambda・メソッド・コンストラクタ — フィールド初期化子はコンストラクタのフレーム内で走る)が1フレームで、組み込みヘルパーやマルチメソッドのディスパッチは数えない。上限と報告される深さは全backendで同一、位置はcall site。カウントは`throw`とともに巻き戻るので、`catch`後は全予算を使い直せる | はい | | `RuntimeError` | 未変換throw siteから伝播した`std::runtime_error`をエンジンが拾うフォールバック。この場合のみ`e.line == 0` / `e.col == 0`がありうる | はい | 構文解析できないソースは、1行も実行される前に拒否されます。メッセージは パーサがそれ以上進めなかった位置と、そこで何があれば先へ進めたかを 示します: SyntaxError: failed to parse module '/home/me/prog.cul' /home/me/prog.cul:1:7: syntax error, unexpected '2', expecting ',', ')' or an operator. その位置で必要だったトークン(閉じ括弧、キーワード、区切り)はその文字列で 列挙されます。完結した式の後ならいつでも続けられる演算子はまとめて `an operator`、式の先頭になれるものは`an expression`と書かれます。空白と コメントは列挙されません。 未catchのエラーは`Kind: message at LINE:COL.`形式で表示し終了コード1で 終了します(未catchのCtrl+Cは`interrupted`を表示して130)。`culebra`でも `culebra build`で作ったバイナリでも同じです。ユーザが`throw expr`で投げた 値は`uncaught: {value} at LINE:COL.` で表示され、位置は`throw`自身のものです。ただし`catch`が実行時エラーと して受け取った値を投げた場合は、isolate境界を越えた後でも、そのエラー 自身としてエラーが起きた位置で表示されます。isolate境界を越えて再raise された値は位置を持ちません(それを投げた`throw`は別スレッドで走った ため)。複数行にわたるmessageは位置を先頭に置く形 (`Kind at LINE:COL: message`)になり、最終行の値の一部と読めてしまう のを避けます。 ¹ スコープ規則で見える宣言がどれも解決しない読み取り(builtinを除く)の `NameError`だけは例外で、評価前に検出され(コンパイル時エラー参照) catchできません。それ以外の`NameError`(宣言は見えるが未実行、スキップ された条件付き宣言、未定義の`x`への`x += rhs`、REPLの行)はruntimeエラーで catch可能です。 ### 値ネストの上限 ループは深さ無制限の値を作れます(`a = [a]`の繰り返し)。値のネストを walkする操作 — 表示系(`inspect` / `print` / 補間 / `to_string` / `join`)、`==` / `!=`、`hash`、メンバーシップ(`contains` / `index_of`)、isolate境界への送出(`Isolate.spawn`の引数、`tx.send`、 `Shared.new` — captureで連なるクロージャの鎖も同じ数え方)、 `@derive`されたメソッド — はすべて1000段で停止し、catch可能な `ValueError`(`nesting too deep (limit 1000)`)をcall siteの位置で 投げます。全backendで同一です。同一ポインタの比較(`a == a`)は walkせずに答えます。より深い値の構築・index・破棄はどの深さでも 安全です — teardownは内部的にboundされ、このエラーにはなりません。 `JSON` / `TOML` / `XML`は自身の木に同じ上限を適用します(stdlibリファレンス 参照)。 `Tensor`のautogradグラフは値のネストではなく、この上限の対象外です。 `.backward()`も、評価前のグラフを破棄する操作も、どの深さでも `ValueError`の上限なしに安全です。計算グラフには自然な「深すぎる」 という意味論が無く(長いsequenceに展開したRNNは正当なグラフであり、 壊れたデータではありません)、そのため人工的な深さ上限を設けず 内部的にboundされています(stdlibリファレンスのAutograd節参照)。 ### コンパイル時エラー プログラム読込時、`try`ブロックが走る前に3つの検査が走ります。 いずれもユーザコードではcatchできず、同じ`Kind: message`形式で 中断し、全バックエンドで同一に動作します。 * **`ShadowError`** — キャプチャ済み外側名と衝突する束縛。ルールは §6 の「シャドウ禁止」を参照。 * **`NameError`(静的に未定義)** — その位置で§6のスコープ規則のもとで 見える宣言が無く、builtinでもない名前の読み取り。メッセージは `NameError: undefined variable 'x' at LINE:COL`。何かが実行される前に、 そのコードに到達するかどうかに関わらず報告されます。どこにも束縛されて いない名前に加えて、宣言したスコープが閉じた後の読み取り(`{ ... }` ブロックの後、ループ本体の後(`for`の変数を含む)、`match`アームの後、 `if` / `cond` / `?:`の腕の後、`catch`の後)、同じ関数内で宣言より前に 読む場合(use-before-def、`inspect(a)`の後に`let a = 1`)、何も宣言して いないデコレータの呼び出し先も対象です。ループ本体は周回ごとに新しいスコープなので、読み取りより 下の宣言が先に実行されることはありません。 * **`ImmutableError`(再代入)** — ある変数の`let x`の後に、その間に `mut`宣言を挟まず裸の`x = v`を書くと、`ImmutableError: cannot reassign 'x' (declared without 'mut')`として報告されます。 それ以外は、宣言は見えるが未実行かもしれない、または静的に失敗を証明 できないため、*runtime*(`try`/`catch`で捕捉可能)のままです: * 外側の関数の変数を読む関数本体で、呼び出し時にその宣言がまだ実行 されていない場合(`let g = fn () { x }`、`g()`、`let x = 1`の順) * 条件付きで評価される位置にある宣言がスキップされた場合(`c && (let q = 5)` の後の`q`、または`if`の後ろのテスト、`if a { … } else if (let m = f()) > 0 { … }` の後の`m`で、`a`が真だったとき) * 未定義の`x`に対するcompound代入(`x += rhs`) * REPLの行(実行時にセッションに対して解決される) * 上記以外の`ImmutableError`(素の宣言で作られた名前、パラメータ、不変 束縛のフィールド、`let`より前に到達する書き込み(上に書かれたクロー ジャ内など))と、missing / unknown kwargs ### Assertion API `assert`キーワード / builtinは存在しません。テストはmatcher一族 (`assert_true` / `assert_eq`等、 §19と`docs/stdlib.ja.md`参照) を使います。productionの不変条件チェックにはObjectをthrow: ```culebra # doctest: skip if !cond { throw {kind: "AssertionError", message: "..."} } ``` 表明の制御フローは、ビルドフラグで消えるキーワードの裏に隠さず 明示的に書きます。 ### JIT サポート JITレーンは`throw` / `try` / `catch` / `defer`をVMと同一の セマンティクスでサポートします: * `throw` / `try` / `catch`はLLVMの`invoke` / `landingpad`と Itanium C++ personalityを使って関数境界を超えて伝播 * `defer`はdeferスタックにクロージャを登録し、scopeのcleanup landingpadが正常終了・`return`・throwの全経路で発火させます。 これはレキシカルスコープ(`{ defer { ... } ... }`)内・関数本体 直下・トップレベル直下のいずれでも同じで、未catchのthrowで 関数やプログラムを抜ける場合もcleanupは走ります 内部ランタイムエラー(`TypeError`, `ZeroDivisionError`など)は 両バックエンドでユーザの`try/catch`に構造化Error Objectとして 流れます — 上の "ランタイムエラー" 節を参照。 --- ## 16. 代数的エフェクト **エフェクト**とは、意味を呼び出し側ではなく、動的に外側を囲むコンテキストが 供給する操作です。コードは操作を`perform`し、コールスタックの上位に設置された `handle`ブロックが、その操作が何をするか — そして、`perform`したコードを **再開 (resume)** するか、何回するか — を決めます。ジェネレータ・例外・協調 スケジューリング・バックトラック探索を、それぞれ専用構文なしに、1つの機構で 表現できます。 エフェクトはparse時に通常のCulebraクラスと小さなランタイムへlowerされます。 そのため3つのバックエンドは同一のコードを走らせ、同一に振る舞います。 ### エフェクトの宣言 `effect fn`は、**操作**(本体なし)か **エフェクトフル関数**(本体あり)の いずれかを導入します: effect fn log(msg) # 操作: performされ、別の場所で処理される effect fn greet(name) { # エフェクトフル関数: 操作をperformしうる perform log("hi {name}") name } 操作宣言は操作名とパラメータを名付けるだけです。直接呼び出すのはエラーで、 必ず`perform`を通して到達しなければなりません。 エフェクトフル関数の本体は合成された計算クラスに下ろされるため、そこでの 裸の`self`はその内部オブジェクトしか指せず、参照できません(`self is not available inside an effect fn body`)。渡したい値はパラメータで渡すか、 外側で`let me = self`と束縛します。`handle { ... }`本体は制約されません — 書かれた場所に残るので、メソッドの中なら`self`はそのメソッドのreceiverの ままです。どちらもローカルを合成オブジェクトに持ち上げますが、ローカルは ただの変数として振る舞います。関数を保持するローカルはそのオブジェクトの メソッドではなく値なので、`f == f`は真のまま、`f()`と呼べば普通のfnの中と まったく同じくreceiverを渡さず、渡した先では後続の呼び出し方がreceiverを 決めます(`holder.f = f`のあとの`holder.f()`は`holder`を見ます)。 ### 操作の perform `perform op(args)`は現在の計算を中断し、`op`に対する最も内側の外側ハンドラへ 制御を移します: ```culebra effect fn ask() let x = handle { let n = perform ask() n + 1 } with ask(resume) { resume(10) } inspect(x) # => 11 ``` ### handle `handle { BODY } with op(params…, resume) { CLAUSE }`は、その間ハンドラを 設置した状態で`BODY`を走らせます。`BODY`(またはそれが呼ぶもの)が`op`を performすると、対応するclauseが、操作の引数を先頭のパラメータに、**継続**を 最後のパラメータ(上の`resume`)に束縛して走ります。継続は第一級の関数で、 渡した値でperform側のコードを再開します。引数なしの`resume()`は`nil`で再開します。 ハンドラは再開してもしなくてもよく、`resume`を呼ばなければperform側の残りの 計算は破棄されます: ```culebra effect fn fail() let r = handle { perform fail() "unreachable" } with fail(resume) { "aborted" } inspect(r) # => 'aborted' ``` #### 複数の操作と `return` clause 1つの`handle`は(操作ごとに)複数の`with` clauseと、`BODY`の正常完了値を 写す省略可能な`with return(v) { … }`を持てます: ```culebra effect fn get() effect fn put(v) mut cell = 0 let out = handle { let a = perform get() perform put(a + 5) perform get() } with get(k) { k(cell) } with put(v, k) { cell = v; k(nil) } with return(v) { "final={v}" } inspect(out) # => 'final=5' ``` `return` clauseは*正常*完了にのみ適用されます。ハンドラが(再開せず)abort した場合は、そのハンドラ自身の値が結果となり、`return`は走りません。 ### 複数回の再開 (multi-shot) 継続はmulti-shotです。ハンドラは`resume`を何回でも呼べ、各呼び出しは 中断点からperform側の残りの計算を独立に再実行します。これで非決定性 / バックトラックを表現できます: ```culebra effect fn choose(a, b) let all = handle { let x = perform choose(1, 2) let y = perform choose(10, 20) x + y } with choose(a, b, k) { [k(a), k(b)] } inspect(all) # => [[11, 21], [12, 22]] ``` 各forkは参照するheap値(配列・オブジェクト)を共有し(forkは浅いコピー)、 独立なスカラ状態はforkごとにコピーされます。 ### 動的スコープとエフェクトフル呼び出し ハンドラは**動的**スコープです。`perform`は字句的に最も内側ではなく、現在の コールスタック上で最も内側のハンドラへ届きます。ハンドルされた本体から エフェクトフルな`effect fn`を呼ぶとそこへ委譲され、その`perform`は同じ ハンドラに届きます: ```culebra effect fn ask2() effect fn double() { let n = perform ask2() n * 2 } inspect(handle { double() } with ask2(k) { k(21) }) # => 42 ``` ### 通常の関数とエフェクト 通常の(`effect`でない)関数も完全に参加できます。plainな関数の中の `perform`は動的ハンドラスタックへ直接ディスパッチされ、plain関数から effect fnを(逆方向も)呼べます — `.map(f)`のようなfirst-classな使い方も 透過します: ```culebra effect fn ask() fn greet() { # plain 関数がエフェクトを perform する let name = perform ask() "hi " + name } inspect(handle { greet() } with ask(k) { k("ana") }) # => 'hi ana' ``` これを支えるのは、各ハンドラ節のparse時分類です: * **末尾resume(tail-resumptive)** — `resume(v)`を節の最終文として ちょうど1回呼ぶ。節はperform地点でのただの関数呼び出しのように走り、 nativeコールスタックがそのまま継続になります。実用の大半(DI・state・ mock・logging)はこれです。継続をキャプチャしないので、末尾performを いくら連鎖してもスタックは一定で、節内の`defer`は節を抜ける時 — perform側のコードが続行する前 — に発火します(plain fn経由でも `effect fn`経由でも同一)。 * **abort** — 節が`resume`を一度も呼ばない。節の結果がその`handle`の 結果になります。perform地点から`handle`までのスタックは`defer`を 走らせながら巻き戻り、途中の`try/catch`からはこの巻き戻りは**見えません** (例外ではないため)。 * **full-control** — それ以外すべて(multi-shot、非末尾の`resume`、 `resume`の保存や受け渡し)。継続のキャプチャが必要で、それは`effect fn` 本体だけが支えられます。plain関数からfull-controlハンドラに届くと `EffectError`になります。`effect fn`マーカーが必要なのはこの場合だけです。 分類は意図的に保守側です: 単独の末尾呼び出し以外の`resume`の使い方は すべてfull-controlに分類されます(これは常に安全 — plainコードからの 直接ディスパッチが拒否されるだけです)。 ### 外側の束縛のキャプチャ エフェクトフルな`effect fn`の本体、または別の`handle`の本体の中に書かれた ネストした`handle`は、外側の計算のローカルを読み書きできます: ```culebra effect fn outer() effect fn inner() effect fn work() { let base = perform outer() handle { base + perform inner() } with inner(k) { k(100) } } inspect(handle { work() } with outer(k) { k(5) }) # => 105 ``` エフェクトとジェネレータは双方向に合成できます — ジェネレータ本体の中の `handle`式、エフェクト本体の中の名前付きジェネレータ関数: ```culebra effect fn scale() fn doubled() { yield handle { perform scale() * 2 } with scale(k) { k(10) } yield 7 } inspect(doubled().collect()) # => [20, 7] ``` ### 意味論と制約 * ハンドラの**ない**操作をperformすると`EffectError`を送出します。 * 通常のコードの`perform`やeffect fn呼び出しが **full-control** な ハンドラ節(multi-shot / 非末尾`resume`)に届くと`EffectError`を送出 します: 間に挟まるnativeフレームは継続としてキャプチャできません。 そのようなperformは`handle`の下で走る`effect fn`を経由してください。 * **effect fn / `handle`本体の中**では、`perform`は**文の位置**、または 無条件に評価されるオペランドでのみ対応します。短絡(`&&` / `||` / `??`)の オペランド、三項演算子のアーム、メソッドチェーンのレシーバ、制御フローの 条件の中の`perform`はparse時に(全バックエンドで対称に)拒否されます。 **plain関数の中**では`perform`はただの式で、位置の制約はありません。 * エフェクト本体内のエラーは、plain関数と同じく、失敗したコードを書いた 行と列で報告されます。ハンドラ不在の`perform`が送出する`EffectError` は、その`perform`の位置を`e.line`と`e.col`として持ちます。 * エフェクトとジェネレータは**合成できます**: `effect fn` / `handle`本体で 宣言した名前付きジェネレータ関数は動作し(本体のローカルも読めます)、 自己完結した`handle { … }`式はジェネレータ本体の中で動作します (yield式の中やループの中でも)。さらにジェネレータ本体の裸の`perform` は動的にディスパッチされます — それを走らせる`.next()`呼び出し時点で 設置されているハンドラに届きます。ジェネレータ本体の中に`effect fn`を 宣言することもできます。残る境界はparse時に(対称に)拒否されます: エフェクト本体の**裸の`yield`**(本体自体はジェネレータではありません — ネストしたジェネレータ関数に包んでください)。 * effect fn / `handle`本体の文レベルにある`defer`は、本体を**どの経路で 抜けても**発火します: 正常終了、`throw`のunwind、そしてabort — ハンドラ節がresumeせず値を返す場合も、plain fnの`perform`や ハンドラ跨ぎのabort *シグナル*がドライバをunwindする場合も。末尾resume 節・abort節がresumeせず`throw`で抜けた場合も中断中の本体のdeferは 走ります。走らないのは**full-control** 節のthrowだけ — `resume`を 保持している可能性があり、保持された継続のdeferはresumeされたforkの 完了時に走ります。制御フロー内にネストした`defer`や`defer`内の `perform`はparse時に拒否されます。 * エフェクト本体で宣言する**名前付き関数**は本体の文レベルに置く必要が あります(ネストした制御フローの中の宣言はparse時に拒否されます)。 ジェネレータにでき、本体のローカルも参照できます。 * ハンドラは **isolateローカル**です。あるスレッドで設置した`handle`は spawnしたisolateからは見えず、子の中の`perform`はその同じisolate内で 設置されたハンドラにのみ届きます(そこで未処理の操作は`EffectError`を送出し、 `join`で親へ伝播します)。これは「script値(継続もその一つ)はisolate境界を 越えない」という並行の不変条件に従います。 --- ## 17. メモリモデル ### 参照カウント すべてのヒープ型(`Array`, `Object`, `Function`、JIT内部では`Cell`, `Closure`)は参照カウントで管理されます。 * オブジェクトのrefcountは生成時に1になる。 * 新しい変数への束縛、配列へのpush、オブジェクトプロパティへの 格納でrefcountが増加する。 * 変数・プロパティ・配列スロットの上書きで、上書き前の値のrefcount が減少する。 * refcountが0になるとオブジェクトは解放され、保持していた参照の refcountが再帰的に減らされる。 ### サイクルコレクタ 純粋な参照カウントは循環(`a.c = a`)を回収できません。両方の バックエンドは全てのヒープrefcountオブジェクトを補助のサイクル コレクタに登録し、マーク&スイープを定期的(適応的な閾値 — 正確な スケジュールはbackend間で異なり、§25参照)およびプログラム終了時に 実行します。 * 対象: `Array`, `Object`, `Tuple`, `Set`、およびクロージャが捕獲 した環境(JITではさらに`Closure`, `Cell`)。両バックエンドとも あらゆるコンテナ循環形状を回収します(`Object`のプロパティマップ だけを経由する循環を含む)。 * `String`はrefcount管理されません。文字列バイトは`Runtime`ごとの slabから確保され、`String`は **traced-only** な値 です — refcountが0になることはないので、上記の トレーシングsweepが唯一の回収経路です。 循環データは通常通り取得・変更できます。外部のルートがなくなれば、 次回のサイクルでコレクタが解放します。 ### 循環の表示 循環の出力は再入位置で`{...}` / `[...]`となり、無限再帰を避けます (§8参照)。 ### 自動 drop(RAII) `Object`が引数0個の`Function`型プロパティ`drop`を持つ場合、 そのオブジェクトのproperties mapが解放される直前にランタイムが 自動的に呼び出します。`drop`契約は代入時点で検査され、非Function や引数のあるFunctionを束縛すると`type error`になります。 ```culebra # doctest: skip File.open = fn (path) { h = _native_open(path) { read: fn () { _native_read(h) }, drop: fn () { _native_close(h) }, # スコープ抜けで呼ばれる } } { let f = File.open('data.txt') process(f.read()) } # ここで f の drop が走る ``` **連鎖**: 親の`drop`が戻ると親のproperties mapがクリアされ、 各子の参照カウントが減少します。カウントが0になった子は自身の `drop`が呼ばれ、さらに連鎖します。親 → 子の順序が保証され、 同一親の兄弟同士はプロパティ宣言順(Array/Tuple/Setの要素は 要素順)に解放されます——どのbackend・プラットフォームでも同じです。 **例外**: `drop`内から送出された例外はstderrに記録して握り潰され、 残りの解放カスケードが続行します。 **構築の失敗**: クラスインスタンスは`C.new(...)`に入った時点で—— 引数が束縛される前、フィールド初期化子が走る前に——存在します。 したがってコンストラクタ呼び出しがどこでthrowしても(引数の不足・ 型違い、デフォルト式のthrow、フィールド初期化子、本体)、作りかけの インスタンスの`drop`は走ります。オーバーロードされた`new`の ディスパッチ失敗はインスタンスを作る前にthrowするので、この場合は 何もdropされません。 **再入するdrop本体**: `drop`本体はそのオブジェクト自身のフィールドを 変更してよく、自身と循環を成す参照を解放すること(`self.other = nil`) も含まれます。これは安全です — この再入的な解放が`drop`を2度目 発火させたりカスケードを壊したりすることはなく、本体が何を解放 しようと`drop`はちょうど1回だけ走ります。 本体が自分自身のオブジェクトへの参照に対して行ったことは、本体が 戻っても(throwしても)そのまま残ります。手放した参照は無くなります: `drop`が自分の循環を切ったオブジェクトは、他に保持するものが無くなり 次第、コレクションを待たずに回収されます。保存した参照 (`log.push(self)`)は、何が`drop`を発火させたかによらず、他の参照と 同じ参照です: 明示的な`x.drop()`でも、スコープの出口でも、最後の参照が 無くなったときでも、コレクションがオブジェクトを到達不能と判定した ときでも同じです。オブジェクトはそれを保持するもののために丸ごと 残り、drop済みのまま二度とdropされません。本体がオブジェクトから 取り出したもの(フィールドの値など)についても同じです。 **置き換えの順序**: スロットの中身を上書きするとき — `a[i] = v`、 `o.x = v`、`o[k] = v`、変数の再代入 — は、新しい値を先に格納し、 古い値をその後で解放します。したがって、その解放で走る`drop`から そのスロットを見ると、dropされている値ではなく新しい値が入って います。`drop`はそのコンテナを伸ばしても書き換えてもかまいません。 同じスロットに代入した場合は、`drop`自身の書き込みが残ります。 ```culebra mut box = [nil] box[0] = {drop: fn () { inspect(box[0]) }} box[0] = 'next' # => 'next' ``` **循環**: 参照循環は`drop`を妨げません。スコープに所有される循環 — そのスコープ下で生成され、スコープ離脱時に外部から到達不能 — のメンバーは、両バックエンドとも**そのスコープの離脱時**に新しい ものから順にdropされます: ```culebra let log = [] make_thing = fn (id) { {id: id, drop: fn () { log.push(id) }} } { let a = make_thing('a') let b = make_thing('b') a.other = b b.other = a } # 循環していても、ブロック離脱でここで両方 drop inspect(log) # => ['b', 'a'] ``` 所有スコープは「循環が最後にescapeした先」です: 関数の戻り値として 出ていった循環は、呼び出し側が捨てた後、*呼び出し側*のスコープ離脱で dropされます。兄弟 **closure** に捕捉されたresourceや、自身の closure slotを通って循環するresourceも、所有スコープの離脱で dropされます。どのレーンでも決定的な窓を外れる形が2つあります: resourceからの逆参照が無いclosure専用循環がそのresourceを片持ち する形(再帰local `fn`が捕捉するなど)と、**トップレベル**(離脱 しないスコープ)で直接捨てられた循環ガベージです。これらは **GCバックストップ**で発火します: コレクションが孤児化したresource の`drop`を厳密に1回 — メモリ回収の前、構造がまだ無傷のうちに — 走らせます(PythonのPEP 442と同じ精神)。バックグラウンドの コレクションはアロケーション数のしきい値で走り、根を保守的に 見つけるので、ある孤児に*いつ*到達するかは規定しません。 `GC.stat()`は参照カウントから根を決めるコレクションを走らせ、 その呼び出し時点で到達不能な孤児をすべてfinalizeします(全backend 共通)。同一コレクション内のfinalize順序は未規定です。**プログラム終了時**の コレクションは例外で、(下記のトップレベル束縛と同様に)終了まで 生き残った孤児はfinalizeされません — メモリは回収されますが `drop`は走りません(全backend共通)。決まった時点での クリーンアップが必要なら、最後の参照が解放される前に循環を手動で切る (`a.other = nil`など)、明示的に`.drop()`を呼ぶ、または所有元の スコープで`defer` (§15) を使ってください。 **非常に大きな循環**: 上記のスコープ離脱カスケードには、通常の プログラムを同期的に保つよう調整されたサイズ上限があります。1つの スコープが一度に大量の循環所有オブジェクト(通常コードにある程度の 数ではなく、数千単位)をdropする場合、そのスコープの離脱時に全部は 解決されないことがあります — 超過分はGCバックストップへ委ねられ、 上記と同じ未規定順序・PEP 442型のfinalizeを受けます。失われることは なく遅れるだけで、通常のプログラムがこのサイズに達することはありません。 **束縛スコープの注意**: `drop`は、オブジェクトが**ブロックスコープ** の束縛に格納され、かつ`drop`関数が**ファクトリ関数**で生成された 場合(closureが外側スコープではなくファクトリのcall envを捕捉する) に確実に起動します。これはチュートリアル §5の「closureでオブジェクト」 イディオムと一致しており、自然に機能します: ```culebra make_thing = fn () { {drop: fn () { inspect('cleaned') }} # make_thing の env を捕捉 } { let t = make_thing() # ブロックスコープ束縛 } # ここで drop が走る ``` `drop`を持つオブジェクトを**トップレベル**で束縛すると、どの バックエンドでもプログラム終了まで`drop`を実行せずに生き残ります。 どのバックエンドもトップレベルスコープの解放時にdropを抑制し、 **未catchのエラー**による終了も同じ扱いです — エラーが通過した スコープは内側から順に決定的に解放されます(各スコープの`defer`の あとにそのスコープの束縛)が、トップレベル自身の束縛はdropされずに 残ります。traitの既定メソッドが捕獲したオブジェクトのように、他の 何かが終了まで保持しているオブジェクトも同じです: プログラムが 終わった後に`drop`が走ることはありません。スクリプト全体にわたる リソースは、どちらの終了経路でも走る`defer`(§15)や明示的な クリーンアップを推奨します。 **JIT**: `--jit`でも自動dropはVMと同じタイミングで 発火します — スコープ離脱時、循環メンバーを含み、closure保持の形も 同様(バックストップ限定の形は上記参照)。トップレベルの孤児は 両方ともGCバックストップ行きで、トップレベルの*束縛*はプログラム 終了時にdropされずにleakしたまま。well-known プロパティの契約(`drop` / `iter` / `has_next` / `next`に束縛する `Function`は引数を取らないこと)も両バックエンドで代入時に強制されます。対象は すべての書き込み面: リテラルプロパティ、計算添字キー(`o[k] = v`)、 nativeビルダー(`JSON.parse`等)、channel転送の受信側で再構築される 値、class宣言(methodテンプレートがwell-known名を非適合な 関数 — arity違い、またはoverloadセット — に束縛するclassは宣言 実行時に`DropContractError`)、そしてtraitのデフォルト実装(適合 する全インスタンスに届くため、`trait`宣言の位置で検査されます)。 bodyを持たないtraitメソッドは要求を宣言するだけで値を束縛しないので 検査対象外です。対象スロットがimmutableの場合は 契約検査より`ImmutableError`が優先され、検査に失敗しても旧値は そのまま残ります。`static`メンバーは名前空間であってインスタンス プロトコルの一部ではありません: `drop`という名前のstatic(arity 不問)はただの関数で、契約検査されず、自動呼び出しもされず、 `C.drop()`と書けば他のstaticと同じように呼ばれます。明示的な `x.drop()`を1回だけに抑えるガードはインスタンスのものです。 クラスオブジェクト・enum・名前空間では`drop`も他と変わらないメンバー名で、 無ければ、存在しないメンバーを呼んだときと同じエラーになります。 ```culebra class Pool { static drop() { 'pool emptied' } drop() { inspect('connection closed') } } inspect(Pool.drop()) # => 'pool emptied' let c = Pool() c.drop() # => 'connection closed' c.drop() # すでにdrop済み: 何も走らない ``` この契約は関数についてのものです。`Function`でない値は、たまたま その名前を使っているだけのデータで、他のプロパティと同じように 束縛できます — プログラムが決められない文字列は、キーも自分で決めます: ```culebra let page = JSON.parse('{"items": [1, 2], "next": "/page/2", "drop": false}') inspect(page.next) # => '/page/2' inspect(page.keys()) # => ['items', 'next', 'drop'] ``` こうした`Object`はどのプロトコルのメンバーでもありません。消えるときに 何も走らず、`for`は他の`Object`と同じくキーを順にたどります。 --- ## 18. 組み込み型のメソッド 以下のメソッドは言語の一部であり、対応する型の任意の値に対して `import`なしで利用できます。ユーザコードで差し替えることはできません (`String`にはプロパティストアがないため;`Array`/`Object`では 同名のユーザ定義プロパティが優先され、ビルトインはフォールバック)。 グローバルなビルトイン関数(`inspect`, `to_string`, `Math.*`, `IO.*`、 matcher一族`assert_true` / `assert_eq`等)は [`docs/stdlib.ja.md`](stdlib.ja.md) で別途規定します。 **Well-knownメソッド(プロトコル).** いくつかのメソッド名はランタ イムが特別に認識し、プレーンな`Object`でも言語機能に参加できる ようにします。両バックエンドで名前ベースで検査されます: | メソッド | 役割 | 定義場所 | |---|---|---| | `__add__`, `__sub__`, `__mul__`, `__div__`, `__mod__`, `__pow__`, `__matmul__`, `__neg__`, `__eq__`, `__lt__`, `__le__` | 演算子オーバーロード | §10 | | `__str__` | カスタム表示形式 | §10 | | `drop` | RAIIクリーンアップフック | §17 | | `iter`, `next` | イテレータプロトコル | §18.5 | 記法: * 型注釈は §14に従います。`Any`は任意の値。 * 位置インデックスは0起点。負のインデックスは末尾からの数え (`-1`が最後)。 * `String`の長さ・インデックスは**バイト**単位です(Go流)。 Unicode単位の走査は明示的に行います — `iter()` / `code_points()` がスカラー、`graphemes()`が書記素クラスタです。 ### 18.1 文字列メソッド すべての文字列メソッドは`String`または`StringView`をレシーバ に取り、引数も`StringLike`の場所では両方を受ける。戻り値は 新規の値で、レシーバは変更しません。 | シグネチャ | 説明 | |-------------------------------------------------|--------------------------------------| | `s.size() -> Long` | バイト長 | | `s.empty() -> Bool` | `size() == 0`か | | `s.presence() -> String \| StringView \| Nil` | 非空なら`s`をそのまま、空なら`nil` — `??`/`?.`と組み合わせて「あれば使う」慣用句に(`x.presence() ?? default`) | | `s.upper() -> String` | 大文字化。Unicode全体のcase mapping(UAX #21)なので長さが変わりうる: `'ß'.upper()`は`'SS'` | | `s.lower() -> String` | 小文字化。mappingの規則は同じ | | `s.capitalize() -> String` | 先頭の文字をtitlecaseに、残りを小文字に(Python/Rubyの`capitalize`) | | `s.title() -> String` | 各単語の先頭文字をtitlecaseに、残りを小文字に。単語境界はUAX #29なので`"o'neil".title()`は`"O'neil"`(アポストロフィは単語を区切らない) | | `s.normalize(form: StringLike = "NFC") -> String` | Unicode正規化: `"NFC"` / `"NFD"` / `"NFKC"` / `"NFKD"`。合成済みと分解済みの同一テキストは`normalize()`後に等しくなる。等価判定自体はバイト単位(§8)なので自動では正規化しない。他の`form`は`ValueError` | | `s.eq_ignore_case(other: StringLike) -> Bool` | 大文字小文字を無視した一致(full case folding)。`'Straße'.eq_ignore_case('STRASSE')`は`true`。正準等価は意図的に含めない — それが要るなら両辺を`normalize()`する | | `s.reverse() -> String` | 書記素クラスタ(UAX #29)単位で反転するので、絵文字のZWJ列や基底+結合文字の組は壊れない。新しいStringを返す — in-placeで変更して`nil`を返す`Array.reverse()`とは異なる | | `s.repeat(n: Long) -> String` | `n`個連結。`n == 0`は`""`、負の`n`は`ValueError`。確保できない大きさの結果も`ValueError` | | `s.truncate(max: Long, ellipsis: StringLike = "...") -> String` | `s`が`max`バイト以内ならそのまま。超えるなら結果(本文+`ellipsis`)がちょうど`max`バイトになるよう切る。`slice`と同じくバイト単位なので、切断点でマルチバイトscalarが分断されうる。`max < 0`、または`ellipsis`が収まらない小ささは`ValueError` | | `s.trim() -> String` | 前後のUnicode空白(`White_Space`プロパティ。` `/`\t`/`\n`/`\r`だけでなくNBSPやU+3000も含む)を除去 | | `s.trim_start(chars: StringLike = "") -> String` | 先頭側を除去。引数なし → 空白、`chars` → その集合の先頭scalar(範囲非対応) | | `s.trim_end(chars: StringLike = "") -> String` | 末尾側を除去。例`s.trim_end("\n")` | | `s.tr(from: StringLike, to: StringLike) -> String` | scalar単位の変換(文字リスト形式 — `a-z`範囲や`^`は非対応)。`s`の各scalarが`from`にあれば`to`の同位置scalarに置換。`to`が短ければ末尾scalarを繰り返し、空`to`は削除。`s.tr("0123456789", "0123456789")` | | `s.split(sep: StringLike, limit: Long = 0) -> Array` | `sep`の出現ごとに分割。`sep`が空なら`[s]`。要素は1個のsourceを共有。`limit`は返す要素数の上限で、最後の要素が残り全部を持つ(`'a.b.c'.split('.', 2)`→`['a', 'b.c']`)。`0`は無制限、負値は`ValueError` | | `s.rsplit(sep: StringLike, limit: Long = 0) -> Array` | 分割結果は同じだが`limit`を右から埋める: `'a.b.c'.rsplit('.', 2)`は`['a.b', 'c']`。返る配列は左から右の順のままで、`limit`なしなら`split`と一致 | | `s.split_whitespace() -> Array` | Unicode空白の連なりで分割し、両端に空要素を作らない: `' a b '.split_whitespace()`は`['a', 'b']`(`split(' ')`は空要素を残す) | | `s.split_once(sep: StringLike) -> Tuple \| Nil` | **最初**の`sep`の前後2つに分割。`sep`がなければ`nil`。`key=value`のパースが欲しい形で、値に`sep`が含まれても壊れない(`split`は壊れる)。空の`sep`は分割しないので`nil` | | `s.rsplit_once(sep: StringLike) -> Tuple \| Nil` | **最後**の`sep`の前後2つに分割。なければ`nil` | | `s.split_iter(sep: StringLike) -> Iterator` | `split`の遅延版。巨大入力で`.take(n)`する場合の早期終了に | | `s.lines() -> Array` | `\n` / `\r\n` / `\r`で分割。終端記号は落とし、末尾の終端は空要素を生まないので`"a\nb\n".lines()`は`["a", "b"]` — `File.lines()`と同じ行境界。`split`と同じくeager | | `s.replace(pat: String \| Regex, repl: String \| Function) -> String` | **全**出現を置換。連鎖可。`pat`が`String` → リテラル置換、`Regex`(`re'…'`含む)→ 正規表現置換で`repl`は`$1` / `$`テンプレートか`fn (Match) -> String`。UFCSで呼ぶstdlibヘルパ — `s.replace(p, r)`は`replace(s, p, r)`。 | | `s.contains(sub: StringLike) -> Bool` | `sub`がどこかに現れるか。空の`sub`は`true` | | `s.count(sub: StringLike) -> Long` | `sub`の重ならない出現数。`"aaaa".count("aa")`は`2`。空の`sub`は`0`(`split("")`がレシーバ全体を返すのに合わせた)。リテラルのみ — パターンはコンパイル済み`Regex`側が数える | | `s.starts_with(prefix: StringLike) -> Bool` | `prefix`で始まるか | | `s.ends_with(suffix: StringLike) -> Bool` | `suffix`で終わるか | | `s.index_of(sub: StringLike, start: Long = 0) -> Long` | `start`以降で最初に現れる`sub`のバイトオフセット。なければ`-1`(`Array.index_of`と同じ「-1 = 見つからない」規約)。負の`start`は`slice`の添字と同じく末尾から数える | | `s.last_index_of(sub: StringLike) -> Long` | 最後に現れる`sub`のバイトオフセット。なければ`-1` | | `s.strip_prefix(prefix: StringLike) -> String` | その接頭辞ちょうどを取り除く。なければ`s`のまま。scalarの**集合**を削る`trim_start(chars)`と違い、接頭辞全体に1回だけマッチする | | `s.strip_suffix(suffix: StringLike) -> String` | その接尾辞ちょうどを取り除く。なければ`s`のまま | | `s.replace_first(pat: String \| Regex, repl: String \| Function) -> String` | `replace`と同じだが最初の1個だけ置換。`pat`/`repl`の規則も同じ | | `s.is_digit() -> Bool` | `s`が非空で、かつ全scalarが10進数字(General_Category `Nd`。全角の`'123'`も該当)か | | `s.is_alpha() -> Bool` | 非空で、かつ全scalarが`Alphabetic`か | | `s.is_alnum() -> Bool` | 非空で、かつ全scalarが`Alphabetic`または数字か | | `s.is_space() -> Bool` | 非空で、かつ全scalarが`White_Space`か | | `s.is_ascii() -> Bool` | 非空で、かつ全バイトが`0x80`未満か。空はここでも`false` — `is_*`系は例外を作らず全部同じ規則にしてある | | `s.slice(start: Long, end: Long) -> StringView` | `[start, end)`の部分ビュー (レシーバのバイトを借用)。負の値は末尾から、`start`は`[0, size()]`、`end`は`[start, size()]`にクランプ | | `s.view() -> StringView` | レシーバのバイトをaliasするview (コピーなし) | | `s.to_string() -> String` | 所有権付き`String`にmaterialize (String受信はno-op、StringView受信はコピー) | | `s.iter() -> Iterator` | UTF-8をスカラー単位でwalkし**1スカラー`StringView`** をyield。`for c in s { ... }`の内部実装。不正バイトは1バイト部分view | | `s.code_points() -> Iterator` | **Unicodeスカラー値**を`Long`(`U+0000`–`U+10FFFF`)としてyieldする遅延イテレータ。`iter`の毎反復アロケが無駄になる数値・範囲・分類処理向け。不正バイトは生バイト値(0–255) | | `s.graphemes() -> Iterator` | **Extended Grapheme Cluster**(UAX #29)を1つずつyield。1ステップが1ユーザー知覚文字(絵文字ZWJシーケンス等は1要素にまとまる) | | `s.words() -> Iterator` | **UAX #29の単語境界**でセグメントに分け、`(text, is_word)`のペアを1つずつyield。境界と境界の間は全部1セグメントになるので、空白や句読点の並びもセグメントとして出てくる。`is_word`はそのセグメントに英数字が1文字でも含まれていれば`true`。単語だけ欲しいときは`s.words().filter(\|(_, w)\| w).map(\|(t, _)\| t)`のようにfilterで絞り込む | | `s.sentences() -> Iterator` | **UAX #29の文境界**でセグメントに分けて1つずつyield。1ステップが1文になり、末尾の空白は直前の文にそのままくっつく | | `s.bytes() -> Iterator` | レシーバの**生UTF-8バイト**を`Long`(`0`–`255`)として1バイトずつyield — `code_points`と違いデコードしない。エンコーディングそのものが欲しい場面向け(ハッシュ計算、tokenizerの語彙、ワイヤーフォーマット等) | | `String.from_code_point(cp: Long) -> String` | `code_points()`の逆演算: Unicodeスカラー値1つを受け取り1文字の`String`を返す。`cp`が`U+10FFFF`超過またはサロゲート範囲`U+D800`–`U+DFFF`の場合`ValueError`(`\u`/`\U`リテラルエスケープ §4.1がパース時に拒否するのと同じ境界) | | `String.from_code_points(cps: Array) -> String` | `code_points()`の複数形の逆演算: Unicodeスカラー値の`Array`を受け取り`String`を返す。各要素は`from_code_point`と同じゲートを通るので`String.from_code_points([cp]) == String.from_code_point(cp)`。要素が`Long`でなければ`TypeError`、範囲外なら`ValueError` | | `String.from_bytes(bytes: Array) -> String` | `bytes()`の逆演算: 生バイト値(`0`–`255`)の`Array`を受け取り`String`を返す。**UTF-8の検証は行わず**、不正入力でも例外を投げない: culebraの`String`は不正なUTF-8を許容する(`iter()`と同様)ため、不正なバイト列を含むあらゆる`String`について`String.from_bytes(s.bytes().collect()) == s`が成立する。要素が`Long`でなければ`TypeError`、範囲外(`0`–`255`の外)なら`ValueError` | | `String.pack(type: String \| Class, values: Array, endian: String = "little") -> String` | `values`の各要素を`type`の固定幅の値1つ(`@packable`クラスならレコード1件)として、順に並べたバイト列。二進のファイル形式や通信で使うバイト列を作る。下の[バイナリデータ](#バイナリデータ-stringpack--stringunpack)を参照 | | `String.unpack(type: String \| Class, data: StringLike, at: Long = 0, count: Long? = nil, into: Array? = nil, endian: String = "little") -> Array` | `pack`の逆演算: `data`のバイト位置`at`から`type`の要素を読む。読む個数は`count`、`into`の要素数、どちらも無ければ残りのバイト全部 | | `String.pack_size(type: String \| Class) -> Long` | `type`の要素1つが何バイトか。`"u16"`なら`2`、レコードならフィールドの幅の合計 | ```culebra # 'é' は UTF-8 で 2 バイトなので 'café' は 5 バイト inspect('café'.bytes().collect()) # => [99, 97, 102, 195, 169] inspect(String.from_code_point(233)) # => 'é' inspect(String.from_bytes([99, 97, 102, 195, 169])) # => 'café' inspect(String.from_code_points([99, 97, 102, 233])) # => 'café' ``` #### バイナリデータ: `String.pack` / `String.unpack` `String.pack`は数値の`Array`を、二進のファイル形式や通信で使うバイト列に 変換する。要素1つが固定幅の値1つになる。`String.unpack`はその逆で、 バイト列から数値を読み出す。値の種類は`type`で指定する: | `type` | バイト数 | 要素 | |---|---|---| | `"u8"` / `"i8"` | 1 | `Long`、`0`–`255` / `-128`–`127` | | `"u16"` / `"i16"` | 2 | `Long`、`0`–`65535` / `-32768`–`32767` | | `"u32"` / `"i32"` | 4 | `Long`、`0`–`4294967295` / `-2147483648`–`2147483647` | | `"i64"` | 8 | 任意の`Long` | | `"f32"` | 4 | `Float`(IEEEの単精度)。`pack`は`Math.f32`と同じ規則で丸め、`Long`も受け付ける | | `"f64"` | 8 | `Float`。`pack`は`Long`も受け付ける | | `"bool"` | 1 | `Bool`。`1` / `0`で書き、読むときは0以外を`true`とする | `"u64"`は無い。上半分が`Long`に収まらないためで、同じ64ビットは`"i64"`で 扱える。`endian:`はバイトの順序で、`"little"`(既定。x86やARMのメモリ上の 順序で、多くのファイル形式もこちら)か`"big"`(ネットワークで使う順序)。 `pack`は要素を1つずつ検査する。型の違う要素は`TypeError`、型の範囲外の値は `ValueError`になる(下位ビットだけ残すような切り詰めはしない)。どちらの メッセージにも要素の位置が入る。`String.pack("u8", a)`は `String.from_bytes(a)`と同じ結果になる。 `unpack`はバイト位置`at`から`count`個を読む。`count`を省くと残りのバイトを 全部読み、そのバイト数が値の幅で割り切れなければ`ValueError`。`into:`を 渡すと、新しい`Array`を作らずに既存の`Array`へ上書きし(読む個数はその 要素数)、その`Array`自身を返す。他の場所からも参照されている配列を 書き換えるときに使う。`at`が`data`の外にあるとき、または末尾を越えて 読もうとしたときは`IndexError`。`count`と`into`を両方渡すと`ValueError`。 `data`には`StringView`も渡せるので、`slice`した部分をコピーせずに読める。 ```culebra let b = String.pack("u16", [1, 256, 65535]) inspect(b.bytes().collect()) # => [1, 0, 0, 1, 255, 255] inspect(String.unpack("u16", b)) # => [1, 256, 65535] inspect(String.pack("u32", [1], endian: "big").bytes().collect()) # => [0, 0, 0, 1] # 個数を u32 で書き、続けてその個数の i16 のサンプルを置く let data = String.pack("u32", [3]) + String.pack("i16", [-1, 0, 1]) let n = String.unpack("u32", data, count: 1)[0] inspect(String.unpack("i16", data, at: 4, count: n)) # => [-1, 0, 1] let samples = [0, 0, 0] String.unpack("i16", data, at: 4, into: samples) inspect(samples) # => [-1, 0, 1] ``` `type`には`@packable`クラスも渡せる。そのときは要素1つがレコード1件になり、 フィールドを宣言した順に、詰め物を入れずに、`endian:`の順序で並べる。 ファイルのヘッダ、通信のパケット、表の1行などに使う。`unpack`はレコードを `new`を呼ばずに作る。レコードの状態はフィールドがすべてだからである。 レコードに持たせられるフィールドの種類と、それぞれの読み書きのされ方は、 標準ライブラリの[バイト列としてのレコード](stdlib.ja.md#バイト列としてのレコード-stringpack)を参照。 `String.pack_size(type)`は要素1つのバイト数を返す。ファイルやソケットから ちょうど1件分だけ読むときに使う。 ```culebra @packable class Idx { magic: UInt32 count: UInt32 } let raw = String.pack("u32", [2049, 60000], endian: "big") let h = String.unpack(Idx, raw, count: 1, endian: "big")[0] inspect([h.magic, h.count]) # => [2049, 60000] inspect(String.pack_size(Idx)) # => 8 ``` #### StringView `StringView`は所有権付き`String`のバイト列を借用するview。 `slice` / `split` / `view` / `iter` / `graphemes` / `sentences`は per-callコピーを避けるため`String`ではなく`StringView`を返す (`words`も`(text, is_word)`のペアの中で`StringView`をyieldする)。 viewはsource を生かし続ける (shared ownership) ので、viewを作ったtempが消えても viewは有効: ```culebra let v = 'hello world'.slice(6, 11) inspect(v) # => 'world' # 等価比較はバイト単位なので view と String は等しい: inspect(v == 'world') # => true inspect(type_of(v)) # => 'StringView' # to_string() は所有権を持つ String を実体化する: inspect(v.to_string()) # => 'world' ``` 所有権付き`String`が必要な場面 (data structureに格納、長寿命の 関数から返す等) では`.to_string()`を呼ぶ。多くのAPIが `StringLike`で宣言されており、どちらのflavorもそのまま受ける。 **既知の制約** (cycle B): `StringView`に対して`.contains()` / `.starts_with()` / `.ends_with()`等を呼ぶと呼び出しごとに一時的な cstrコピーが発生する。これは他の`String`と同様トレーシングコレクタ が回収する(§17)のでleakはしないが、viewを大量のhot-loopで 通す場合は避けられるallocationが発生し続けるので、1度 `.to_string()`でmaterializeしておくとよい。(`String`と `StringView`のObject key正規化は制約ではない — §18.3参照。) ```culebra inspect('hello'.size()) # => 5 inspect('HeLLo'.lower()) # => 'hello' inspect(' hi '.trim()) # => 'hi' inspect('a,b,c'.split(',')) # => ['a', 'b', 'c'] inspect('hello'.slice(1, 4)) # => 'ell' inspect('hello'.slice(-3, -1)) # => 'll' inspect('hello world'.truncate(8)) # => 'hello...' inspect(''.presence() ?? 'default') # => 'default' # 同じ文字列の3つのビュー inspect('café'.size()) # => 5 inspect('café'.code_points().count()) # => 4 inspect('café'.graphemes().count()) # => 4 # 絵文字 ZWJ シーケンス: 5 スカラー、1 grapheme inspect('👨‍👩‍👧'.code_points().count()) # => 5 inspect('👨‍👩‍👧'.graphemes().count()) # => 1 # code_points なら数値演算が自然 upper = 'Hello World'.code_points().filter(fn (cp) { cp >= 65 && cp <= 90 }).count() # 2 ('H', 'W') # 単語セグメント(UAX #29): 空白や句読点も1セグメントとして出て # くるので、本当の単語だけ欲しければ is_word で filter する inspect('Hello, world!'.words().collect()) # => [('Hello', true), (',', false), (' ', false), ('world', true), ('!', false)] inspect('Hello, world!'.words().filter(|(_, w)| w).map(|(t, _)| t).collect()) # => ['Hello', 'world'] # 文セグメント(UAX #29): 末尾の空白は直前の文にそのままくっつく inspect('Hello. World.'.sentences().collect()) # => ['Hello. ', 'World.'] ``` これらのイテレータはすべて **UTF-8を逐次decode** します。各 `next()`ステップで必要なぶんだけバッファを進めるので、数MBの 文字列に対して`s.graphemes().take(3).collect()`としても最初の 3クラスタを解決するのに必要なバイト数しか触りません(`words`と `sentences`も同じwindow decode実装を共有し、境界の判定規則だけが 違います)。イテレータごとの内部状態(decodeオフセットとlookahead バッファ)は独立しているため、同じ文字列から派生した複数の イテレータを並走させても互いに干渉しません。 **JIT**: `iter` / `code_points` / `graphemes` / `words` / `sentences`はiterator オブジェクトを返し、JITはユーザー定義イテレータと同じプロトコル 経路で駆動します。セマンティクスはVMと一致。性能は ステップあたりのクロージャ呼出コストが支配的なので、速度重視で ArrayやStringスカラーを回す場合は`for c in s { ... }` (native loop)を推奨。 ### 18.2 配列メソッド **破壊的**と記したメソッドはレシーバを直接変更し、`nil`を返します (`pop`と`remove_at`だけは取り除いた要素を返します)。それ以外は 新しい`Array`を返してレシーバを変更しません。 コールバックがレシーバを変更してもよく、その場合の走査は生きた配列に 追従します。各ステップの前にサイズを読み直すので、走査は配列の終端で 終わります。縮めれば走査は早く終わり、伸ばせば走査は続きます。これは `for x in a`(§12)と同じ規則で、`map` / `filter` / `for_each` / `reduce` / `find` / `any` / `all` / `flat_map`に適用されます。 ```culebra mut a = [1, 2, 3, 4] mut seen = [] a.for_each(fn (x) { seen.push(x) a.pop() }) inspect(seen) # => [1, 2] ``` | シグネチャ | 説明 | |---------------------------------------------|--------------------------------------| | `a.size() -> Long` | 要素数 | | `a.empty() -> Bool` | `size() == 0`か | | `a.presence() -> Array \| Nil` | 非空なら`a`をそのまま(コピーでなく同じArray)、空なら`nil` — `??`/`?.`と組み合わせて「あれば使う」慣用句に(`xs.presence()?.join(",") ?? default`) | | `a.push(x: Any) -> Nil` *(破壊的)* | 末尾に追加 | | `a.pop() -> Any` *(破壊的)* | 末尾を取り除いて返す。空なら`nil` | | `a.extend(other: Array) -> Nil` *(破壊的)* | `other`の全要素を末尾に追加。`a.extend(a)`は呼び出し時点の`a`の要素を追加する。`Tuple` / `Set`を対象にしたい場合はスプレッド(§9)を使う | | `a.insert(i: Long, x: Any) -> Nil` *(破壊的)* | 位置`i`に`x`を挿入し、以降を右へずらす。負の`i`は`a[i]`と同じく末尾から数える。`i == a.size()`は末尾追加スロット。範囲外は`IndexError` | | `a.remove_at(i: Long) -> Any` *(破壊的)* | 位置`i`の要素を取り除いて返し、以降を左へずらす。負の`i`は末尾から数える。`insert`と違い`i == a.size()`は範囲外で`IndexError`。空配列ではどの`i`でも`IndexError` | | `a.get(i: Long, fallback: Any) -> Any` | 位置`i`の要素(負の`i`は`a[i]`と同じく末尾から数える)、範囲外なら`fallback`。読み取り専用で、決して例外を投げない | | `a.slice(start: Long, end: Long) -> Array` | 浅い部分配列`[start, end)`。`String.slice`と同じクランプ規則 | | `a.join(sep: String) -> String` | 要素を`to_string`で変換(文字列は引用符なし)し`sep`で連結 | | `a.contains(v: Any) -> Bool` | いずれかの要素が`v == elem` — `==`の規則そのままで、要素の`__eq__` / `eq`も効く | | `a.index_of(v: Any) -> Long` | `v`と等しい最初の要素のインデックス。なければ`-1` | | `a.reverse() -> Nil` *(破壊的)* | 配列をin-placeで反転 | | `a.map(f: Function) -> Array` | 各要素に`f(x)`を適用した新配列。`f`は1引数を受け取る | | `a.filter(f: Function) -> Array` | `f(x)`が真となる要素だけの新配列。`f`は1引数を受け取る | | `a.for_each(f: Function) -> Nil` | 各要素に`f(x)`を適用(副作用目的)。`f`は1引数を受け取る | | `a.reduce(init: Any, f: Function) -> Any` | 畳み込み: `init`から始めて`acc = f(acc, x)`を適用し最終`acc`を返す。`f`は2引数を受け取る | | `a.find(f: Function) -> Any` | `f(x)`が真となる最初の要素。なければ`nil`。`f`は1引数を受け取る | | `a.any(f: Function) -> Bool` | いずれかの要素で`f(x)`が真なら`true`、そうでなければ`false`。`f`は1引数を受け取る | | `a.all(f: Function) -> Bool` | すべての要素で`f(x)`が真なら`true`(空配列でも`true`)、そうでなければ`false`。`f`は1引数を受け取る | | `a.flat_map(f: Function) -> Array` | 各要素に`f(x)`を適用し、その結果配列を連結。各`f(x)`は`Array`を返す必要あり。`f`は1引数を受け取る | | `a.sum() -> Long \| Float` | 全要素(`Long`または`Float`)の合計。全要素が`Long`の間は`Long`、一つでも`Float`があれば`Float`になる。空 → `0` | | `a.product() -> Long \| Float` | 全要素の積。昇格規則は`sum`と同じ。空 → `1` | | `a.min() -> Any` | 最小の要素。要素は`<`と同じ規則(§5)で比較するので、数値・String・Tuple・`cmp` / `__lt__`を持つObjectのどれでもよく、`<`が拒む対は例外になる。返すのは要素そのものなのでその型が保たれ、同値なら先に現れた方を返す。空配列では例外 | | `a.max() -> Any` | 最大の要素。規則は`min`と同じ | | `a.min_by(f: Function) -> Any` | キー`f(x)`が最小の要素。`f`は1引数を受け取り、キーは`<`と同じ規則で比較する(Tupleのキーなら複数の項目で一度に比べられる)。キーは各要素につき1回だけ計算し、同値なら先に現れた方を返す。空配列では例外 | | `a.max_by(f: Function) -> Any` | キー`f(x)`が最大の要素。規則は`min_by`と同じ | | `a.to_set() -> Set` | 要素を初出順に持つ新しい`Set`(重複は除去)。コレクションから`Set`を作る手段で、値から作るのは`Set(a, b)`。ハッシュ不可の要素は例外 | | `a.to_object() -> Object` | `(key, value)`タプル列から新しい`Object`を作る。`Object.iter()`の逆なので、テーブルを`mut` + ループでなく式として組める。キーは初出順を保ち、重複したキーはその位置のまま値だけ上書き(後勝ち)。エントリは`group_by`と同じく **mutable**(実行時に挿入したキーの既定)。要素が2要素タプルでなければ`TypeError`、ハッシュ不可のキーは通常どおり例外 | | `a.group_by(f: Function) -> Object` | `f(x)`をキーに要素をArrayへ振り分ける(キーは初出順)。`f`は1引数を受け取り、ハッシュ可能なキーを返す必要あり | | `a.partition(p: Function) -> Tuple` | 1パスで`(条件を満たす, 満たさない)`に分割し、両方とも順序を保つ。`p`は1引数を受け取る。分配束縛できる: `let (yes, no) = xs.partition(p)` | | `a.unzip() -> Tuple` | `(a, b)`ペアを`(Array, Array)`に分割する — `zip`の逆。各要素は2要素タプルか`{first, second}`形のObjectのどちらかを受け付け、それ以外は`TypeError`。分配束縛できる: `let (xs, ys) = pairs.unzip()` | | `a.sort(reverse: Bool = false) -> Nil` *(破壊的)* | 自然順でin-place安定ソート。要素は`<`と同じ規則で比較し、Objectの`__lt__` / `cmp`を尊重する(`Path`配列もソート可)。比較不能な要素はthrow(このとき配列は元のまま)。キーワード専用`reverse: true`で降順(安定のまま) | | `a.sorted(reverse: Bool = false) -> Array` | `sort`の非破壊版。新しいソート済みArrayを返し、受け手は不変=チェーン可(`xs.sorted().join(",")`)。`reverse: true`で安定降順 | | `a.sort_by(key: Function, reverse: Bool = false) -> Nil` *(破壊的)* | `key(x)`を比較キーとして昇順にin-placeで安定ソート。`key`は1引数を受け取る。キーは`<`と同じ規則で比較するので、数値・String・Tuple(複数の項目で一度に並べる: `\|n\| (n.bar, n.beat)`)・`cmp` / `__lt__`を持つObjectのどれでもよい。キーワード専用`reverse: true`で降順(安定のまま) | | `a.sorted_by(key: Function, reverse: Bool = false) -> Array` | `sort_by`の非破壊版。新しいソート済みArrayを返し、受け手は不変=チェーン可(`xs.sorted_by(f).join(",")`)。`reverse: true`で安定降順 | ```culebra mut a = [1, 2, 3] a.push(4) inspect(a.pop()) # => 4 a.extend([9, 8]) inspect(a) # => [1, 2, 3, 9, 8] a.insert(0, 0) inspect(a) # => [0, 1, 2, 3, 9, 8] inspect(a.remove_at(-1)) # => 8 inspect([1, 2] + [3]) # => [1, 2, 3] inspect([10, 20, 30, 40].slice(1, 3)) # => [20, 30] inspect(['a', 'b', 'c'].join('-')) # => 'a-b-c' inspect([1, 2, 3].contains(2)) # => true inspect([10, 20, 30].index_of(99)) # => -1 inspect([].presence() ?? 'default') # => 'default' inspect([1, 2, 3].map(fn (x) { x * x })) # => [1, 4, 9] inspect([1, 2, 3, 4].filter(fn (x) { x % 2 == 0 })) # => [2, 4] inspect([1, 2, 3, 4].reduce(0, fn (acc, x) { acc + x })) # => 10 inspect([3, 1, 4, 1, 5].find(fn (x) { x > 3 })) # => 4 inspect([1, 2, 3].any(fn (x) { x > 2 })) # => true inspect([1, 2, 3].all(fn (x) { x > 0 })) # => true inspect([1, 2, 3].flat_map(fn (x) { [x, x * 10] })) # => [1, 10, 2, 20, 3, 30] mut words = ['banana', 'fig', 'apple'] words.sort_by(fn (s) { s.size() }) inspect(words) # => ['fig', 'apple', 'banana'] ``` **コールバックの引数個数。** 高階メソッドはコールバックを固定個数の引数で呼ぶ (`map` / `filter` / `find`などは1個、`reduce`は2個)。コールバックは ちょうどその個数を受け取れなければならない。固定引数個数が合わない関数は `TypeError`。チェックは反復の前に1度だけ行われるので、空のレシーバでも失敗する: ```culebra [1, 2, 3].reduce(0, |x| x) # !! reduce expects a 2-parameter function ``` ```culebra [1, 2, 3].map(fn (a, b) { a }) # !! map expects a 1-parameter function ``` `*args`コールバックは渡された分をすべて吸収するので任意の個数で使える。各呼び出しの 引数はrest配列として届く: ```culebra inspect([10, 20].map(fn (*xs) { xs.size() })) # => [1, 1] inspect([1, 2, 3].reduce(0, fn (a, *xs) { a + xs.size() })) # => 3 ``` `range` / `iota`(可変長ビルトイン)をそのままコールバックとして渡せるのはこのため。 この規則はVM・`--jit`・AOTで同一。 **コールバック引数の型注釈。** コールバック引数の型注釈は直接呼び出しと同様に 毎回の呼び出しで検査される。型の合わない要素に達した時点で`TypeError`: ```culebra [1, 'x'].map(fn (v: Long) { v * 2 }) # !! parameter 'v' expects Long ``` ### 18.3 オブジェクトメソッド | シグネチャ | 説明 | |----------------------------------|--------------------------------------------| | `o.size() -> Long` | 自身のプロパティ数 | | `o.empty() -> Bool` | `size() == 0`か | | `o.presence() -> Object \| Nil` | 非空なら`o`をそのまま(コピーでなく同じObject)、空なら`nil` — `??`/`?.`と組み合わせて「あれば使う」慣用句に | | `o.keys() -> Array` | キーの配列を挿入順で返す(表示順と同じ — §8) | | `o.values() -> Iterator` | 値を挿入順で返す遅延イテレータ。`o.iter()`(`(key, value)`ペアをyield)の値だけのビュー。他のイテレータ同様に連鎖/`collect`できる | | `o.has(key: String) -> Bool` | `key`を自身のプロパティ、または(クラスインスタンスなら)その名のメソッドとして持つか。ビルトインメソッド名は含まない | | `o.get(key, fallback) -> Any` | `key`に対応する値、無ければ`fallback`。読み取り専用で、決して挿入しない | | `o.get_or_put(key, init) -> Any` *(破壊的)* | `key`に対応する値。ミス時は`init`を格納してそれを返す(ストレージを共有するので`o.get_or_put(k, []).push(x)`は格納済みの配列を伸ばす)。`init`が関数なら遅延呼び出しされ、ミスしたときだけコストを払う: `o.get_or_put(k, || [])` | | `o.remove(key: String) -> Nil` *(破壊的)* | `key`が存在すれば削除 | `String`とバイト列が等しい`StringView`(例: `s[0..2]`)は**同じキー**扱いです — 上記のどの操作でも`o[key] = v`でも同じスロットに到達します。 ```culebra o = {b: 2, a: 1, c: 3} # keys() と values() はどちらも挿入順に走査する: inspect(o.keys()) # => ['b', 'a', 'c'] inspect(o.values().collect()) # => [2, 1, 3] inspect(o.has('a')) # => true # 無いキーはフォールバックを返すだけで、挿入はしない: inspect(o.get('z', 0)) # => 0 # 先頭 1 文字でグループ化する(キーは StringView): mut groups = {} for w in ['apple', 'avocado', 'banana'] { groups.get_or_put(w[0..1], || []).push(w) } inspect(groups) # => {mut a: ['apple', 'avocado'], mut b: ['banana']} mut p = {a: 1, b: 2} p.remove('a') inspect(p) # => {b: 2} ``` `o.iter()`が`(key, value)`タプルを返し、`to_object()`がそれを受けるので、 テーブル1つが式で書けます(`mut`のアキュムレータを宣言する必要がない): ```culebra mut prices = {apple: 100, banana: 80} # 値を変換する: inspect(prices.iter().map(|(k, v)| (k, v * 2)).to_object()) # => {mut apple: 200, mut banana: 160} # 反転する: inspect(prices.iter().map(|(k, v)| (v, k)).to_object()) # => {mut 100: 'apple', mut 80: 'banana'} ``` ### 18.4 特殊識別子 | 識別子 | 可視性 | 意味 | |--------|--------------------------|--------------------------------| | `fn` | すべての関数本体で有効 | 現在実行中の関数自身 | | `self` | メソッド呼出時のみ | メソッドのレシーバ | ### 18.5 イテレータプロトコル `for x in expr { ... }`(§12)は`expr`がイテレータプロトコルに 参加していることを要求します。プロトコルは`Object`(サブ型の `Array`含む)上の3つの **well-knownメソッド名**を使います — 各`next`の手前に`has_next`のゲートを置く形です: | メソッド | 形状 | 呼び出される型 | 戻り値 | |---|---|---|---| | `iter` | `fn () -> Object` | **Iterable** | **Iterator** (`self`で可) | | `has_next` | `fn () -> Bool` | **Iterator** | 次の要素があるか | | `next` | `fn () -> Any` | **Iterator** | 次の要素(`has_next()`が真だったときだけ呼ばれる) | 組み込みイテレータを終端より先で呼ぶと、例外ではなく`nil`が返ります (汲み尽くしたgeneratorと同じ)。`nil`自体も要素になりうるので、 両者を区別したいときは`has_next()`でゲートしてください。 **契約(プロパティ代入時に検査)**: `iter` / `has_next` / `next`を 引数を取るFunctionに束縛すると、代入時点で `DropContractError: type error: '' must be a Function taking no arguments.`が送出されます(§17の`drop`契約と同じ)。`Function`でない値は データであり、他のプロパティと同じように束縛できます。データは決して プロトコルのメンバーにならないので、`next`というキーを持つだけの `Object`はイテレータではなく、`iter`キーにデータを持つ`Object`はキーを 順にたどられます。名前が似ているだけのフィールド(`has_next_at`)は もともと影響を受けません。 **イテレータを開くときに検査**: `for`-inはヘッドで最初のステップの 前に、遅延チェーンは終端(`collect` / `count` …)が最初に駆動した 時点で検査します。チェーンの構築自体は何も引かないので、エラーは 終端側に現れます: | 状況 | エラー | 報告位置 | |---|---|---| | `iter()`が`Object`以外を返した | `TypeError: type error: iter() did not return an Object` | iterable式 | | イテレータが`has_next` / `next`を欠く | `TypeError: type error: iterator missing has_next()/next()` | iterable式、またはチェーンを駆動した終端 | **省略可能な`dispose`**: イテレータが引数なしの`dispose`を持つ場合、 全exitパス(正常なdrain・`break`・早期`return`・ループを抜ける例外)で 呼ばれます。例外はどちら側が投げたかを問いません — ループ本体、 `has_next()` / `next()`、中断中にthrowしたジェネレータ本体のいずれでも 同じです。ジェネレータはこれを使って、中断された本体に登録されたdeferを 走らせます。呼ばれるのはその反復自身の束縛が解放された後なので、ループ 変数と本体ローカルの`drop`はどのexitパスでも`dispose`より先に走ります。 すでに例外がunwind中のexitパスで`dispose`自身がthrowした場合は握り潰され、 外側の`catch`には元のエラーが渡ります。ループ変数のパターンが要素に マッチしなかった場合もその例外にあたるので、`catch`に届くのはmismatchの エラーです。 同じ契約は**終端イテレータメソッド**にも適用されます( プロトコルを駆動した者が閉じる)。すべての終端 — drainするもの (`collect`、`count`、…)、早期終了するもの(`find`、`any`、`first`、…)、 途中でthrowしたもの(callbackからでもproducerからでも) — は、 結果を計算し終えてから返す前に、`dispose`をちょうど1回呼びます。 throwする経路では元のエラーが伝播し、`dispose`自身のthrowは握り潰され ます。正常なdrainでは`dispose`のthrowは伝播します(`break`と同じ)。 遅延チェーンは消費側から見れば1つのイテレータなので、閉じればソースも閉じ ます。`map` / `filter` / `take` / `zip`などは自分が引いているイテレータへ `dispose`を転送します(2ソースのcombinatorは両方へ)。したがって `for x in gen().map(f) { break }`は`for x in gen() { break }`と同様に `gen()`のdeferを走らせます。`dispose`を持たないソース上のチェーンは、 自身も`dispose`を持ちません。 let countdown = fn (start) { mut i = start { iter: fn () { self }, has_next: fn () { i > 0 }, next: fn () { let v = i; i = i - 1; v } } } for v in countdown(3) { inspect(v) } # 3, 2, 1 **IteratorはIterableでもある**: イテレータの`iter`は自身を返す べきで、これにより`for x in some_iterator { ... }`が別途 Iterableラッパなしで動作します。 **組み込みIterable**: | 型 | `iter()`がyieldするもの | 順序 | |---|---|---| | `Array` | 要素 | インデックス順 (0..size-1) | | `Object` | `(key, value)`タプル | 挿入順(`o.keys()`と一致) | | `String` | UTF-8スカラ1つ分の`String` | バイト順 | `String`の反復はUTF-8バッファを遅延walkするため、100 MBの文字列 を数ステップで`break`しても残りはメモリ化されません。yieldされる 値は1-scalarの`String`で、整数のcode pointではありません。 `.map`で必要な形に変換してください。 `Object`の反復は`(key, value)`ペアをyieldします。なので 自然なループは`for k, v in obj`で、`obj.iter()`はエントリのビュー (他のイテレータ同様に連鎖・`collect`できる)です。片側だけのビューは `obj.keys()`(`Array`)と`obj.values()`(遅延イテレータ): ```culebra for k, v in {a: 1, b: 2} { inspect("{k}={v}") } # 'a=1' then 'b=2' for k in {a: 1, b: 2}.keys() { inspect(k) } # 'a' then 'b' for v in {a: 1, b: 2}.values() { inspect(v) } # 1 then 2 ``` **Object iterと変更**: 反復はループ開始時に取ったキーのスナップショット を回すので、ループ本体で対象を変更しても安全( fail-fastガードは無い)。反復中に**追加**したキーは対象に入らず、 **削除**したキーはスキップ、各値はその時点で**ライブ**に読む。これにより 「反復しながら派生エントリを追加する」setdefault的パターンがコピー無しで 書ける。`for k, v in obj`のsugarも同じプロトコル。 ```culebra mut o = {mut x: 1, mut y: 2} for k, v in o.iter() { o[k] = 99 } # 既存の値を更新 inspect(o.x) # => 99 ``` ```culebra mut books = {a: ('alpha', 1)} for reading, n in books.values() { # 反復しながら別名を追加 if !books.has(reading) { books[reading] = (reading, n) } } inspect(books.has('alpha')) # => true ``` **イテレータメソッド**: イテレータ・インターフェイスを満たすObject ——`next`と`has_next`、または`iter`を持つもの——は、以下の遅延 メソッド群を獲得します。これらは受け手を`has_next()` / `next()`で 駆動するため、ユーザの`iter()`結果(素の`{has_next, next}`オブ ジェクト)やジェネレータ(§11「ジェネレータ」)も、組み込みイテレータと 同じくcombinatorを連結できます(`range`・配列イテレータに限りません)。非終端メソッド は新しいIteratorを返し、終端メソッドはイテレータを消費して具体値を 返します。 ```culebra fn nums() { yield 1 yield 2 yield 3 yield 4 } inspect(nums().filter(|x| x % 2 == 0).map(|x| x * 10).collect()) # => [20, 40] ``` | 非終端 | 戻り値 | 説明 | |---|---|---| | `it.map(f)` | Iterator | 各`x`に対し`f(x)`をyield | | `it.filter(p)` | Iterator | `p(x)`が真の`x`のみyield | | `it.take(n)` | Iterator | 先頭`n`個、以降`done` | | `it.skip(n)` | Iterator | 最初の`next()`で先頭`n`個を捨てる | | `it.take_while(p)` | Iterator | 最初に`p(x)`が偽になるまでyield | | `it.skip_while(p)` | Iterator | 先頭の`p(x)`が真である連続分を捨て、以降は無条件にyield | | `it.step_by(n)` | Iterator | 先頭要素と、その後`n`個おきの要素。`n`は1以上 | | `it.distinct()` | Iterator | 各要素の初出のみyieldし、以降の重複は捨てる(要素はハッシュ可能である必要あり) | | `it.tap(f)` | Iterator | `f(x)`を副作用のために呼び、`x`はそのまま流す。遅延チェーンの観測用 | | `it.scan(init, f)` | Iterator | running fold。各ステップで`acc = f(acc, x)`をyield(`init`から開始)。`init`自体はyieldしないので、出力の個数は入力と一致する | | `it.flatten()` | Iterator | ネストを1段外す。各要素はiterableである必要あり(`flat_map`と同じ変換規則) | | `it.chunk_by(f)` | Iterator | キー`f(x)`が同じ**隣接**要素をArrayにまとめる。別のキーを挟んで再登場したキーは新しいrunになる — 位置に関係なくキーでまとめる`group_by`との違い | | `it.chunks(n)` | Iterator | `n`個ずつArrayにまとめてyield(最後のグループは短くなりうる)。`n`は1以上 | | `it.windows(n)` | Iterator | 直近`n`個のスライディングウィンドウをArrayでyield、1つずつ進む。`n`は1以上 | | `it.flat_map(f)` | Iterator | `f(x)`はiterableを返す必要あり、結果を連結 | | `it.chain(other)` | Iterator | `it`の次に`other`をyield | | `it.zip(other)` | Iterator | `{first, second}`のペアをyield、短い側で終了 | | `it.enumerate()` | Iterator | `(index, value)`タプルをyield(`index`は0起点)。`Array`の直接メソッド(`arr.enumerate()`)でもあり、遅延イテレータを返す | | 終端 | 戻り値 | 説明 | |---|---|---| | `it.collect()` | `Array` | Arrayに実体化 | | `it.join(sep)` | `String` | 要素を`sep`で連結(各要素は`to_string`相当で描画)。`Array.join`と同じなので`xs.map(...).join(",")`に中間`.collect()`は不要 | | `it.for_each(f)` | `Nil` | 副作用目的で`f(x)`を呼ぶ | | `it.reduce(init, f)` | Any | 左畳み込み: `acc = f(acc, x)`を`init`から | | `it.find(p)` | Any \| `nil` | 最初に`p(x)`が真の`x`、無ければ`nil` | | `it.any(p)` | `Bool` | 一つでも`p(x)`が真なら`true` | | `it.all(p)` | `Bool` | すべて`p(x)`が真なら`true`(空は`true`) | | `it.count()` | `Long` | 要素数 | | `it.first()` | Any \| `nil` | 先頭要素、空なら`nil`。1個しか引かない | | `it.last()` | Any \| `nil` | 末尾要素、空なら`nil` | | `it.nth(n)` | Any \| `nil` | 0起点で`n`番目の要素、無ければ`nil`。`n + 1`個しか引かない。負の`n`は`ValueError` | | `it.position(p)` | `Long` \| `nil` | 最初に`p(x)`が真になる0起点の添字、無ければ`nil`(`-1`を返す`Array.index_of`とは異なる) | | `it.contains(v)` | `Bool` | `== v`の要素があれば`true`(`Array.contains`と同じ) | | `it.sum()` | `Long` \| `Float` | 合計。全要素が`Long`なら`Long`、一つでも`Float`があれば`Float`(空は`0`) | | `it.product()` | `Long` \| `Float` | 積。昇格規則は`sum`と同じ(空は`1`) | | `it.min()` | Any | 最小の要素。比較は`<`と同じ規則で、返すのは要素なのでその型が保たれる。空では例外 | | `it.max()` | Any | 最大の要素。規則は`min`と同じ | | `it.min_by(f)` | Any | キー`f(x)`が最小の要素。キーは`<`と同じ規則で比較する。同値なら先に現れた方。空では例外 | | `it.max_by(f)` | Any | キー`f(x)`が最大の要素。同値なら先に現れた方。空では例外 | | `it.to_set()` | `Set` | 初出順のメンバー、重複は除去 | | `it.to_object()` | `Object` | `(key, value)`タプルをObjectへ — `Object.iter()`の逆。キーは初出順、重複はその位置で上書き、エントリはmutable。2要素タプルでない要素は`TypeError` | | `it.group_by(f)` | `Object` | `f(x)`をキーに要素をArrayへ振り分ける。キーは初出順 | | `it.partition(p)` | `Tuple` | 1パスで`(条件を満たす, 満たさない)`に分割。両方とも順序を保つ | | `it.unzip()` | `Tuple` | `(Array, Array)` — `zip`の逆。各要素は2要素タプルか`{first, second}`形のObjectのどちらかを受け付け、それ以外は`TypeError` | すべての終端は自分が駆動したイテレータをdisposeします(上記 **省略可能な`dispose`** 参照)。早期終了する終端も同様なので、 `it.find(p)`の後はソースが閉じており、同じ`it`への後続の終端は何も 返しません(`collect`は`[]`)— `break`の後とまったく同じです: ```culebra fn g() { yield 1 yield 2 yield 3 } let it = g() inspect(it.find(|x| x == 2)) # => 2 inspect(it.collect()) # => [] ``` この`find`の時点で`g()`のdeferが走っています。2つ目が空なのは、 dispose済みのソースがdoneを返しているためです。 **eager vs lazy**: `Array`には独自のeager版`map` / `filter` / `for_each` / `reduce` / `find` / `any` / `all` / `flat_map`(§18.2) があり、すべて新しい`Array`を返します。`Array`に対してこれらを 呼ぶとeager版にディスパッチされます。遅延チェーンに切り替えるには 先に`.iter()`を呼びます。既定をeagerにしているのは、各段で実体化 される連鎖のほうが追いやすいからです。遅延は、列が大きい/無限のときに と同じ二層構造です。 ```culebra # doctest: skip # Eager: 中間 Array を 2 つ確保 arr.map(f).filter(g) # Lazy: single pass、中間 Array なし arr.iter().map(f).filter(g).collect() # Lazy + 早期終了: 要素 4 つしか触らない range(1000000).filter(f).map(g).take(4).collect() ``` **ユーザ定義の例**: ```culebra countdown = fn (start) { mut i = start { iter: fn () { self }, # Iterator は自身が Iterable has_next: fn () { i > 0 }, next: fn () { v = i i -= 1 v }, } } for x in countdown(3) { inspect(x) } # 3, 2, 1 ``` **JIT**: 本節のすべて — for-inによるプロトコル駆動、ユーザー定義 イテレータ、lazy iteratorメソッドチェーン、`range`、 `String.code_points()` / `.graphemes()`、`[...].map(...).filter(...)` のようなArrayのeagerチェーン — が`--jit`でVMと 同じセマンティクスで動作します。Array / String / keysの高速パスは nativeのまま(要素あたり1 load)。iteratorプロトコル駆動は ステップあたりのクロージャ呼出コストを支払います(動的言語での iterator chainで不可避)。eagerに実体化して最大スループットが 欲しい場合は`iota` + `Array.map` / `.filter` / `.reduce`、 定数メモリでストリーミングしたい場合は`range` + lazy メソッドを使い分けてください。 --- ## 19. コア組み込み関数 以下の関数は言語本体の一部で、すべての実行環境にグローバル名として バインドされ、置き換えはできません。第1グループ(`to_long` / `to_float` / `to_string`、`type_of`)は言語セマンティクス(ソース位置に 紐付くエラー、型の内省、表示規則)に結びついています。第2グループ (`range`、`iota`、`grid`)は標準的な整数列ファクトリで、`range` / `iota`は両バックエンドがfusion / specialisationの対象として認識し、 言語全体の`for`-inループで使われる正規形です。`repeat`は関連するが 別物の、eagerな`Array`コンストラクタです — 整数列ではなく、1つの値 を`n`個複製します。第3グループはmatcher 一族 (`assert_true` / `assert_eq` / `assert_throws` / `assert_close`等) — 全referenceは [`docs/stdlib.ja.md`](stdlib.ja.md) を参照。`Math` / `IO` / `Sys` といったネームスペース付きの標準ライブラリも同じく`stdlib.ja.md`を 参照。出力プリミティブ`inspect` / `print` / `println`はCLIが追加する グローバルです(§22)。 これらのグローバルはすべて **first-class value** です。変数に束縛したり 高階関数へ渡したりすると、両バックエンドでクロージャと同じように振る舞います。 ```culebra inspect([1, 2, 3].map(type_of)) # => ['Long', 'Long', 'Long'] inspect([1, 2, 3].map(range).map(|r| r.collect())) # => [[0], [0, 1], [0, 1, 2]] let f = range inspect(f(0, 10, step: 2).collect()) # => [0, 2, 4, 6, 8] ``` 直接呼び出しは引き続きfast pathで、クロージャ形は名前が値の位置に 現れたときのみ使われます。`range` / `iota`は1〜2個の位置引数と、 `range`のキーワード専用`step`もこのクロージャ経由で受け取ります (`range(0, 10, **{step: 2})`も動作)。診断 (`ArityError` / 未知キーワード) は直接呼び出しと同一です。 ### `to_long(v: Any, *, base: Long = 10) -> Long` `v`を`Long`に変換します: * `Long` → そのまま。 * `Float` → 0方向への切り捨て。 `to_long(3.7) == 3`、`to_long(-3.7) == -3`。NaN、無限大、Longの範囲 (±2⁶³)を超える値は`ValueError`。 * `String` → `base`の符号付き整数としてパース。前後の空白は許容、 それ以外は失敗。 * 上記以外の型は`type error`。 `base`はキーワード専用で2〜36。base 16 / 8 / 2では対応する `0x` / `0o` / `0b`接頭辞も受け付けるので、ソースからコピーした リテラルがそのままパースできます。非Stringの`v`に`base`を渡すのは 黙って無視せずエラーにします。キーワード専用にしているのは、 `to_long`を1引数関数のまま保って`map(to_long)`が通るようにするためです。 **例外**: パースできない文字列、非数値・非文字列、それらに`base`を 渡した場合は`type error at L:C.`。`base`が2〜36の外なら`ValueError`。 ```culebra inspect(to_long('42')) # => 42 inspect(to_long('-7')) # => -7 inspect(to_long(3.9)) # => 3 inspect(to_long('ff', base: 16)) # => 255 inspect(to_long('0b1010', base: 2)) # => 10 ``` ### `to_float(v: Any) -> Float` `v`を`Float`に変換します: * `Float` → そのまま。 * `Long` → `Float`に昇格(絶対値2⁵³ までは厳密、それ以上は精度を 失うことがある)。 * `String` → 10進または指数表記の浮動小数点数としてパース。前後の 空白は許容。 * 上記以外の型は`type error`。 ```culebra inspect(to_float(3)) # => 3.0 inspect(to_float('1.5')) # => 1.5 inspect(to_float('1e-5')) # => 1e-05 ``` ### `to_string(v: Any) -> String` `v`を表示形式に変換します(補間時と同じフォーマット。文字列は 引用符なしで渡る)。表示規則は §8を参照。`Float`はround-trip最短 表現で、必ず小数点か指数を伴うため`Long`と視覚的に区別できます。 ```culebra inspect(to_string(42)) # => '42' inspect(to_string(1.0)) # => '1.0' inspect(to_string(1e-5)) # => '1e-05' inspect(to_string([1, 2])) # => '[1, 2]' inspect(to_string('hi')) # => 'hi' ``` ### `class_of(v: Any) -> Object?` `v`を作ったクラスオブジェクト、または`nil`。`type_of`が「何と呼ばれるか」 を答えるのに対し、こちらは実体そのものを答えます。クラスのstaticはそこに あり、クラスを受け取って返すデコレータが足したものもそこにあります。 どちらもインスタンスにはコピーされないので、インスタンスから届く手段は これだけです。 ```culebra let mark = fn (cls) { cls.marked = true cls } @mark class Point { new(x, y) { self.x = x self.y = y } } let p = Point(4, 5) inspect(class_of(p) == Point) # => true inspect(class_of(p).marked) # => true inspect(class_of(p).name) # => 'Point' inspect(class_of(p).name == type_of(p)) # => true ``` クラスオブジェクト自身は`class_of`に自分を答えるので、この読みは冪等です。 enumのバリアントは自分のenum(バリアントを読み出す元のオブジェクト)を 答え、`type_of`はバリアント名を答えるので、この2つで「どのenumのどの バリアントか」が分かります。 ```culebra enum Mirroring { Horizontal, Vertical } enum Shape { Circle(Float), Rect(Float, Float) } inspect(class_of(Mirroring.Vertical) == Mirroring) # => true inspect(class_of(Shape.Circle(1.0)) == Shape) # => true inspect(type_of(Shape.Circle(1.0))) # => 'Circle' ``` バリアントはenumを指しますが、生かしてはおかないので、ほかに誰もenumを 持っていなければ(戻り済みの関数の中で宣言し、コンストラクタを外に渡して いないenumなど。enumから読み出したコンストラクタはenumを持ち続けます)、 そのバリアントは `nil`を答えます。別の場所で作り直されたバリアント(`Channel`で受け取った もの、`SharedBuffer`から読み戻したもの)も`nil`で、`Channel`で受け取った classインスタンスと同じです。どちらの場合も、表示と比較は元のバリアントの ままです。 自分のクラスオブジェクトを持たない値は`nil`で、これは見た目より広い範囲を 指します: 普通のObject、プリミティブ、そして`Range`。つまり `class_of(v).name`は`type_of(v)`の別の綴りではありません — 両方を持つのは `class`構文の値だけです。 ```culebra inspect(class_of(42)) # => nil inspect(class_of({a: 1})) # => nil inspect(class_of(1..3)) # => nil ``` ### `type_of(v: Any) -> String` `v`の型の名前を、型注釈と同じ語彙で返します。 `'Nil'`, `'Bool'`, `'Long'`, `'Float'`, `'String'`, `'StringView'`, `'Array'`, `'Object'`, `'Function'`, `'Tensor'`, `'Tuple'`, `'Set'`, `'Range'`のいずれか。あるいは、あるクラスが作った値ならそのクラス自身の 名前(enumのバリアントはバリアント名)。クラスオブジェクト自身は `'Class'`です。つまりこの集合は開いていて、`type_of(v) == 'T'`は `v: T`が引数に問うのと同じことを問います。違いは、注釈は部分型も受ける (`Object`はどのインスタンスも受ける)のに対し、こちらは厳密な名前を 答えることです。 ```culebra inspect(type_of(42)) # => 'Long' inspect(type_of(1.5)) # => 'Float' inspect(type_of('hi')) # => 'String' inspect(type_of([1, 2])) # => 'Array' inspect(type_of((1, 2))) # => 'Tuple' inspect(type_of(Set(1, 2))) # => 'Set' inspect(type_of({a: 1})) # => 'Object' inspect(type_of(1..3)) # => 'Range' ``` ### `range(n: Long, *, step: Long = 1) -> Iterator` / `range(start: Long, end: Long, *, step: Long = 1) -> Iterator` 遅延評価の整数列ファクトリ。Iterator(§18.5)を返し、整数を1つずつ yieldします。`for`-inやイテレータメソッドチェーンと組み合わせると、 範囲のサイズに関わらず**一定の追加メモリ**で反復できます。 * `range(n)`は`0, 1, ..., n-1`をyield。`n <= 0`なら即座に完了。 * `range(start, end)`は`start, start+1, ..., end-1`をyield。 `start >= end`なら即座に完了。 * `step:`はキーワード専用。正の値で昇順 (exclusive end)、負の値で 降順 (exclusive end)。`step: 0`は`ValueError`。 ```culebra for i in range(5) { inspect(i) } # 0, 1, 2, 3, 4 for i in range(2, 6) { inspect(i) } # 2, 3, 4, 5 for i in range(0, 10, step: 2) { inspect(i) } # 0, 2, 4, 6, 8 for i in range(5, 0, step: -1) { inspect(i) } # 5, 4, 3, 2, 1 # 巨大な上限でも定数メモリで動く for i in range(1000000000) { if i > 3 { break } inspect(i) } ``` **JIT**: `range`はJITネイティブなiterator Objectを返し、 `range(N).(...)`のメソッドチェーンは直接のcounterループに fusionされます。§18.5を参照。 ### `iota(n: Long) -> Array` / `iota(start: Long, end: Long) -> Array` `range`のeager版。同じ整数列を`Array`として実体化します。APL / C++ `std::iota` / Scheme SRFI-1から命名。`for`-inループには`range`を 推奨し、本当に`Array`全体が必要な場合(インデックスアクセスする、 `Array`を期待する関数に渡す等)に`iota`を使います。 * `iota(n)`は`[0, 1, ..., n-1]`。`n <= 0`なら空配列。 * `iota(start, end)`は`[start, start+1, ..., end-1]`。`start >= end` なら空配列。 ```culebra inspect(iota(3)) # => [0, 1, 2] inspect(iota(2, 5)) # => [2, 3, 4] inspect(iota(5, 2)) # => [] ``` ### `repeat(n: Long, value: Any) -> Array` `value`を`n`個複製した`Array`を作ります。`n < 0`は`ValueError`、 確保できないほど大きい`n`も`ValueError`(`repeat() result is too large`)、 `n == 0`は`[]`。全スロットは同じ`value`です — `Array`/`Object`の ような可変参照の場合、`n`個のスロットは同一インスタンスを共有します (Pythonの`[value] * n`やJavaScriptの`Array(n).fill(value)`と同じ 挙動)。 ```culebra inspect(repeat(3, 0)) # => [0, 0, 0] inspect(repeat(0, "x")) # => [] let shared = repeat(2, []) shared[0].push(1) # 両スロットとも同じArray inspect(shared) # => [[1], [1]] ``` ### `grid(x_range: Range, y_range: Range) -> Iterator` 2つの有界な整数RangeのCartesian積を返す遅延評価ファクトリ。 Iterator(§18.5)を返し、`(x, y)`のTupleをyieldします(`x`が最も 速く変化)。これは`for y in y_range { for x in x_range { ... } }` という二重forループが辿る順序と同じで、1行かつ一定の追加メモリ で書けます。両方の引数は有界な`Range`値(`a..b` / `a..=b`、 `by`ステップ指定可)である必要があり、無限範囲や`step: 0`は Rangeに対する`for`-inと同じエラーを送出します。どちらかの Rangeが空なら、積全体も空になります。 ```culebra for (x, y) in grid(0..3, 0..2) { inspect((x, y)) } # (0,0), (1,0), (2,0), (0,1), (1,1), (2,1) ``` ```culebra inspect(grid(0..2, 0..2).collect()) # => [(0, 0), (1, 0), (0, 1), (1, 1)] # x側かy側が空なら、積全体も空になる。 inspect(grid(0..0, 0..3).collect()) # => [] ``` ### `__ARGS__` (可変長 catch-all バインディング) 任意の関数本体内で、暗黙のローカル`__ARGS__`は宣言済みパラ メータからあふれた位置引数を`Array`として保持します。明示的な `**rest`(こちらは*キーワード*引数を集めます、位置引数では ない)を宣言せずに可変長の末尾値を受けたい場合に使えます。 ```culebra let logger = fn (level) { inspect("[{level}] " + __ARGS__.join(' ')) } logger('info', 'building', 'fizzbuzz') # → '[info] building fizzbuzz' ``` あふれた分に必ず行き先があるので、ユーザ定義関数を宣言より多い 位置引数で呼ぶことは**エラーではありません**。組み込みが `ArityError`(§15)を出すのとは異なります。ユーザ定義関数で positionalを制限したい場合はパラメータリストに単独の`*`を置きます — それを超えた分は`__ARGS__`に届かず`TypeError`になります。 `__ARGS__`はキーワード引数を受け取りません — そちらは明示的な パラメータリストか`**rest`経由です。引数を取らないと宣言した fnが引数付きで呼ばれた場合、全ての引数が`__ARGS__`に入ります。 ### 関数イントロスペクション 全ての`Function`値は読み取り専用のイントロスペクション用プロパティを 3つ公開します: | プロパティ | 型 | 説明 | |-----------------|----------------|---------------------------------------------------------------------| | `fn.name` | `String` | ソース上の宣言名 (`fn name(...)`)。無名関数は`""` | | `fn.return_type`| `String` | 戻り型注釈 (`fn f() -> X`) または注釈なしなら`""` | | `fn.params` | `Array`| 各パラメータのmetadata。`name / mut / type / has_default / kw_only / kwargs_rest` | ```culebra fn greet(name: String, *, prefix = "hi") { "{prefix}, {name}" } inspect(greet.name) # => 'greet' inspect(greet.return_type) # => '' let ps = greet.params inspect(ps.size()) # => 2 inspect(ps[0].name) # => 'name' inspect(ps[0].type) # => 'String' inspect(ps[1].kw_only) # => true inspect(ps[1].has_default) # => true ``` `fn.params`はアクセス毎に新規`Array`を返します。これを書き換えても 関数自体には影響しません。多重ディスパッチ用ディスパッチャは **最初に登録されたメソッドの**シグネチャを返します (それ以降の オーバーロードは見えません)。 --- ## 20. 多重ディスパッチ トップレベルで`fn name(params) body`形式の名前付き関数宣言を 複数書き、パラメータの型注釈(§14)を変えると、それらは1つの **マルチメソッド** を構成します。呼び出し側では引数の動的型に基づき、 最も特異的に一致するメソッドが選ばれます。 ``` fn area(s: Long) { s * s } fn area(s: Float) { s * s } fn area(s: String) { s.size() } area(5) # → 25 (Long 版) area(5.0) # → 25.0 (Float 版) area("hello") # → 5 (String 版) ``` 無名関数式`let f = fn(...) {...}`の挙動は変わりません。 マルチメソッドはあくまで **トップレベルの`fn name(...)`宣言** に だけ作用します。 ある名前のオーバーロードは、それを宣言したスコープに属します。`if`や `cond`のarmは独立したスコープ(§6)なので、その中に書いた `fn name`はarm自身の関数です。外側のスコープのオーバーロードには加わらず、 armの終わりで消えます: ```culebra fn describe(x: Long) { 'a number' } if true { fn describe(x: String) { 'a string' } inspect(describe('hi')) # => 'a string' } inspect(describe(1)) # => 'a number' # !! DispatchError describe('hi') ``` **デフォルト引数とアリティ。** デフォルト値を持つメソッドは、位置引数の 個数が「必須個数(デフォルト無しの引数)」から「全引数個数」までの範囲に 入る呼び出しにマッチします。省略された末尾はデフォルトで埋められます。 型特異性が同じ候補の中では、デフォルトで埋める数が少ない方(よりアリティ が厳密な方)が選ばれます。 **キーワード引数**は、位置引数で埋まらなかった必須引数を埋めることも できます(キーワードは「どのメソッドが適用可能か」に寄与する)。ただし 選択自体は位置引数だけでスコアするので、キーワード(やキーワードで渡す型) でしか違わない2つのオーバーロードは**曖昧**になり、勝手に選ばれません。 位置引数でもキーワードでも与えられない必須引数があれば、その呼び出しは 未マッチ(`DispatchError`)です。 ``` fn at(a, b = 10) { a + b } at(1) # → 11 (b はデフォルト) at(1, 2) # → 3 at(1, b: 2) # → 3 (b をキーワードで) at(a: 1, b: 2) # → 3 (必須の a をキーワードで) at(b: 9) # !! DispatchError — 必須の a が無い ``` ### 構文 ``` MULTIFN_DECL <- 'fn' IDENTIFIER PARAMETERS RETURN_TYPE? BLOCK ``` 文の位置で`fn名前(...)`が出現したらこの規則が優先されます。 `fn(...) {...}`のように名前のないものは従来通り式(クロージャ) です。 ### 特異度ルール 各引数 *i* について、パラメータの型注釈とその引数の動的型から 特異度スコアを求めます: | パラメータ注釈 | 引数 | 特異度 | |--------------------|--------------------|--------| | `Any`(注釈なし) | 何でも | 0 | | `Object` | クラスインスタンス(`Square`, `Circle`等) | 1 | | 厳密一致(`Long`, `Float`, ..., 具体クラス名) | 同じ型 | 2 | | それ以外 | — | 不一致 | `Object`と厳密一致の間にも段があり、粗いほうから順に、引数が conformするtrait、union (`Ok | Err`)、variant引数の親enum名 (`Result`) の3つです。厳密一致より上は2つで、修飾したvariant (`Result.Ok`は素の`Ok`に勝つ)、それから型引数まで書いたgeneric (`Array`は素の`Array`に勝つ)。`T?`は`T`が本来取るスコアを unionの段まで下げるので、素の`T`が常に`T?`に勝ちます。 引数ごとのスコアからなるタプルを比較し、すべての要素で **他方 以上、少なくとも1要素で他方より大** の関係を満たすメソッドが あればそれを採用します。スコアタプルが完全に一致する場合は **ambiguous dispatch** エラーを投げます。比較不能(互いに勝つ 要素がある)な場合は先に登録されたメソッドが残ります。 ``` fn label(x: Long, y) { "long-any" } fn label(x, y: Long) { "any-long" } fn label(x: Long, y: Long) { "long-long" } # 最特異 fn label(x, y) { "any-any" } label(1, 2) # → "long-long" label(1, "x") # → "long-any" label("x", 1) # → "any-long" label("x", "y") # → "any-any" ``` ### クラスインスタンスのディスパッチ `class`構文(§10)で生成したインスタンスはクラス名でディスパッチ できます: ``` class Square { new(side) { self.side = side } } class Circle { new(r) { self.r = r } } fn shape_area(s: Square) { s.side * s.side } fn shape_area(c: Circle) { 3.14 * c.r * c.r } shape_area(Square.new(4)) # → 16 shape_area(Circle.new(2)) # → 12.56 ``` これはクラスシュガーが各インスタンスに付与する`class:` String プロパティを利用しています(§10)。`Object`注釈はこれより緩く、 任意のクラスインスタンスにマッチします。 ### 同名・同シグネチャの再宣言 同じ関数名で **パラメータ型シーケンスが完全に一致する** 宣言が 再び現れた場合、そのエントリは新しい本体で上書きされます。 REPLでの試行錯誤を意図したセマンティクスです。 ``` fn greet(name: String) { "hi, {name}" } fn greet(name: String) { "hello, {name}" } # 上書き greet("alice") # → "hello, alice" ``` 上書きが及ぶのは宣言スコープの**1回の実行**の中だけです。関数本体や ループ本体の中にある宣言は走るたびに新しいオーバーロード集合を作るので、 各本体はそれを宣言した実行時の捕獲を保ちます(先に得た`fn`値が後の実行の 本体に差し替わることはありません)。後述のクラス側のオーバーロード集合も 同じ規則です。 ``` fn make(tag) { fn m(a) { "{tag}-one" } fn m(a, b) { "{tag}-two" } m } let a = make("A") let b = make("B") [a(1), b(1)] # → ["A-one", "B-one"] ``` ### 既存機能との共存 * 通常のローカル束縛`let f = fn(...) {...}`はそのまま動作します。 * クラスメソッド`obj.method()`のディスパッチには干渉しません。 メソッドは引き続き`class`ボディ内で定義し、`self.`経由で 呼び出します。 * `Any`全引数のメソッドはキャッチオールとして利用できます。 ### 制約 * **トップレベル / フリー関数のみ.** ブロック内ネスト宣言や クラスメソッドはこの仕組みの対象外です。 * **エラー.** 一致するメソッドが無い場合は`no matching method`、 特異度が完全に並んだ場合は`ambiguous dispatch`を投げます。 どちらもランタイム例外ではなく即座にプログラムを中断します (§15、§24)。 ### キーワード引数とマルチメソッド ディスパッチは**位置引数**の型だけで行われます(Julia風の kwsorter)。キーワード引数や`**` splatはディスパッチには 参加せず、選ばれたメソッドのシグネチャに対して通常のkwargsルール (§11)でbindされます。 ``` fn paint(s: String, *, color = "red") { "{color} {s}" } fn paint(n: Long, *, color = "blue") { "{color} {n}" } paint("circle") # → "red circle" paint(7) # → "blue 7" paint("box", color: "green") # → "green box" paint(7, **{color: "gold"}) # → "gold 7" ``` 各メソッドのkw-onlyデフォルト、`**rest`キャッチオール、パラメータ 名はそれぞれ独立で、ディスパッチに使われるのは位置シグネチャだけです。 ### メソッド多重ディスパッチ(インスタンスメソッド) クラスは**同名で位置パラメータ型シグネチャが異なる**インスタンス メソッドを複数宣言できます。これらは1つのディスパッチャにまとまり、 `obj.method(args)`は明示引数のランタイム型でoverloadを選びます。 レシーバはproperty lookupで確定済み(スコアされるのは引数のみ)で、 `self`は選ばれたoverloadに束縛されます。 ```culebra class Calc { new() {} go(x: Long) { "long: {x}" } go(x: String) { "string: {x}" } go(x: Long, y: Long) { "sum: {x + y}" } } let c = Calc.new() c.go(1) # → "long: 1" c.go("a") # → "string: a" c.go(2, 3) # → "sum: 5" ``` スコアリング・デフォルト引数・`*args`・kwarg・`**rest`のルールは free関数のマルチメソッドと同じ(キーワード引数は選ばれたoverloadに 流れます)。マッチするoverloadが無ければfree関数同様catch可能な `DispatchError`を送出します。 名前を1度だけ宣言したクラスは通常メソッドのまま(ディスパッチャ無し・ オーバーヘッド無し)。以下はコンパイル時エラーのまま: 同一シグネチャの 2メソッド、フィールドとメソッドの名前衝突、フィールド重複。 コンストラクタ(`new`)・演算子/dunderメソッド(`__add__`, `__eq__`, `__call__`等)もoverloadできます(下記)。 ### メソッド多重ディスパッチ(static メソッド) staticメソッドも同様にoverloadできます。位置パラメータ型シグネチャ が異なる同名`static`メソッドは、クラスオブジェクト上の1つの ディスパッチャにまとまり、`Cls.method(args)`は引数型で選びます。 `self`は関与しません(static呼び出しは`self`を束縛しません): ```culebra class Vec { static make(x: Long) { "long: {x}" } static make(x: String) { "str: {x}" } static make(x: Long, y: Long) { "pair: {x}, {y}" } } Vec.make(5) # → "long: 5" Vec.make("hi") # → "str: hi" Vec.make(1, 2) # → "pair: 1, 2" ``` staticとinstanceのoverloadセットは独立です: staticとinstanceの メソッドが同名で、それぞれ独自のoverloadを持てます。同一シグネチャの 2つのstaticメソッド、およびstaticフィールドとstaticメソッド名の 衝突はコンパイル時エラーのままです。 ### コンストラクタ多重ディスパッチ(`new`) クラスは位置パラメータ型シグネチャの異なる`new`本体を複数宣言できます。 `C(args)`(または`C.new(args)`)は引数の実行時型で、メソッドoverloadと 同じように振り分けます: ```culebra class Point { new(x: Long, y: Long) { self.tag = "xy" self.x = x self.y = y } new(s: String) { self.tag = "str" self.x = s.size() self.y = 0 } new() { self.tag = "empty" self.x = -1 self.y = -1 } } Point(3, 4).tag # → "xy" Point("hi").tag # → "str" Point().tag # → "empty" ``` overloadは**インスタンスを確保する前**に選ばれるため、どのoverloadにも マッチしない呼び出しは副作用なしでcatch可能な`DispatchError`を送出します (インスタンスは作られず、field初期化子(§10)も走りません)。overloadが 選ばれると、その宣言field初期化子が1度走り、続いて本体が走ります。この間 `self`は不変です(`self = …`の再代入は単一`new`と同様どのoverloadでも `ImmutableError`)。 デフォルト引数・キーワード引数・`*args`はメソッドoverloadと同じルール。 `new`を1度だけ宣言したクラスは通常コンストラクタのまま(ディスパッチャ 無し・オーバーヘッド無し)。同一シグネチャの2つの`new`本体はコンパイル時 エラーのままです。 ### dunder / 演算子多重ディスパッチ 演算子メソッド(`__add__`, `__eq__`, `__lt__`, `__index__`, `__call__` 等、§10)は演算子ルックアップ経路で到達する通常のインスタンスメソッドなので、 オペランド型シグネチャの異なる複数を宣言すると1つのディスパッチャにまとまり、 演算子がオペランドの実行時型でoverloadを選びます: ```culebra class Vec { new(x: Long, y: Long) { self.x = x self.y = y } __add__(o: Vec) { Vec(self.x + o.x, self.y + o.y) } # 要素ごと __add__(n: Long) { Vec(self.x + n, self.y + n) } # スカラ } let v = Vec(1, 2) v + Vec(10, 20) # → Vec(11, 22) (Vec overload) v + 5 # → Vec(6, 7) (Long overload) ``` 可換演算のauto-reflectionは不変: `5 + v`は`v.__add__(5)`に反射し`Long` overloadを選びます。どのoverloadにもマッチしないオペランドはcatch可能な `DispatchError`(kind・文面・位置がbackend間で一致)— 型注釈付きパラメータを 満たさないメソッド呼び出しと同じルールです。dunderを1度だけ宣言したクラスは 通常メソッドのまま。同一シグネチャの2つのdunder本体はコンパイル時エラーの ままです。 --- ## 21. デコレータ `fn`または`class`宣言の前に`@expr`の行を置くと、宣言された値を `expr`で包んでから元の名前にbindする糖衣構文。スタックされたデコ レータは下から上 (bottom-up) に適用される — 宣言に最も近いものが先 に実行され、最上層が最後に実行される: ```culebra # doctest: skip @a @b fn foo() { ... } ``` は`foo = a(b(<オリジナルのfn値>))`と等価。 この呼び出しと同じく、デコレータ式そのものは上から順に(`a`、`b`の順に) 評価され、そのあとで適用される。 ### デコレータ式 各`@`の後ろには`name` / `name.attr` / `name(args)`のいずれかの 形 — つまり`CALL`チェーン — を書ける。式は宣言時に **一度だけ** 外側スコープで評価され、結果は (1引数の) callable (関数or class) でなければならない。複数引数を取りたい場合はfactoryパターン: `@combine(a, b)`のように呼び出した結果が「1引数のデコレータ」を 返す形にする。 ### binding デコレータは**bind前の宣言値**を受け取り、変数に入る値を返す: ```culebra let tag = fn (f) { fn () { "[{f()}]" } } @tag fn greet() { "hello" } greet() # "[hello]" ``` クラスの場合はclass objectを受け取って返す: ```culebra let mark = fn (cls) { cls.marked = true cls } @mark class Point { new(x, y) { self.x = x self.y = y } } Point.marked # true ``` 名前が束縛されるのはデコレータが戻った後です。デコレータの実行中に その名前を読むコード(デコレータが呼んだそのclassのメソッド、自分自身を 呼ぶデコレータ付きの関数)は`NameError: undefined variable`になります。 ### 多重ディスパッチとの相互作用 デコレータ付き`fn name(...)`は多重ディスパッチに**参加しない** — デコレータの戻り値がそのまま`name`にbindされる。両方欲しいなら、 multimethod側を先に定義してから別名でラップしたdecorated fnを作 る形にする。 ### 組み込みデコレータ: `@value` `@value`は呼び出し可能値ではなく、クラスを**値型**として印付ける。使う動機は コストにある。小さなデータクラスをホットループで使うと、中間結果ごとに アロケーションを払うことになるが、全フィールドがスカラの`@value`クラスは それを1度も払わない。ループがマシンのスロットに落ち、インスタンスを1つも 作らないからである: ```culebra @value class Vec2 { x: Float y: Float new(x: Long | Float, y: Long | Float) { self.x = to_float(x) self.y = to_float(y) } __add__(o) { Vec2.new(self.x + o.x, self.y + o.y) } __mul__(s) { Vec2.new(self.x * s, self.y * s) } } let DT = 1.0 / 60.0 let G = Vec2.new(0, -9.8) let mut p = Vec2.new(0, 100) let mut v = Vec2.new(12, 0) for _ in 0..600 { v += G * DT p += v * DT } inspect(Math.round(p.x)) # => 120 ``` `@value`の行を消しても同じ数を印字するが、そのときは1周ごとに4つの 中間インスタンス(`G * DT`・`v + …`・`v * DT`・`p + …`)が作られて 捨てられ、`__add__`と`__mul__`には通常のランタイムディスパッチ経由で 到達する。付いていれば、`p`と`v`はループの間ずっと4つのマシンスロットの ままである。逆に、そのアロケーションが問題にならない場所では通常のクラスで 十分である。この性能と引き換えに`@value`が手放すものが、以下の内容である。 手放すのは**同一性(identity)**である。同じフィールドを持つ2つの インスタンスは同じ値であり、プログラムからそれらを区別する手段は存在しない。 §5の値型と参照型の区別をクラスに適用したもので、インスタンスは`Long`が すでにそうであるもの — その中身だけ — になる。コピーと区別できない値で あれば、コンパイラはレジスタに置いても、分解しても、作り直してもよい。 通常のインスタンスは同一性を持つので、2つの名前は同じ1つの可変な実体を 指すことになる: ```culebra class Mutable { x: Float new(x: Float) { self.x = x } } let p = Mutable.new(1.0) let q = p q.x = 5.0 inspect(p.x) # => 5.0 ``` 同じプログラムを`@value`クラスで書くと、最後の行には到達しない。書き込みが 拒否されるからであり、1つの値を指す2つの名前が食い違う余地はこれで消える: ```culebra @value class Frozen { x: Float new(x: Float) { self.x = x } } let p = Frozen.new(1.0) let q = p inspect(p == Frozen.new(1.0)) # => true q.x = 5.0 # !! immutable property 'x' ``` contractは5条項からなり、それぞれがインスタンスの同一性を観測しうる 経路を1つずつ塞ぐ: | 条項 | 検査 | |---|---| | 全インスタンスフィールドが宣言済みで、型は`Long`/`Float`/`Bool`または別の`@value`クラス | 宣言時(`SyntaxError`)と、`new`内の各書き込み(`TypeError`、§10) | | どのメンバも宣言にないフィールドを書かない — 同じ値クラスの2つのインスタンスが違う形を持つことはない | 宣言時(`SyntaxError`)と`new`の実行中(`ImmutableError`) | | `drop`メソッド・フィールドを持たない — デストラクタは*どの*インスタンスが死んだかを観測する | 宣言時(`SyntaxError`) | | `@packable`と併用しない — packedインスタンスは共有バイト列をaliasする | 宣言時(`SyntaxError`) | | `new`が返った時点で凍結: フィールドの書き換え・追加・削除は不可 | 書き込み時(`ImmutableError`) | `new`の中ではインスタンスは通常のオブジェクトなので、コンストラクタは いつも通り**宣言済み**フィールドを代入する。できないのは追加すること: ```culebra # doctest: skip @value class Bad { x: Float new(k: String) { self.x = 1.0 self.z = 2.0 # 宣言時に SyntaxError: `self.z` はクラスが宣言していない # フィールドを書いている self[k] = 2.0 # `new`の実行中に ImmutableError — 算出キーは宣言からは # 見えないので、インスタンス側が拒否する } } ``` 凍結が閉じるのは上の書き込みだけではなく、形そのものである: ```culebra # doctest: skip v.z = 9 # ImmutableError: cannot add property 'z' to a @value instance v.remove('x') # ImmutableError: cannot remove property 'x' from a @value instance ``` `@value`クラスにはフィールドから導出された`eq`と`hash`が付く (`@derive(Eq, Hash)`が生成するのと同じ対)ので、フィールドで一致する キーになり、`==`は構造比較になる。フィールドは宣言されたスカラーで それぞれ型が1つなので、`1`と`1.0`を並べて持つ派生classのように2つが 分かれることはない。分かれるのは`Float`フィールドが`NaN`のときだけで、 そのようなインスタンス2つは`Set`の要素としてもObjectのキーとしても1つに なるが、`==`は偽になる。 通常のクラスが持たないのは`hash`のほうで、これが無ければインスタンスは `Set`の要素にもObjectのキーにもなれない。自前の`__eq__`または`eq`を 書いたクラスはそれを保持し、対の両方を辞退する — これが対の整合を保つ。 それ以外は何も変わらない: メソッド・演算子・getter・static・`match`の 型パターン・`keys()`・表示は、どのクラスとも同じに振る舞う(§10)。 フィールド型を意図的に狭く取っているのは、`String`・`Array`・`Object`・ クロージャが自前の本体や同一性を持つため。別の`@value`クラスを フィールド型に書く場合、そのクラスは先に宣言されているか、stdlibのもの (`Vector2`・`Vector3`。ファイルがその名前で何も宣言していない場合)である 必要がある。 注釈は名前なので、そのファイルがその名前で宣言するクラスはすべて`@value` でなければならない。通常のクラスがファイルのどこかに1つでもあれば、 そのフィールドは拒否される。 アンボックス化は束縛ごとに決まり、all-or-nothingである。フィールド 読み・同クラスの被演算子・再代入のいずれでもない使い方が1箇所でもあると — untypedな関数に`v`を渡す、配列に入れる、など — その束縛はスコープ 全体で通常の経路に戻る。 ### 組み込みデコレータ: `@packable` `@packable`は呼び出し可能値ではなく、クラスを**固定レイアウトの構造体** として印付ける。フィールドはスカラ型注釈と任意のデフォルトを持ち (`x: Float32 = 0.0`)、デコレータがそのバイトレイアウト (C ABIアライン メント) を確定する。`@packable`クラスは、isolate間でzero-copy共有 される`SharedBuffer`の要素型になる — [stdlib §12 SharedBuffer](stdlib.ja.md#sharedbuffer--zero-copy-で共有する固定レイアウトデータ) を参照。 ### `@` matmul 演算子と衝突しない理由 `@`は行列乗算演算子 (PEP 465) としても使われる。両者は決して重ならな い: デコレータの`@`はstatement先頭でしかマッチせず、matmulの`@` は式中の2つのoperand間でしかマッチせず改行を越えない。`}\n@deco fn ...`は前の式のmatmul継続ではなく、独立した2 statementとして パースされる。 --- ## 22. コマンドラインインタフェース culebra [flags] [script.cul | -] [arg ...] culebra [command-flags] スクリプトパスより後の引数はそのまま`Sys.argv`としてスクリプトに 渡されます(`--`不要。[`docs/stdlib.ja.md`](stdlib.ja.md) 参照)。スクリプトより前の単独の`--`は任意のエスケープハッチで、フラグ 解析を止めるため、ダッシュ始まりの名前でも次の引数がスクリプトとして扱われます。 `-`はファイルの代わりに標準入力からスクリプトを読み込みます(例: `curl ... | culebra -`)。この場合`Sys.script`はREPLと同様`nil`になります。 `-`という名前のファイル自体は`./-`のようにパスを明示すれば引き続き参照できます。 ### フラグ | フラグ | 効果 | |----------------|-----------------------------------------------------| | `--shell` | REPLを起動(スクリプト未指定時のデフォルト) | | `--ast` | 実行せず、解析したASTを出力 | | `--debug` | 実行しながらデバッグ診断を出力 | | `--jit` | バイトコードVMではなくLLVM ORC JITを使用 | | `--jit-faststart` | `--jit`と同様だがIRと機械語の両方の最適化を省き、JITのウォームアップ時間を3〜5分の1にする。定常状態の代償はホットな処理のうち最適化器が手を出せる割合で決まる(C++/BLASランタイム側なら約0%、呼び出しの多いスクリプトコードで約30%、スカラー算術のタイトループでは数倍)。`-O0`を含意。出力は`--jit`と一致 | | `--emit-llvm` | `--jit`と併用で生成されたIRを出力して終了 | | `-O0`〜`-O3` | `--jit`の最適化レベル(デフォルトは`-O2`) | | `-h`, `--help` | オプションとコマンドの一覧を出力して終了 | | `--version` | バージョン、開発ビルドならビルド元のコミット、対応バックエンドを出力して終了。リリースタグをクリーンなチェックアウトからビルドした場合はバージョンのみ | ### サブコマンド バイナリはツールチェーンも兼ねています。開発用のサブコマンドは [`tooling.ja.md`](tooling.ja.md)、配布用のものは [`deployment.ja.md`](deployment.ja.md) が扱います。 | コマンド | 効果 | 参照 | |---|---|---| | `test [paths...]` | テストとdoctestを実行(`--filter`, `--doc`, `--reporter`, `--bail`, `--list`) | [`tooling.ja.md` §1](tooling.ja.md#1-テスト-culebra-test) | | `lint [paths...]` | 実行せずに静的エラーと警告を報告(`--fix`は未使用importを削除) | [`tooling.ja.md` §2](tooling.ja.md#2-lint-culebra-lint) | | `fmt [paths...]` | 正準スタイルに整形(`-i` in-place、`-l`一覧、`--check`) | [`tooling.ja.md` §3](tooling.ja.md#3-フォーマット-culebra-fmt) | | `dap` | Debug Adapter Protocolをstdin/stdoutで話す(起動するのはエディタ) | [`tooling.ja.md` §4](tooling.ja.md#4-デバッグ-culebra-dap) | | `build -o ` | AOTコンパイルして単体実行ファイルを生成(バンドルされるモジュールグラフは §24、クロスコンパイル用フラグは`culebra build --help`) | [`deployment.ja.md` §1](deployment.ja.md#1-standalone-バイナリビルドculebra-build) | | `wrap` | 自前のC++ クラスを組み込みとして公開する拡張`culebra`バイナリを作る | [`deployment.ja.md` §3](deployment.ja.md#3-c-ライブラリのラッピングculebra-wrap) | | `toolchain [status\|install\|uninstall]` | `build`がこのホストでリンクするために必要なものを報告・導入・削除する | [`deployment.ja.md` §1](deployment.ja.md#1-standalone-バイナリビルドculebra-build) | スクリプトを指定しない場合、自動的にREPLが起動します。REPLは常に VMのexecutorで動きます — プロンプトの1行がホットループになることは ないため、`--jit`はスクリプト専用で、REPLに付けてもnoteを出すだけ です。セッション状態は入力間で保持され、一つの入力で宣言した`let` / `mut` / `fn`は、ネストしたクロージャからも含め、後続の入力から参照 できます。 受理した入力の評価結果は表示されますが、`nil`のときは表示しません — `println(...)`・`fn`宣言・elseのない`if`はいずれもnilに評価されるため、 表示すると出力が毎行二重になります。プログラム自身の出力(`println`, `print`)はこの規則の影響を受けません。 REPLの入力履歴はセッション間で永続化されます。保存先は `$CULEBRA_HISTFILE`があればそれ、なければ`$XDG_STATE_HOME/culebra/history`、 さらになければ`~/.culebra_history`。1行受理ごとに書き出すため、 セッション途中でクラッシュしても履歴は失われません。 ### CLI が追加するグローバル CLIバイナリはユーザコード実行前に、以下3つのグローバルを スクリプト環境に追加します: | グローバル | エイリアス先 | |------------|----------------| | `inspect` | `IO.inspect` | | `print` | `IO.print` | | `println` | `IO.println` | もっとも頻出する出力呼び出しのための便利なショートカットで、 `IO`配下と**同一の関数値**を指すため`inspect(x)`と`IO.inspect(x)` は完全に等価です。どちらのエンジンも無条件に束縛するので、C++ホストに 埋め込んで動かすプログラムからも、スクリプトと同じ3つの名前が見えます。 --- ## 23. 既知の制約 * 多倍長整数(bignum)はなし。`Long`のオーバーフローはラップする。 * シングルクォート`'...'`はRaw(エスケープなし、補間なし、 アポストロフィを含められない)。`"..."`側のエスケープは限定的で、 `\n \r \t \\ \" \{`のみ(`{{` / `}}`形式は未サポート)。 * `String`はバイト単位のインデックス(`size` / `slice`はバイト数)。 Unicode処理は`code_points()` / `graphemes()`イテレータを使う。 * `Array` / `Object` / `Tuple` / `Set`の等価比較は構造的(値)比較。 参照同一性で比較するのは`Function` / `Tensor`のみ。 * 型注釈は関数境界と注釈付き代入でのみ検査され、言語を静的にする ものではない。 * パターンマッチに網羅性検査はない。 * ドット形式のプロパティ名は識別子のみ(`obj.foo`)。非Stringの ハッシュ可能キー(`Long`, `Float`, `Bool`, `Nil`, `Tuple`, enum variant)は添字パス(`obj[k]`)でアクセスし、サイドカーマップに 格納されます。 ランタイム`String`キーは`obj[k]`経由でもshapeに統合され、 `obj['x']`と`obj.x`は同じスロットに到達します。詳細は §10 「添字代入」を参照。 * `yield`は関数かメソッドの本体に置くもの。ファイルのトップレベル、 `|...|`ラムダ、`new`、`drop`の中ではパース時`SyntaxError` (§11「ジェネレータ」)。 --- ## 24. モジュール 複数ファイルにまたがるプログラムは、ファイル単位の`import` / `export`で繋ぎます。設計は静的かつ最小限 — パスは文字列リテラル、 import/exportはトップレベル限定、実行時に名前を解決し直すことは ありません。 ### モジュール構文 モジュール側は通常のculebraファイル。`export`文で公開する名前を 列挙します: # math_utils.cul let add = fn (a, b) { a + b } let sub = fn (a, b) { a - b } let helper = fn () { ... } # 公開しない class Pair { new (x, y) { self.x = x; self.y = y } } export { add, sub, Pair } 呼出側はモジュールを1つの名前で受け取ります: # main.cul import math from './math_utils.cul' math.add(2, 3) # 5 let p = math.Pair.new(1, 2) math.helper # nil — export Objectに無い ### 文法 | 形 | 意味 | |---|---| | `import NAME from 'path'` | `path`のモジュールを読み込み、そのexport Objectを現在のファイルで`NAME`に束縛 | | `export { N1, N2, ... }` | 列挙した名前をモジュールのexport Objectに追加 | パスはSTRINGリテラル(シングルクォート、補間なし)。import元 モジュールのディレクトリ基準で解決します。絶対パスはそのまま使われます。 ### トップレベルのみ `import` / `export`はモジュールのトップレベルでしか書けません。 関数本体、`if`の枝、ブロック等の内側に書くと文法エラー: let f = fn () { import lib from './lib.cul' # SyntaxError } 依存グラフとexport名の集合をparse時に確定するための制約で、 AOTバンドリングとtree-shaking解析が成り立つ前提になります。 ### 評価規則 1. **依存解決.** ドライバはエントリモジュールを読み込み、`import` 文を辿って依存を再帰収集します。結果はトポロジカル順 (依存が依存先より前)に整列。 2. **モジュール毎のスコープ.** 各依存モジュールは新しいスコープ で評価され、内部のtop-level束縛はそのファイル+export Objectの中だけで見えます。importer側へは漏れません。 3. **キャッシュ.** 同じ絶対パスはプログラム実行中に高々1回だけ 評価されます。以降のimportはキャッシュ済みObjectを取得。 4. **export Object.** モジュール本体終了後、ランタイムが全 `EXPORT_STMT`の名前をひとつのObjectに集めます(プロパティ はimmutable)。複数の`export`文はソース順に**マージ**されます。 5. **エントリモジュール.** 最後に呼出側のスコープと共有する形で 評価され、top-levelのbindingsがそのまま外側に見えます。 ### export について * `export`文が無いモジュールは空のObjectを返します。`import x from "..."`は成功し、存在しないメンバ参照は通常のObject規則 (§10) に従って`nil`を返します。side-effectのみのモジュールに 有用。 * 1ファイルに複数の`export { ... }`を書くとマージされます。 ヘルパを定義→公開、追加で定義→追加で公開、を分けて書けます。 * 同じ名前を2回書くと(1つの`EXPORT_STMT`内でも複数の `EXPORT_STMT`をまたいでも)parse時に`SyntaxError`。 export対象はmodule内で先にlocal束縛として定義されている 必要があります。 ### エラー | 発生条件 | `kind` | |---|---| | `import`先ファイルが存在しない | `IOError` | | import先がparse失敗 | `SyntaxError` | | 循環import (A → B → A) | `ImportError` | | `import` / `export`がトップレベル外 | `SyntaxError` | | 1つの`export`内に重複名 | `SyntaxError` | | `export { foo }`の`foo`がmodule内に未定義 | `NameError` | (importしたnamespaceの存在しないメンバ参照はエラーでは ありません — `mod.unknown`は通常のObject規則 §10と同じく `nil`を返します。) 全エラーは`kind` / `message` / `line` / `col`を持ち、両backend で同一に発生します。`ImportError` (循環依存) は §15のカタログに 追加されたcatchable kindです。 ### 両 backend で同一動作 両backendは任意のモジュールプログラムについて観測可能な振る舞いを 完全に一致させ、依存の繋ぎ方も両エンジンで同じです: 全モジュールの 本体を1つのスコープ分離されたプログラムにコンパイルし、各依存 モジュール本体の後でコンパイル済みコードが `culebra_runtime_module_register`を呼んで絶対パスとexport Objectを 登録。`IMPORT_STMT`は`culebra_runtime_module_get`にコンパイルされ、 同じパスで取り出して束縛します。 モジュールテーブルは絶対パスをキーとする`thread_local` mapなので、 同一スレッド上で複数のJITセッションが走っても互いに干渉せず、 スレッド間で状態を共有することもありません。 --- ## 25. 付録: VM ↔ JIT の差分 本書が規範的です。VMのexecutorとLLVM loweringは同じコンパイラが 生成した同じバイトコードを消費し、すべてのプログラムについて **観測可能な挙動が一致する**ことを要求されます — 戻り値・副作用順・ エラー`kind` / `message` / 位置情報すべて同じ。内部表現は外部観測動作が 一致する限り自由に異なって良く、観測動作のずれはバグとして扱います。 リリース済みバイナリが、ゲートの比較対象となる独立した第二実装です。 ### 観測可能な挙動が一致する点 VM・JIT・AOTすべてのビルドで同一が保証されます: * **数値演算 (§7).** Long overflow wrap、Float IEEE-754 (NaN、inf、符号付きゼロ)、ゼロ除算/剰余の`ZeroDivisionError`、 比較・真偽値ルール。 * **引数評価順.** 位置引数・キーワード引数・`**` splatすべて ソース上の左から右へ。`||` / `&&` / `??`は最初の決定的オペランド で短絡。配列・オブジェクトリテラルも同じ規則。 * **メソッドディスパッチとUFCS(§10)、演算子の特殊メソッド (§10)、 `__str__`表示 (§10)、イテレータプロトコル (§18.5)。** * **`throw` / `try` / `catch` / `defer` (§15).** 関数直下や トップレベルに置いた`defer`もthrow巻き戻し経路で発火します。 JITはLLVM `invoke` / `landingpad` + Itanium personalityに lowerしますが、観測可能な伝播挙動は同一。 * **Auto-drop (§17).** 参照カウントが0になったタイミング(スコープ 退出・プロパティ上書き・配列再代入のいずれでも)で発火。親→子の カスケード順も一致。スコープ所有の循環メンバーも両backendとも スコープ離脱時にdrop。closure保持・トップレベルの循環はGC バックストップで発火(コレクション駆動・順序未規定、§17参照)。 * **エラー報告.** §15に列挙された各`kind`は両backendで同じ 条件で発生し、`e.message` / `e.line` / `e.col`が同一値でpopulated。 未捕捉エラーは`Kind: message at L:C.`形式で表示。 * **`class`構文 (§10)、`static`メソッド、不変な`self`、 自動合成される`parameters()`リフレクション。** * **モジュールスコープ評価.** 文の実行順、トップレベルclosure、 forward-reference解決、デコレータ適用すべて同一ルール。 ### 許可される内部最適化 実行方法は変えますが観測内容は変えません: * **オブジェクトのプロパティストレージ.** 両エンジンがプロセス インターン化されたhidden-class(Shape)レイアウト + ベクトル バックのスロットを共有し、JITはコールサイト単位のinline cache を加えます。キー反復順は両者とも挿入順。 * **HOF inline展開.** `--jit`では`array.map / filter / for_each / reduce`や`range(N).(...)` / `iter.map(λ).collect()`は lambda本体を反復ループ内に直接展開。副作用順は維持されます。 * **クラスメソッドの保管.** メソッドはクラス共有メタオブジェクト に置かれ、プロト委譲で参照されます。`obj.m`はどちらのエンジン でもバインド済み関数を返す。 * **サイクルコレクタの起動タイミング.** 両エンジンは世代別ではない フルのmark-sweepを1つ共有し、適応的な閾値(生存集合とlive bytes から再武装)で走らせます。つまりcollectの**タイミング**は実行間 で違う。プログラムから観測できる挙動は同じで、循環メンバーも pre-sweepパスで`drop`が発火する(§17)。 * **Forward-ref pre-allocation.** 両エンジンとも各関数本体を スキャンして捕捉セルを事前確保し、`let`宣言より前にコンパイル されたclosureも宣言後の値を見られるようにします。 ### 振る舞いに影響しない技術的な差 ユーザコードからは見えませんが、Culebraを組み込む側は知っておく 価値があります: * **サイクルGCの内部方式.** 両エンジンは保守的なスタック走査 mark-sweepを1つ共有します。あらゆる循環形状を回収します (`Object`のプロパティマップだけを経由する循環を含む)。 * **スレッド安全性.** ほとんどのランタイム状態(GC、defer stack、 interrupt flag)は`thread_local`な`Runtime`に 置かれ、別スレッドで動く並行isolate間で共有されません。 プロセス共有の2つのinternテーブル(`ShapeRegistry`とtrait registry)はmutexでガードされています(既にあるShapeはロックを 取らずに引けます)。単一`Runtime`内の実行は シングルスレッドなので、1つの`Runtime`を複数スレッドから駆動する 組み込み側は自前で呼び出しをシリアライズする必要があります。 ### トップレベル drop に関する注意 (§17) `drop`を持つオブジェクトをトップレベルに束縛すると、どちらの エンジンでもプログラム終了まで`drop`を実行せず生存します(§17の 例参照)。エンジンはトップレベルスコープ解放時にdropを抑制します。 どのレーンで走らせるかに関わらず、スクリプト全体のリソースには `defer`かfactory関数を使ってください。 疑わしい場合は本書が正。どのレーンであれ観測可能挙動のずれは バグとして扱い、2つのレーンとリリース済みバイナリとの differentialがそれを検出します。