標準ライブラリ (prelude)
本書は prelude (起動時に root フレームに束縛される組込群) を定義する。言語の意味論は language-spec.md を参照。本書中の §N 形式の参照は、特に断りがない限り language-spec.md の節を指す。
本書で定義する識別子は 2 語以上の場合 snake_case で命名する (例: to_int, cond_block)。
なお test / assert は prelude には含まれず、標準ライブラリモジュール std:test / std:assert (import で取得) として提供する。仕様は test.md・std/test.md・std/assert.md を参照。
prelude が root に常在束縛する組込は原則 flat な名前 (print / range / if …) だが、設計上は namespace object も常在束縛しうる (std:assert / std:* は namespace object だが import 限定で root には常在しない)。prelude はこの形の namespace object を root には持たない。
1. 入出力
各組込 (print / println / eprint / eprintln / input / exit / panic) の形・戻り値・例は自動生成のリファレンス reference/prelude.md を参照。本節はその意味論を定める。
print / println は標準出力、eprint / eprintln は標準エラーへ書き分ける。接尾辞 ln の有無が末尾改行の有無を表す — 素の print / eprint は改行を付けず、println / eprintln は末尾に改行を足す。exit(code) は通常の評価フローを巻き戻してトップで停止し、code をプロセスの exit code にする (回復可能エラーや panic とは別系統で、診断メッセージは出さない)。code が Int でなければ呼び出し位置の panic (§16)。exit は panic と同じく 発散する (呼び出し位置に値として戻らない) ため、結果型は Never (ボトム型、§12) で任意の位置に置ける (match { p => v; _ => exit(1) } のような分岐の穴埋めに使える。Never は全型の部分型なので発散アームは分岐合流で他方の具体型へ寄る)。
panic(message) は明示的に バグ層 へ入る入口で、exit と異なり診断付きでトップ停止し非ゼロ終了する (language-spec.md §16.2)。message が String でなければそれ自体が型不一致 panic になる。想定内の失敗には使わない — ファイル不在などの回復可能エラーは値 (Err(e)、§12) で運ぶ。panic はバグの表明専用である。
1.1 input のエラーモデル
input を ! で待つと Result(String) に resolve する。成功時の line は改行を除いた入力 String。失敗時の Err payload は回復可能エラーオブジェクト {kind, message} (§16)。
| ケース | resolve 値 |
|---|---|
| 成功 | Ok(line)。空行は Ok("") |
| 入力終端 | Err({kind := "eof" …}) |
| その他の読取失敗 | Err({kind := "io_error" …}) |
input が返す正規 kind (閉じた一覧):
| kind | 意味 |
|---|---|
eof |
入力が終端に達した (Ctrl-D / リダイレクト末尾 / パイプ閉) |
io_error |
その他の読取失敗 |
末尾の入力行に改行が無くても、読めた部分はその行として成功 (Ok(line)) で返り、ゼロバイトで終端したときに eof となる。message は input: ... のように関数名を含む人間可読文字列。分岐は match res { Ok(line) => …; Err(e) => match e.kind { … } } で行い、message の文字列マッチに依存しないこと。
2. Boolean メソッド
Boolean のメソッド (not ほか) の網羅表は自動生成のリファレンス reference/prelude.md を参照。
3. 数値 (Int / Float) 演算子・メソッド
Int と Float は別の数値型で、演算子は実行時型でディスパッチする。暗黙の型変換はない — 混合算術 (1 + 2.0) と混合比較 (1 < 2.0) はエラー / panic で、to_float / to_int で明示変換する (language-spec.md §7.1 / §9.4)。変換は方向で非対称: Int.to_float は全域関数で Float を直接返し (範囲超は Inf)、Float.to_int は Option を返す (NaN/Inf は None)。詳細は §3.1 / §3.2 と language-spec.md §7.1。
両者をまとめる型値として Number(= OneOf(Int, Float) の透過的エイリアス、§11)を束縛する。数値多相の関数(std:math の abs / sign 等)は境界付き型変数 a: Number(language-spec.md §17.2)で書く — {x: a: Number, y: a | a} は「両引数が同じ数値型で、結果もその型」を表明し、混在(1 と 2.0)を型エラーにする。単独の注釈 x: Number は「Int または Float」のみで引数間の同型は要求しない。順序演算 min / max / clamp は数値専用でなく Comparable 境界の大域関数(§13.5)。
3.1 Int
- 算術:
+-*/%(§7.1。Int op Int → Int、/は切り捨て) - 比較:
==!=(§9.1) / 順序<<=>>=(language-spec.md §9.4) - Int は Comparable で
compareを持つ (§13。< <= > >=はcompareから導出) - 文字列との
+連結 (§7.2) to_floatは全域関数でFloatを直接返す (Option ではない)。float64の有限範囲を超える巨大 Int はInf(language-spec.md §7.1)
Int のメソッド (to_char / to_float ほか) の網羅表は自動生成のリファレンス reference/prelude.md を参照。
3.2 Float
- 算術:
+-*/%(§7.1。Float op Float → Float、/は実数除算、%は切り捨て除算の剰余で符号は被除数に従う) - 比較:
==!=(§9.1。構造等価でNaN == NaNはtrue) / 順序<<=>>=(language-spec.md §9.4) - Float は Comparable で
compareを持つ (§13)。順序は 全順序 (NaN にも定位置を持ち、負の NaN が最小・正の NaN が最大)。sortが NaN 混入時も壊れない - NaN / Inf は値として存在しうる (オーバーフロー由来など。language-spec.md §7.1)
Float のメソッド (to_int / is_nan / abs / floor / sqrt ほか) の網羅表は自動生成のリファレンス reference/prelude.md を参照。
注:sin/cos/log/exp/pi等の超越関数は本書のスコープ外 (std:mathモジュール。定数は std/math.md §2、関数は同 §4)。
3.3 NonZero / NonZeroFloat (型値)
除数として使える値を表す refinement 型 (language-spec.md §17.7) で、prelude が束縛する。
NonZero # refinement { v: Int | v != 0 } NonZeroFloat # refinement { v: Float | v != 0.0 && v != -0.0 }
/ と % の除数には非ゼロの証明義務がかかる (language-spec.md §7.1・static-analysis.md §2.11「除数の非ゼロ義務」)。この 2 つはその義務を書き手が引き受けるための綴りである — 上流で宣言型を絞る (d: NonZero := …) か、式アスクリプションで実行時へ委ねる (a / (d: NonZero))。条件で検査する形 (if (d != 0) { a / d }) は絞り込みが事実を立てるので、どちらも書かずに済む。
NonZeroFloat が -0.0 も除くのは、-0.0 での除算も実行時エラーであり、Float の -0.0 と 0.0 が全順序では別の値だからである (§3.2)。片方だけの述語では -0.0 が通り抜ける。
基底型の値としてそのまま使える — refinement 型は基底型の部分型なので (language-spec.md §17.7)、d: NonZero に算術も比較もそのまま書ける。
4. String メソッド
文字単位は rune (Unicode code point)。"お".length! は 1、"お".0 は "お" (長さ 1 String)。
String のメソッド (length / is_empty / split / splitn / lines / slice / code / each / map / contains / replace / trim ほか) の網羅表は自動生成のリファレンス reference/prelude.md を参照。要素は長さ 1 の String (= 1 rune) で、Sequence のメソッド (each / map / filter / fold / find / any / all / count / is_empty。language-spec.md §7.4) の map / filter は List を返す (String を返す版は持たない)。
String は Comparable で、辞書順 (Unicode code point 単位) の compare / < <= > >= を持つ (language-spec.md §9.4 / §13)。要素を並べ替えるには to_list で List に開いてから sort* する (§6.6)。
4.1 repeat — 反復連結
"ab".repeat 3 #> "ababab" " ".repeat 0 #> "" " ".repeat (-1) #> ""
s.repeat n は s を n 回連結した新しい String を返す。n <= 0 は空文字列 (エラーではない) — 「不足分を埋める」用途で n が計算結果になることが多く、負を空と同一視すると呼び出し側の下限チェックが要らなくなるため。n が Int でなければ panic。
4.2 to_int / to_float — 数への変換
"42".to_int! #> Some(42) "+5".to_int! #> Some(5) "007".to_int! #> Some(7) "3.14".to_int! #> None # 整数の綴りではない "1_0".to_int! #> None # 桁区切りは受理しない "0x10".to_int! #> None # 読むのは 10 進だけ " 1".to_int! #> None # 前後の空白も受理しない "3.5".to_float! #> Some(3.5) " 3.5 ".to_float! #> None
s.to_int / s.to_float は部分的である。数の綴りでなければエラーではなく None を返す — 外から来た文字列 (入力行・設定値) をそのまま渡せることに意味がある。to_int の桁数に上限は無い (Int は任意精度)。
受理するのは数の綴りそのものだけで、前後の空白は受理しない。 余白を落とすのは呼び手の責務であり、s.trim!.to_int! と綴る。空白を黙って落とす設計は、桁区切りの _ や 16 進の接頭辞を拒む厳密さと噛み合わない — 「一部の非数字は通り、一部は通らない」という中間の規則になり、どちらに落ちるかを呼び手が個別に覚えることになる。
4.3 trim_prefix / trim_suffix — 接頭辞・接尾辞の除去
"/usr/bin".trim_prefix "/" #> "usr/bin" "usr/bin".trim_prefix "/" #> "usr/bin" # 不一致ならそのまま "README.md".trim_suffix ".md" #> "README" "| a | b |".trim!.trim_prefix("|").trim_suffix("|") #> " a | b "
s.trim_prefix p は s が p で始まればその分を取り除いた String を、始まらなければ s をそのまま 返す。trim_suffix は末尾について同じ。除去は 1 回だけで、繰り返し剥がさない ("//x".trim_prefix "/" は "/x")。空の p は常に一致し s をそのまま返す。
不一致時に Option でなく元の値を返すのは、連鎖 (.trim_prefix(…).trim_suffix(…)) を成立させるためである。Option を返す設計では中間結果に名前を付けて .or で合流させる必要があり、この 2 つが解こうとしている「主語を何度も書く」問題 (本書 §16) がそのまま残る。既存の trim が String を返すのとも揃う。
「一致したか否か」を値として観測したい用途には Option を返す別名 (strip_prefix / strip_suffix) が要るが、持たない。strip_* の名は、将来その意味 (一致しなければ None) で導入できるよう空けてある。
5. Bytes メソッド
Bytes は不変のバイト列 (language-spec.md §7.2.2)。s.to_bytes! や std:fs の read_bytes(path) (std/fs.md) で得るほか、Int の List から構築関数 bytes(list) で作れる (Option(Bytes)、範囲外は None・非 Int は panic、空は Some(空 Bytes))。range/Range と bytes/Bytes は「リテラルを持たない型を小文字構築関数で作る」対称。
Bytes のメソッド (length / is_empty / to_string / slice / each / map / contains ほか) の網羅表は自動生成のリファレンス reference/prelude.md を参照。要素は Int (0〜255) で、map / filter は List を返す (Bytes ではない)。Sequence のメソッド は §6 List と同義。
6. List メソッド
List のメソッド (要素アクセス xs.N、length / is_empty / map / filter / fold / sort / take / flat_map / zip / unique / partition / join / unlines ほか) の網羅表は自動生成のリファレンス reference/prelude.md を参照。本節はその意味論・不変条件を定める。
- 非破壊で新 List を返す:
map/filter/sort/sort_by/sort_with/reverse/to_list/take/drop/take_while/drop_while/flat_map/flatten/zip/unique/partitionは元を変えず新 List を返すためclosedlist でも動く。ソートの詳細 (安定性・空 List・key 算出回数・block 制約) は §13.4。 Sequenceのメソッド:find/any/all/count/is_emptyはmap/filter/foldと同じく String / Bytes / Range でも動く (§7.4)。is_emptyはlength! == 0と定義される (独立した測度ではない)。take以降は List 固有 (シーケンスはto_listで List に開いてから使う)。- block 内の制御: 述語・射影
block内のreturnは外側関数から脱出し、break/continueはmap/filterと同じく不可 (エラー)。foldは空ならblockを呼ばずinitを返す。
sort / sort_by / sort_with / reverse / to_list / take / drop / take_while / drop_while / flat_map / flatten / zip / unique / partition は map / filter と同じく非破壊で新 List を返すため、closed list でも動く。ソートの詳細 (安定性・空 List・key 算出回数・block 制約) は §13.4 を参照。
例:
[1, 2, 3, 4].fold(0) { acc, x | acc + x } #> 10 (合計) ["a", "b", "c"].fold("") { acc, s | acc + s } #> "abc" (連結) [].fold(42) { acc, x | acc + x } #> 42 (空は init をそのまま返す)
6.1 スロットの自己記述メソッド (universal)
スロット軸に対する universal method。Sequence の positions / has_position と対をなす。全オブジェクトが持つ。names / values / has_name の形・戻り値は自動生成のリファレンス reference/prelude.md を参照。いずれもスロットを 宣言順 で扱う (values は obj.names().map({ k | obj.[k] }) と等価)。
positions / has_position は §3.10 の動的アクセス recv.[Int] と対応する。names / has_name は recv.[String] の スロット軸のみを担う — recv.[String] はスロットに加えて組込型メソッド・演算子も解決する (§3.2) ため、names / has_name が返す集合は recv.[String] が解決できる名前の真部分集合でしかない (組込型メソッド・演算子は含まれない)。名前で解決できる全てを列挙する手段は言語には無い。「キー = 名前 (String) | 位置 (Int)」という二軸の見方は変わらない。
6.5 range メソッド
range(start, end) は prelude 大域。半開区間 [start, end) の整数列を表す遅延シーケンスを返す (language-spec.md §7.6)。start / end は Int 必須の 2 引数。range(5) は部分適用になる。start / end の受理範囲は実装定義 (int64) であり、超過は実行時エラーになる。
Sequence のメソッド (§6 と同じ length / is_empty / each / map / filter / fold / find / any / all / count / first / last / positions / has_position および r.N 添字) をすべて実装する。map / filter は List を返す。find は Some(Int) / None、any / all は Bool、count は Int。論理添字 i の要素は start + i*step。range 固有メソッド (step / reverse / to_list / contains / index_of) の網羅表は自動生成のリファレンス reference/prelude.md を参照。contains / index_of は実体化せず算術判定する。
例:
range(0, 5).each { i | print(i) } # 0,1,2,3,4 range(0, 10).step(2).to_list() #> [0, 2, 4, 6, 8] range(0, 5).reverse().to_list() #> [4, 3, 2, 1, 0] range(0, 10).map { n | n * n } #> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] range(1, 6).fold(0) { acc, n | acc + n } #> 15 (左畳み込みで合計) range(10, 0).length() #> 0 (空 range)
6.6 to_list (Sequence: List 化)
to_list はSequence のメソッドで、シーケンスを List に実体化する。positions / each / map などと同じ読み取りである (§6.1 のスロットの自己記述メソッドと対をなす)。
| レシーバー | 結果 |
|---|---|
| List | 要素の新 List (コピー) |
| String | 長さ 1 String (rune 単位) の List(String) |
| Bytes | 各バイト (Int 0〜255) の List(Int) |
| Range | 全要素を実体化した List(Int) (§6.5 既存) |
String / Bytes / Range を並べ替えたいときは to_list() で List に開いてから sort* (§6) する ("dcba".to_list().sort().join("") で文字列に戻せる)。
6.7 Sequence (型値)
Sequence は「要素の並びを持つ値」を表す型値で、構造的 interface (language-spec.md §17.5) として prelude が束縛する — Comparable (§13.3) / Copyable (§15) と同じ仕組みの上に立ち、組込の特別型ではない。エントリは §6 が並べる 15 のメソッド (length / is_empty / positions / has_position / first / last / each / map / filter / fold / find / any / all / count / to_list) である。
v matches Sequence は List / String / Bytes / range で true、それ以外では false。タプルは一致しない — 持っているのは t.N と length だけなので (§7)、length だけを求める { length |} には一致するが Sequence の全体には届かない。オブジェクトも一致しない — オブジェクトが持つのは名前で引くスロットである (§6.1)。
[1, 2] matches Sequence #> true "ab" matches Sequence #> true (1, 2) matches Sequence #> false (length しか持たない) (1, 2) matches { length |} #> true
7. Tuple メソッド
Tuple のメソッド (要素アクセス t.N・length) の網羅表は自動生成のリファレンス reference/prelude.md を参照。is_empty は生えない — Tuple は固定長で、唯一の空値が () であり判定に意味が無いため (§6 のSequence のメソッド からは意図的に除外)。
8. 制御構造
if / when / while / break / continue / return は予約語ではなく prelude が束縛した特殊オブジェクトである。「特殊」とは実装が組み込み (Hikari ではなく処理系の実装言語で書かれている) という意味のみで、評価規則は他の関数と同じ。引数は呼び出し前に評価され、ブロックを渡せばその評価は遅延される。適用規則も関数と同じ — 未束縛スロットが 1 つなら複数引数はタプル 1 個として丸ごと束縛され、2 つ以上なら数の一致が要る(不一致はエラー)、省略された optional 引数はデフォルトで補われる(§3.6)。return / break の多値は return(a, b) / break(a, b) でよい(タプル 1 個の適用)。
when のような conduit 系制御構造(ブロック起動を if 等の builtin に委譲し、自身は return/break/continue を素通しするだけ)は Hikari でも定義できる — conduit 注釈(language-spec.md §18.3)を付けた関数は呼び出し境界で制御値を捕捉せず透過するため、when := conduit {cond, then | if cond then {()}} のように書ける(透過は builtin の特権ではない)。本書が builtin として挙げる when 等もこの形で self-host へ移しうる。一方 while のように break/continue を消費するループそのものは別機構(loop + capture、language-spec.md §18.4)が必要で、builtin(native)として提供する。
分岐 dispatch は match(§8.4)が担う。これは prelude 束縛の特殊オブジェクトではなく、予約語=言語コアの構文形(§1.4)— 値でなく構文である。match subject { … }(構造照合+述語ガード)と subjectless match { … }(独立述語の短絡連鎖)の両形を持つ。型テスト matches は据え置きの universal method(全値に生える。§17.4)。
各制御構造 (if / when / while / break / continue / return) の形・例は自動生成のリファレンス reference/prelude.md にもある。以下 §8.1〜§8.7 でそれぞれの意味論を定める。
8.1 if
if := { cond, then, else | ... } # cond/then/else すべて必須
呼び出し形:
if cond { then_block } { else_block } # 並置 + 短縮ブロック (段階 curry。推奨) if(cond, then_block, else_block) # タプル (cond, then, else) を一括適用
並置形が成立するのは language-spec.md §3.5 の curry 規則による: if x → cond=x で部分適用、(if x) {y} → then={y} でさらに部分適用、(if x {y}) {z} で else={z} を埋めて本体評価。
両形は評価結果が同じだが、並置形が idiomatic。タプル形も有効な構文だが、hikari lint の control-juxtaposition ルールが並置形を推奨し、hikari lint --fix で並置形へ変換する (lint.md)。
例:
# 1 行: 並置形が基本 result := if (x > 0) { "pos" } { "non-pos" }
条件が中置演算子 (==、<、+ 等) を含む場合は条件を (...) で囲む。 並置 (優先順位 8) が比較・算術 (優先順位 4〜6) より強く結合する設計から来る制約 (language-spec.md §8.3)。if x == 0 {y} {z} のように書くと ((if x) == 0) {y} {z} 方向にパースされ意味が変わる。
複数行でも } と次の { を同一行に置くこと。 then ブロックや else ブロックの中身は自由に複数行に渡って書けるが、then を閉じる } と else を開く { の間に改行を挟むと、その改行が文区切りとして働き、else 側の並置が断ち切られる。
# OK: ` } { ` は同一行 if (x == 0) { print "a" bar() } { print "b" qux() } # NG: `}` の直後で改行すると else 側が独立式扱いになる if (x == 0) { print "a" } { print "b" }
タプル形 if(cond, {...}, {...}) は () の中なので改行が文区切りにならず } と { を別の行に書けるが (language-spec.md §1.3)、hikari lint は並置形を推奨し、--fix で } { 同一行の並置形へ変換する。
elseif は else ブロック中で再度 if を呼ぶ:
if (x > 0) { "pos" } { if (x < 0) { "neg" } { "zero" } }
8.2 when
else を省略したいときは when を使う:
when := { cond, then | if cond then { () } { () } }
呼び出し:
when (x == 0) { print "zero" } when (x > 0) { print "positive" } result := when found { value } # 非成立時 result は () (unit)。maybe 値が要るなら if found { Some(value) } { None }
when は非成立時に () を返すため、「成立時だけ値を加工し、非成立時は元の値を通す」用途には使えない。そちらは apply_when (本書 §17) が担う — if cond { f x } { x } は x.apply_when cond f と書ける。
8.3 while
while := { cond_block, body_block | ... }
条件もブロックで渡す (繰り返し評価のため)。
mutable i := 0 while { i < 10 } { print i i = i + 1 }
i は再代入するので mutable で宣言する (language-spec.md §6.1)。i := 0 だと immutable となり i = i + 1 がランタイムエラーになる。戻り値は () (unit)。
8.4 match
match subject { arms } # subjectful(構造照合)
match { arms } # subjectless(述語連鎖)
match は予約語(§1.4)で、値ではなく言語コアの分岐構文(universal method ではない。型テスト matches は据え置きの universal method)。subject(対象値)を各アームの パターンに構造照合し、最初に一致したアームの body(=> の右)を評価してその値を返す。どのアームにも一致しなければ () (unit) を返す。値の等価・型・タプル・レコード・値コンストラクターの形で分岐でき、パターンに述語ガードを重ねられる単一のディスパッチ構造。subject を省いた subjectless 形は独立述語の短絡連鎖を担う(旧 cond を吸収)。
- アーム:
{ }内にアームを改行区切りで並べる。各アームはpattern [| guard] => body形。=>の左がパターン(+任意のガード)、右が分岐先 body。 - OR はカンマ:
pattern1, pattern2 => body。対象がいずれか1 つに一致したら body(前から最初の一致で確定)。0, 1, 2 => …はOneOf(0, 1, 2)相当。タプルは括弧(a, b)、OR は括弧なしカンマで区別する。 - 全枝が同じ名前を束縛しなければならない。 どの枝が当たっても body から見える名前が同じでなければ、body は自分が読める名前を静的に決められない。枝ごとに違う名前を束縛する形(
{ a }, { b } => …)は静的エラーである(static-analysis.md §2.9)。 - 束縛を行わない枝同士の OR(
0, 1, 2/None, Empty)は制約を受けない。束縛が 1 つも無いことも「全枝で一致」である。 なおname := vは照合しつつ束縛もするので、{ a := 1 }, { b := 2 }は枝ごとに違う名前を束縛する形にあたる(値で振り分けたいだけならアームを分ける)。 - ガード
| guard: パターンが一致し束縛が入った後にguardを評価する遅延述語。Bool 厳格(§9.3。非 Bool は panic)。偽なら次のアームへフォールスルーする(body 内ifと違い別アームへ落ちられるのがガードの意義)。ガード式内で束縛(Some(n)のn等)を参照できる。 - catch-all
_ => body: どのアームにも一致しないとき選ばれる default。_はワイルドカード(専用文法が所有するので[]の値参照にはならない)。最初の 1 つを採用する。 - 型付き束縛
name: T => body: 対象全体を型Tにmatches(language-spec.md §17.4)で照合し、一致したらname := 対象値を body に束縛する。無制約の束縛はname: Any(Anyは全値に matches)。 - パターンの種類(対象全体に照合):
- 型値(
Int/Float/String/Bool/Unit/Any/List(T)/OneOf(A, B)/Option(T)/Result(T)/Future(T)/ shape):matches(language-spec.md §17.4)で照合。束縛しない(型で絞りつつ束縛するなら型付き束縛name: T)。 - タプルパターン
(p₁, …, pₙ): 対象が同 arity n のタプルで、各要素が対応サブパターンpᵢに一致したとき一致(要素ごと再帰照合)。各要素は型値→matches/ 素の値→==/Ctor(…)・レコード→再帰照合。arity 不一致・非タプル対象は(エラーでなく)不一致で次アームへ。(Int, Int)は「対象がそのタプル型に一致するか」と同結果(要素ごとmatches= タプル型照合の一般化)。 - 素の値(
5/"text"/None/ 計算した変数値):==(language-spec.md §9.1)で照合。ユーザー定義型に==スロットを定義すれば独自の等価性で dispatch できる。束縛しない。 - レコードパターン
{ field… }(末尾パイプ不要 —matchは専用文法で body(=>の右)と曖昧にならない): 対象の同名スロットを構造照合し、一致したら束縛を body に導入。各 field エントリ: name: 対象が slotnameを持てばname := 対象.nameを束縛(制約なし)。name := v: 対象の slotnameがvと==(§9.1)で一致すれば束縛。name: T: 対象の slotnameが型Tにmatchesすれば束縛。- スロット不在・制約不一致のいずれかでそのパターンは不一致(次のアームへ)。対象が非オブジェクトでもスロット不在で安全に不一致。
- 値コンストラクターパターン
Ctor(pat…)(または無引数Ctor): タグ名で判別し、各 payload 位置をサブパターンpatに再帰照合する。Ctorは bare タグ(destructure 済み、または prelude が先取り公開したSome/Ok/Err等)でも、修飾コンストラクターパターンName.Tag(enum 型値の下のタグを直接参照する形。language-spec.md §17.4)でもよく、両者は同じ被覆・照合規則に従う(Circle(r)≡Shape.Circle(r)、Some(x)≡Option.Some(x))。payload 位置のpatは次のいずれか: - 小文字始まりの bare 識別子 (
x/v/e): その payload を新規束縛する (Some(x)/Ok(v)/Err(e))。_は判別のみで破棄 (Some(_))。 - それ以外 (大文字始まりのタグ
None/Empty/Shape.Empty、入れ子の値コンストラクターCtor(…)、素の値5/"s"、レコード{…}、型値): その payload へのサブパターンとして本リストの規則で照合する (束縛は内側の小文字 bare 識別子だけが行う)。 - 無引数の bare タグ
Circle/ 修飾タグShape.Circle(payload 付きタグ)は payload を問わないタグ照合(束縛しない。c matches Shape.Circleと同義、language-spec.md §17.4)。payload 無しタグ(None/Empty)ではタグ照合と==が一致する。
入れ子は任意の深さで書ける (Ok(Some(x)) / Ok({status := 404}) / Some(Circle(r)))。payload 位置で「大文字始まり=タグ/値照合・小文字始まり=束縛」と分ける (この区別は payload 位置でのみ働く)。実体はタグ付きデータのタグを使ったレコードパターンの糖衣で、ユーザー定義タグ付きデータにも同形で開ける。
- subjectless(
match { arms }): subject が無いのでアーム頭はパターンでなくガード(Bool 式)。上から短絡評価し、最初に真になったアームの body を返す(_は catch-all)。どれも真でなく_も無ければ()。独立述語の順次評価(旧cond)を担う。 - subject の曖昧性:
matchと{の間に式があれば subjectful、無ければ subjectless。波括弧で始まる/末尾に波括弧を juxtapose する subject は括弧化する(match ({ … }) { … })。match {は常に subjectless。subjectful のアーム頭は常にパターン(Bool 式を書くと==照合、ガードは|の右に置く)。 - 走査は前から順、最初の一致で打ち切り。body は遅延評価(一致したアームだけ評価)。アーム body は定義位置のスコープを捕捉する(外側の変数が見える)。型で絞った値全体に名前を付けたいときは型付き束縛
name: T => …を使う。 - body を省略したアーム は自分自身と等しい値を返す(language-spec.md §2.2)。分岐結果を返したいアームには必ず body を書く。
# 値ディスパッチ(素の値は == で比較。OR はカンマ) sign := { n | match n { 0 => "zero" "a", "e", "i", "o", "u" => "vowel" # OR _ => if (n > 0) { "pos" } { "neg" } # catch-all } } # 型ディスパッチ + ガード(束縛にさらに述語を課す。偽なら次 arm へ) classify := { v | match v { Int | v > 100 => "big-int" Int => "int" String => v.length! List(Int) => v.fold(0, { a, b | a + b }) _ => "other" } } # 型付き束縛(型で絞った値全体に名前 n を付ける) classify2 := { v | match v { n: Int => n + 1 # n := v(Int に絞った値) s: String => s.length! # s := v(String に絞った値) _ => "other" } } # レコード分解 + 束縛(末尾パイプ不要) area := { shape | match shape { { kind := "circle", r } => 3 * r * r # r を束縛 { kind := "rect", w, h } => w * h # w, h を束縛 _ => 0 } } # subjectless(旧 cond)— 独立述語の短絡連鎖 grade := { n | match { n >= 90 => "A" n >= 80 => "B" n >= 70 => "C" _ => "F" } }
レコードパターンは { field… } で書く(match は専用文法なので末尾パイプは不要)。
値コンストラクターパターン(Option / Result)の例:
match opt { Some(x) => use(x) None => default } match res { Ok(v) => process(v) Err(e) => handle(e) }
ユーザー定義タグ(enum。language-spec.md §17.4)も同形で開ける。タグは Shape の下に置かれるため、修飾コンストラクターパターン Shape.Tag(...) で直接書けるほか、名前 destructure(language-spec.md §6.3)で bare 化すれば従来どおりのパターンで書ける:
enum Shape := OneOf(Circle(Int), Rect(Int, Int), Empty) match s { # 修飾コンストラクターパターン(destructure 不要) Shape.Circle(r) => r * r * 3 Shape.Rect(w, h) => w * h Shape.Empty => 0 } { Circle, Rect, Empty } := Shape match s { # destructure 後は bare パターンも同様に書ける Circle(r) => r * r * 3 Rect(w, h) => w * h Empty => 0 }
payload 位置はサブパターンなので、入れ子の値コンストラクター・素の値・レコードを任意の深さで照合できる:
# Result(Option(T)) を 1 つの match で開く(内側 Some は大文字始まりなのでタグ照合、x は束縛) match res { Ok(Some(x)) => use(x) Ok(None) => empty Err(e) => fail(e) } # payload の素の値・レコードで絞る match shape { Some(Circle(0)) => "unit circle" # 入れ子の素値サブパターン(== で 0 を照合) Some(Circle(r)) => area(r) Some({ kind := "rect", w, h }) => w * h # 入れ子のレコードパターン _ => 0 }
タプルパターンは複数の値を 1 つの match で平坦に分岐できる(各要素は素の値なら ==、型値なら matches)。method × path のルーティングを入れ子なしで書ける(subject が ( 始まりなので括弧化は不要):
match (req.method, req.path) { ("GET", "/") => ok("hello") ("GET", "/todos") => ok(todos) ("POST", "/todos") => created _ => not_found }
8.5 break / continue
ループの反復制御。break は囲っているループを抜け、continue は今の反復を打ち切って次の反復へ進む。
| 構文 | 意味 |
|---|---|
break! (≡ break()) |
ループを抜ける。ループ式の戻り値は () |
break value / break(value) |
ループを抜け、ループ式全体の戻り値を value にする |
break(a, b)(break((a, b)) も同義) |
ループを抜け、戻り値をタプルにする(多値。return と対称) |
continue! (≡ continue()) |
今の反復を打ち切り、次の反復へ進む |
break / continue は予約語ではなく prelude が束縛した値であり、if / while と同じ適用規則で呼ぶ。単独参照 break は値が返るだけで発火しない — 発火には引数なし呼び出しの postfix ! (language-spec.md §3.2、v! ≡ v()) が要る。break value は juxtaposition による適用で、value が中置演算子を含むときは language-spec.md §8.3 の if と同じ括弧制約があり break(x + 1) と囲う。
有効な構文範囲: while と、Sequence の反復メソッド each (String / Bytes / List / Range の s.each(block)、§6〜7) のブロック内でのみ使える。map / filter は結果リストの意味が壊れるため対象外 (使うと panic)。
スコープは字句的 (lexical)。break / continue は それを字句的に囲っているループのブロックにのみ効く。ループ本体から呼び出した別の関数の中で break しても、その関数を字句的に囲うループは無いため panic になる (関数定義境界を越えない)。「呼ばれた文脈のループを遠隔で切る」動的脱出は持たない (詳細と設計判断は language-spec.md §18.1)。
mutable i := 0 while { i < 10 } { i = i + 1 when (i == 3) { continue! } # 3 だけ print を飛ばす when (i == 7) { break! } # 7 で while を抜ける print i } #> 1 2 4 5 6 # each でも効く (字句的に each のブロック内なので OK) [10, 20, 30].each({ x | when (x == 20) { break! } print x }) #> 10 # 値つき break — ループ式全体がその値になる result := while { true } { when found { break(item) } # while 全体が item を返す }
ループの外 (字句的に囲うループの無い箇所) での break! / continue! は panic (バグ層、language-spec.md §16.2)。
8.6 return
関数 (body) から値を返して脱出する。body の任意の位置に書け、末尾の return v は v を置くのと等価 (§11.1)。意義は末尾でない位置から抜けられること (早期脱出の用途)。戻り値は省略可。
| 構文 | 意味 |
|---|---|
return! (≡ return()) |
関数を抜ける。戻り値は () |
return value / return(value) |
関数を抜け、戻り値を value にする |
return(a, b)(return((a, b)) も同義) |
多値タプルで抜ける(タプル 1 個の適用) |
return は予約語ではなく prelude が束縛した値であり、break / if と同じ適用規則で呼ぶ。単独参照 return は値が返るだけで発火しない — 発火には引数なし呼び出しの postfix ! (language-spec.md §3.2、v! ≡ v())、または値の適用が要る。return value は juxtaposition による適用で、value が中置演算子を含むときは language-spec.md §8.3 の if と同じ括弧制約があり return(x + 1) と囲う。多値は return(a, b)(タプル 1 個の適用。return((a, b)) も同義)。, がタプル構築のため括弧なしの return a, b は (return a), b と解釈される。
スコープ: return は最も近い、実呼び出しで入ったユーザー関数 {...} の境界から脱出する。if / when / while / each のブロックと match のアーム body は呼び元 (ビルトインまたは match の評価) が呼ぶため透過で、その中の return は外側の関数から脱出する (break が if を貫いてループに効くのと同一)。while / each 本体の return はループでなく外側関数から脱出する (break はループだけ抜ける)。トップレベル (暗黙 root リテラル本体) の return はモジュール / スクリプト評価を終了する。詳細と設計判断は language-spec.md §18.2。
# 逐次の失敗 bail (Result)。`?` 演算子 (language-spec.md §16.5) はこの定型の糖衣で、ここは下地の return を示す process := { path | match read(path)! { Err(e) => return Err(e) Ok(content) => match parse(content) { Err(e) => return Err(e) Ok(cfg) => Ok(cfg) } } } # ガード節 (その場で新しいエラーを作って抜ける) classify := { n | when (n < 0) { return "neg" } when (n == 0) { return "zero" } "pos" } # ループ本体の return は外側関数から脱出 (break はループだけ抜ける) find_even := { xs | xs.each({ x | when (x % 2 == 0) { return Some(x) } }) None }
8.7 述語チェーン分岐(subjectless match)
独立した述語を上から短絡評価する多分岐は、subjectless の match { … }(§8.4)で書く(旧 cond を吸収。専用構文が導入された 8.4 以前は prelude 束縛の cond が担っていた)。if は else-if を持たず右ネストするため、平坦な述語多分岐はこの形を使う。逆に単一の値の形・型で分岐するなら subjectful の match subject { … }。
grade := { n |
match {
n >= 90 => "A"
n >= 80 => "B"
n >= 70 => "C"
_ => "F"
}
}
アーム頭の述語は Bool 厳格(§9.3。非 Bool は panic)、上から最初に真になったアームの body で確定・以降は評価しない(短絡)。body 内の break / continue / return は match を貫いて外側のループ / 関数に効く(language-spec.md §18.1 / §18.2)。
9. 非同期評価
各組込 (fork / spawn / sleep / now / wait_any / all / race) と Future メソッドの形・戻り値は自動生成のリファレンス reference/prelude.md を参照。以下 §9.1〜§9.9 でスケジューラー上の意味論を定める。fork (§9.1) は可変状態を共有する協調フロー、spawn (§9.9) は捕獲を複製して切り離す隔離並列フロー。
Future 自身のメソッド .map / .and_then (合成オペレーター) は §9.6。これらが乗るスケジューラーの意味論 (フロー切り替え規則・root 終了時の未解決 Future の扱い・失敗伝播) は language-spec.md §10 を参照。
9.1 fork
fork := { block | ... } # 必須スロット 1 個、組み込み実装
blockは必須スロット 0 個の本体付きオブジェクト ({body}形)- 呼び出し時に block をスケジューラーに登録し、即座に
Futureを返す。別フローでの評価は明示的 kick (block!) と同じ意味論 — デフォルトを持つスロットは補完される (language-spec.md §3.6) fork(block)、fork {body}、{body}.fork!は等価 (language-spec.md §8.1)- 引数検証 (fail-fast): 明示的 kick が panic する値 — デフォルトを持たない必須スロットが未束縛で残るオブジェクト (language-spec.md §3.6。本体の有無を問わない) — を渡した場合、
forkはFutureを生成せず呼び出し自身が panic する。評価前に判定できる構造的不整合を別フローの遅延 panic として運ばない (§9.5wait_anyの空 List panic と同種の即時検証)。kick が成功する値 (全デフォルトの block、組込や data object 等の R=0 値) の挙動は変わらない
Future 値の振る舞い:
f!(=f()) で block の評価結果を返す。未完了なら現フローを中断、完了後に値を返す- 同一
Futureへの複数回!は同じ値を返す (block 本体評価は 1 回のみ)。block が panic した場合は panic も memoize され、各!が同じ panic を再現する (language-spec.md §10.2) - 本体付き値なので language-spec.md §3.6 ルール 3 (本体評価) で動く。ルール 5 (本体なしオブジェクトの self 返却) には該当しない
Future同士の==は参照同値 (language-spec.md §9.1)
失敗の扱い: 回復可能エラーは block が Err(e) を 返す 形で表し、input 等と同じく Future が運ぶ通常の Result 値となる (§9.4)。block 評価中の panic (バグ層) は await の ! で取り出した消費側フローへ伝播する (language-spec.md §10.2)。
fork は可変状態を共有する協調フローで並列化しない。捕獲を複製して切り離し真に並列に走らせたい場合は spawn (§9.9) を使う。
9.2 sleep
整数 ms ミリ秒だけ現フローを中断する。返り値は () (unit)。現状は sleep と「未完了 Future への !」だけが中断点 (フロー切り替えが起こりうる点) となる。
9.3 now
現在時刻をミリ秒の整数で返す。中断しない。now! で呼ぶ。
基点は UNIX epoch で、std:fs の mtime および std:time の from_epoch_ms と同じである。値は符号付きである — epoch 以前へ合わせた時計は負の値を返す。0 は 1970-01-01T00:00:00Z という答えであって「時計を読めなかった」の申告ではない。
分解能と単調性は定めない。now! は壁時計なので、時刻合わせで前後に飛びうる。経過時間を測る用途でも壁時計しか無いので、下の例のような差分は飛びの影響を受ける。
タイミング測定の例:
# (a) 直列 vs 並列 mutable start := now! sleep 100; sleep 100; sleep 100 elapsed_serial := now! - start #> 約 300 start = now! # 既存の可変を更新 fs := [fork { sleep 100 }, fork { sleep 100 }, fork { sleep 100 }] fs.each({ f | f! }) elapsed_parallel := now! - start #> 約 100 # (b) 別作業との重ね start = now! # 既存の可変を更新 f := fork { sleep 200; 42 } sleep 100 v := f! total := now! - start #> 約 200 # (c) Result によるエラー (I/O の典型) io_fut := fork { try_io! } match io_fut! { Ok(v) => use(v); Err(e) => print e.message }
9.4 ブロッキング I/O と Future
ユーザー入力やファイル I/O のような「呼び出し時点で結果が確定しない外部待ち」を行う組込は、即座に Future を返し、! を当てた時点で resolve を待つ形にする。これにより language-spec.md §10.1 の規則 (a) (未完了 Future への !) に自動的に乗り、scheduler token を握りっぱなしにせず他フローが進行できる。
該当する組込は本書の input (§1)、および標準ライブラリ std:fs の read / read_bytes / write・std:term の read_key (std/index.md) である。後者は prelude ではないが、ブロッキング I/O を Future で表す方針は共通する。これらはいずれも resolve 値を単一 Result に揃えており、成否分岐の書き方 (match res { Ok(v) => …; Err(e) => … }) が組込をまたいで一致する。
9.5 wait_any
wait_any := { futures | ... } # 必須スロット 1 個、組み込み実装
futuresはFutureの Listsleepと同じく、呼ぶと現フローを中断する組込 (中断点)。最初の入力が解決するまで現フローを中断し、解決した入力Future自身 を直接返す (wait_any自身に!は不要。返るのは値ではなく単層のFuture)- 勝った
Futureの値は通常どおり!で取り出す。勝者は解決済みなので即返る - どの入力が勝ったかは参照同値
==(§9.1) で判別する
win := wait_any([key_fut, tick_fut]) if (win == key_fut) { byte := win!.unwrap! # 勝者から値を取り出す (解決済みなので即返る) } { # tick_fut が勝った }
- 負けた未解決
Futureは変化しない。呼び出し側が参照を保持していれば生存し、後で!/ 再wait_anyできる (持ち越し)。これによりイベントループで「読みかけ」を取りこぼさない
mutable key_fut := read_key() while { running } { win := wait_any([key_fut, fork { sleep 16; () }]) if (win == key_fut) { byte := win!.unwrap! key_fut = read_key() # 次のキーを張り直す } { render() # tick が勝った。key_fut は未解決のまま持ち越し } }
- 単発の timeout は
.timeout(ms)(§9.6) を使う。wait_anyを直接使う形でもwait_any([work, fork { sleep 1000; () }])のようにsleepするFutureと多重待ちして合成できる
エッジ:
- 入力が空 List のときは panic (永久ブロックさせない)
- 要素が
Futureでないときは panic - 呼び出し時点で複数の入力が既に解決済みなら、入力 List の 最小位置 の
Futureを返す (決定的)。同一Futureを複数位置に置いた場合も最小位置 wait_any自身は負けフローを停止しないが、負けたFutureを保持していれば.cancel!(§9.8) で停止を要求できる
9.6 合成オペレーター (.map / .and_then / .timeout / all / race)
Future を組み合わせる合成オペレーター。いずれも fork / wait_any / ! の上の糖衣として定義され、失敗伝播は §9.1・language-spec.md §10.2 をそのまま継承する。中で ! を使うため、要素が panic していれば合成結果の Future を await した時点で surface し、要素が Err(e) (回復可能) なら通常の値として block / List に渡る。fork と同じく結果の Future は root 完了時のキャンセル対象に登録される (§13.3)。
Future のメソッド (レシーバー f は Future):
f.map { v | g(v) } # f の解決値 v に g を適用した新 Future。≡ fork { g(f!) } f.and_then { v | fut } # block が Future を返す flatMap。入れ子を平坦化。≡ fork { g(f!)! } f.timeout(ms) # ms 内に解決すれば Some(v)、超過すれば None を運ぶ Future
.map:fの解決値に block を適用する (functor map)。fがErr(e)に解決していれば block はErr(e)をvとして受ける —FutureはResultではないため、Result の.map(Ok の payload だけを写す) とは異なり解決値そのものを写す。Ok だけに作用させたいときはf.map {r | r.map {v | …}}のように合成する。fが panic していれば block を呼ばず panic を伝播する。.and_then: block はFutureを返す。結果は平坦化されFuture(Future(T))にはならない。block がFuture以外を返した場合は await できず panic (バグ層)。fの panic 時は block を呼ばず伝播する。.timeout(ms): 整数msミリ秒だけfの解決を待つFuture(Option(T))を返す。f.timeout(ms)!で取り出し、ms内に解決すればSome(v)、超過すればNone。fがms内に panic していればSomeで包まず panic をそのまま伝播する (await 点で surface)。タイムアウトしてもf自身は自動では停止されず走り続ける (wait_any/raceの敗者と同じ。止めたいならf.timeout(ms)!がNoneの後にf.cancel!する。§9.8)。wait_any([f, fork { sleep ms; () }])の合成を糖衣化したもの。- ブロック内の制御値は誤用:
.map/.and_thenのブロックは別フローで遅延起動されるため、return(構文糖の?を含む) やbreak/continueが現れると panic する (language-spec.md §18.2)。ブロックを取る他の組込メソッドと違い外側の関数へは脱出させない — 起動時点で外側の関数は既に戻っており、素通しすると!を書いた無関係な関数が代わりに脱出してしまう。この panic も他の失敗と同じく!を当てた消費側フローで surface する。Resultを運ぶFutureの Ok だけに作用させたいときはr?ではなく上記のブロック内合成 (f.map {r | r.map {v | …}}) を使う。 Result/Optionの.map/.and_then(prelude.md §12) と同じ functor / monad の語彙をFutureでも踏襲する。
prelude 関数 (引数 futures は Future の List):
all([f0, f1, f2]) # 全解決を待ち [v0, v1, v2] を運ぶ Future。≡ fork { futures.map { f | f! } } race([f0, f1]) # 最初に解決した値を運ぶ Future。≡ fork { wait_any(futures)! }
all: 入力順に各Futureを await し、値の List を運ぶFutureを返す。all(fs)!で List を取り出す。要素が panic していれば (入力順で最初のものが) 合成Futureの await 時に surface する。空 List は[]に解決する。race: 最初に解決したFutureの値を運ぶFutureを返す。race(fs)!で値を取り出す。wait_anyが勝者Future自身を返す低レベル primitive (敗者の持ち越し・==判別が要るイベントループ向け) なのに対し、raceは勝者の値を返す高レベル糖衣。空 List はwait_anyと同じく panic する。敗者はwait_any同様に自動では停止されず走り続けるが、そのFutureを保持していれば.cancel!(§9.8) で停止を要求できる。
9.7 状態問い合わせ (is_ready)
f.is_ready! # f が解決済みなら true、未解決なら false
Futureのメソッド。fが解決済みならtrue、未解決ならfalseを返す。- 中断しない (
nowと同様に scheduler token を握ったまま即座に返る)。!(= await) が未完了時に現フローを中断して resolve を待つのに対し、is_readyは待たずに完了状態だけを問い合わせる。ポーリングや「!すると中断するかの事前判定」に使う。 is_readyがtrueを返しても、f!の解決値がOk/Err/ panic のいずれかは別問題 (本メソッドは解決済みか否かのみを見る)。
9.8 キャンセル (cancel)
f.cancel! # f を生んだフローのキャンセルを要求する。返り値 ()
Futureのメソッド。fを生んだ別フローのキャンセルを要求する。返り値は()。root 完了時の一括キャンセル (language-spec.md §13.3) と同じ機構をユーザーが明示起動できるようにしたもの。- 協調スケジューラーなのでキャンセルは中断点で観測される (プリエンプティブではない)。観測規則 (未開始フロー・待機中フロー・CPU バウンドの扱い) と診断との関係は language-spec.md §10.3。
- キャンセルされたフローの
Futureは panic で解決し、f!で await するとその panic が surface する (§10.2 の失敗伝播に乗る)。回復可能エラーErr(e)ではない (Result の値ではなくバグ層と同じ経路で運ぶ) ため、fork { 42 }の型はFuture(Int)のまま。通常は cancel-and-forget (キャンセルしたものは!しない) で使う。 - 冪等: 同じ
Futureへの複数回.cancel!、および既に解決済みのFutureへの.cancel!は no-op (解決値はそのまま)。
wait_any / race の敗者など「走り続ける負けフロー」も、その Future を保持していれば .cancel! で停止を要求できる (§9.5・§9.6)。
9.9 spawn
spawn := { block | ... } # 必須スロット 1 個、組み込み実装
fork (§9.1) と同じく block を別フローで評価開始し Future を返すが、fork が可変状態を共有する協調フローなのに対し、spawn は隔離された並列フローを作る (language-spec.md §10.1 の「隔離フローの並列実行」)。
spawn(block)、spawn {body}、{body}.spawn!は等価 (language-spec.md §8.1)。blockは必須スロット 0 個の本体付きオブジェクト。戻り値Futureの振る舞い (f!で待機・複数回!は同値・panic の memoize・==は参照同値) は §9.1 と共通。- 隔離 (捕獲の複製):
spawnは評価開始前に、blockが到達可能な可変状態を複製 (language-spec.md §9.5 のcopy) して呼び出し元から切り離す。ゆえに子フロー内の変異は親から観測できず、その逆も無い。複製の深さ・循環と内部エイリアスの保存・default 式を再評価しないことはcopyの意味論 (language-spec.md §9.5) に従う (不変部は共有・可変部だけ作り直し)。ただし静的に移譲と判定された捕獲では、非 Copyable な実体に達したところだけ複製せず共有する (下記)。 - 非 Copyable 捕獲は panic:
blockが非 Copyable な値 (Futureや不透明な外部ハンドル。language-spec.md §9.5 / §17.5) を到達可能な位置に捕獲していると隔離を保証できないため、spawnはFutureを生成せず呼び出し自身が panic する (copyを非 Copyable に呼んだときと同種の fail-fast。language-spec.md §16.2)。forkのような遅延 panic には運ばない。判定は実行時の値に対して行う — その値が非 Copyable であるかだけを見る (裸のFutureのように基底型を持たない値もここで捕まる)。多重度型については、この規則の根拠になるのは基底型が非 Copyable な不透明ハンドルであることである。 - 例外 — 静的に移譲と判定された捕獲は非 Copyable 検査をしない: 静的検査が「この捕獲は所有権の移譲である」と判定した位置 (static-analysis.md §2.14) では、
spawnは上の非 Copyable 検査を行わず、複製の途中で非 Copyable な実体に達したらそこだけ共有して進む。捕獲した時点で親側ではその値が消費済みになるため、その実体を共有しても親からは触れない。複製そのものは省かない — 多重度型の値が内部に持つ可変な値 (多重度を持たないもの・メソッドの閉包が捕獲したもの) には親側から届く経路が残りうるので、そこは通常どおり作り直す (language-spec.md §17.8「複製と別フローへの捕獲」)。どの捕獲を移譲と判定するかは static-analysis.md §2.14 が定める (行き先のブロックの条件は「追跡の対象と消費する操作」、捕獲される値の条件は「複製と別フローへの捕獲の禁止」)。判定を実行時が受け取る形は型消去 (language-spec.md §17) と同じで、判定が付かない位置 (gradual 境界を経由した値など) では上の規則がそのまま働く。 - 伝播しない構造に入った多重度型の値の捕獲には panic を当てにしない: 多重度を伝播しない構造 (
Listと record) へ多重度型の値を入れてから捕獲する形は、移譲にならないので静的に禁じられる (static-analysis.md §2.14 のaffine-not-copyable)。上の panic は肩代わりにならない — 基底型が record の多重度型 (language-spec.md §17.8) を含む構造は実行時には普通の Copyable な値なので、複製されて通ってしまう。塞ぐのは静的検査の側である。 - 並列実行: 子フローは専用のスケジューラー領域で親と同時に走りうる (マルチコア対応)。可変状態を共有しないため language-spec.md §10.1 の不変条件は保たれる。子フロー内の
fork/spawn/sleep/awaitは子の領域内で従来どおり協調する。 - 結果: 子の評価結果は
Futureの resolve→await 辺で親へ渡る (language-spec.md §10.1 の happens-before)。resolve 時点で子は完了しており結果は子ヒープに閉じているため、参照のまま渡り複製されない (所有権移譲)。 - 引数検証 (fail-fast):
forkと同じく、デフォルトを持たない必須スロットが未束縛の値を渡すとspawn呼び出し自身が panic する (§9.1 の引数検証と同種)。加えてspawnはスロットを持たない本体だけのサンク{ body }のみを受け付け、スロットを持つ値 (デフォルト付きスロットを含む) を渡すとspawn呼び出し自身が panic する (forkはデフォルト付きスロットを許すが、spawnは隔離のため捕獲抽出が届く本体のみに限る)。 - 失敗・キャンセル: 専用機構は持たず
Future一般の規則に従う。回復可能エラーはblockがErr(e)を返す形 (§9.4。language-spec.md §16.1)、本体評価中の panic は await の!で消費側へ伝播し memoize される (language-spec.md §10.2)。root 完了時の未解決Futureは放棄され、子はキャンセル要求を中断点で観測する (language-spec.md §10.3 / §13.3)。
spawn は「隔離して並列に走らせたい」ときに使い、可変状態を意図的に共有したい協調フローには fork を使う。実行可能な例 (並列の wall-clock 観測を含むリファレンス) は実装時に追加する。
10. ファイルシステム (→ std/fs.md へ移設)
ファイルシステム組込 (read / write / mkdir ほか) は prelude に含めず、標準ライブラリモジュール std:fs として提供する。import で取得する。定義は std/fs.md を参照。
import { read, write } := "std:fs" content := read("foo.hika")!.unwrap!
11. 型語彙 (type vocabulary)
型注釈 (language-spec.md §17「型注釈」) で使う型は prelude 束縛の値(型値)である。本節が並べるのは型語彙が束縛する名前で、照合の規定は language-spec.md §17.4 が持つ — 照合する位置(束縛/分解/呼び出し時パラメーター/スロットのデフォルト確定。型不一致は panic)・関数型の照合の深さ・関数型注釈が付いた境界の呼出時強制はいずれもそちらである。加えて hikari check / LSP が実行前に健全な範囲で型不一致を検出する(static-analysis.md §2。誤検出は出さない)。
- 原始:
Int/Float/String/Bool/Unit/Bytes/Any/Never(型値) - 特殊:
Any(トップ型・任意の値に一致)/Never(ボトム型・どの値にも一致しない=v matches Neverは常にfalse。panic/exitなど発散位置の結果型で、全型の部分型として分岐合流で他方の具体型へ寄る。language-spec.md §16.2 / §17.2) - 型コンストラクター:
List(T)/OneOf(A, B, …)/Option(T)/Result(T)/Future(T)(型値を返す) - 順序:
Ordering(=Less∣Equal∣Greater) - 数値グループ:
Number(=OneOf(Int, Float)の透過的エイリアス。閉じた union。数値多相は境界付き型変数a: Numberで書く — language-spec.md §17.2、§3) - 構造的 interface:
Comparable(={compare |}、§13)/Copyable(={copy |}、§15)/Sequence(要素の並びを持つ値の契約、§6)。prelude 定義の structural 型(language-spec.md §17.5)で、ユーザーも{メソッド名… |}で定義できる - 効果: 型構築子
Effect(T, …)(結果型と効果ラベルの集合。関数型の結果型位置にのみ書ける)/EffectConduit(F)(その引数の効果が呼び出し元へ透過する)と、効果ラベルIo/Fs/Net/Proc/Time/Rand/Mut/Susp(language-spec.md §17.9、§17) - shape 型はリテラル再利用: record/object
{x: Int |}、function{Int, Int | Int}(body=結果型)、tuple(Int, String) - 組込値型:
Range(§7.6 の遅延シーケンス値の型)/Error(={kind: String, message: String |}の shape エイリアス、ResultのErr(Error)が指す。構築はerror(kind, message)、§12.3) - 型位置の
{}はname: 型=型付きフィールド・bare 小文字=メソッドフィールド・bare 大文字=埋め込み・| 結果型=function 型(language-spec.md §17.5)。[...]は型位置では不可 - 型定義は
type Name := <型式>(§17.4)。右辺を型コンテキストで評価し型値をNameに束縛する - タグ付きデータ定義は
enum Name := OneOf(Tag(T…), …)(§17.4)。トップレベルに束縛するのはName(OneOf(...)相当の型値)だけで、各タグの値コンストラクターはフラット束縛せずNameの slot(Name.Tag)として置かれる。bare 名で使うには名前 destructure{ Tag, … } := Name(language-spec.md §6.3)で明示的に引き込む。下記 Option / Result / Ordering はこの enum の組込特殊形であり、ユーザーは同形のものをenumで定義できる — prelude はこの同じ機構でOption/Result/Orderingを先取り destructure してSome/None/Ok/Err/Less/Equal/Greaterを bare 公開しているだけで、組込は特別扱いされない - 値の型テストは universal method
matches(value matches T/value.matches(T))で真偽値化する(language-spec.md §17.4)。Tが型値でなければ panic。束縛は伴わない(束縛はmatch§8.4)
型名・値コンストラクター(Some / None / Ok / Err など)は大文字始まりの慣用(予約語ではない)。casing 慣用の全体は language-spec.md §1.2.1 を参照。
12. Option / Result
不在は Option、失敗は Result で表す(language-spec.md §9.2 / §16)。いずれも prelude のタグ付きデータ値で、値コンストラクターで作り match の値コンストラクターパターン(§8.4)で消費する。どちらも enum(language-spec.md §17.4)で書けるタグ付きデータの組込特殊形であり(Option ≅ enum Option := OneOf(Some(T), None)、Result ≅ enum Result(a, b) := OneOf(Ok(a), Err(b)))、ユーザーは同形のものを自分で定義できる(組込はジェネリックな点と、Result が E を省略でき既定 Error を持つ点だけが特権)。
- Option:
Some(v)(在)またはNone(不在)。型値はOption(T)。first/last/index_of/to_int/to_string/String.to_floatなどが返す(Int.to_floatは全域で Option を返さない、§3.1)。 - Result:
Ok(v)(成功)またはErr(e)(失敗)。型値はResult(T, E)(E 既定は標準 Error{kind, message}。Result(T)≡Result(T, Error))。エラーを型で区別したいときはResult(T, E)で E に構造化型(enum等)を据える。input/std:fsのread/writeなどは既定のErrorを返す(Future(Result(T)))。success に値が無い操作はResult(Unit)(Ok(())/Err(e))。
値コンストラクターは、payload を取るタグ (Some / Ok / Err) が 1 引数の関数値、payload 無しのタグ (None) が arity 0 で呼び出し不要のそのまま値(language-spec.md §1.2.1)。
bare と修飾は同格: Option/Result は enum 型値なので、タグは本来 Option.Some / Option.None / Result.Ok / Result.Err のように型値の下に置かれる(language-spec.md §17.4)。prelude はこれを先取りの名前 destructure({ Some, None } := Option 相当。language-spec.md §6.3)で bare 公開しているため、通常のコードは修飾せず Some / None / Ok / Err をそのまま使う。修飾形も等価に使え、match の値コンストラクターパターン(§8.4)・matches・型注釈のいずれでも Some(x) ≡ Option.Some(x)。
12.0 受け手はタグの所属まで見る
Option / Result のメソッドの受け手は、所属を持たないタグ Some / None / Ok / Err に限る。タグ同一性は所属 enum を含むので(language-spec.md §17.4)、綴りが偶然一致するユーザー定義タグは Option でも Result でもない。所属を持つタグ・Ordering のタグ・その他の Variant を受け手にした呼び出しは、呼び出し位置で Error(panic 型、language-spec.md §16)になる。
enum My := OneOf(Some(Int), Nope) My.Some(1).unwrap! # panic: unwrap: called on My.Some, not an Option or a Result My.Nope.is_some() # panic: is_some: called on My.Nope, not an Option or a Result Less.or(5) # panic: or: called on Less, not an Option or a Result
通してはならないのは、通すと所属が失われるからである。 My.Some(1).map {…} を Option の map が受け取ると、結果は所属を持たない bare な Some になり、My の値だったという事実がそこで消える。これは「Auth.None == Color.None は偽」(language-spec.md §17.4)という等価規則と両立しない。
Option 専用のメソッド(filter / zip / flatten / ok_or)を Result の受け手に、Result 専用のメソッド(map_err / ok / err / unwrap_err)を Option の受け手に適用した場合も同じく呼び出し位置で Error になる。どちらにもあるメソッド(map / and_then / or / unwrap / unwrap_or_else / or_else)は受け手の種別に従って振る舞う。
12.1 Option メソッド
Option のメソッド (unwrap / or / map / and_then / ok_or / filter / zip / flatten ほか) の網羅表は自動生成のリファレンス reference/prelude.md を参照。map は Some の内容に block を適用し、and_then は block が返す Option を平坦化する(Some(Some(...)) に二重化しない flatMap)。
12.2 Result メソッド
Result のメソッド (unwrap / unwrap_err / or / map / and_then / map_err / or_else / ok / err ほか) の網羅表は自動生成のリファレンス reference/prelude.md を参照。map / and_then は Ok 側に作用し、map_err / or_else は Err 側に作用する。
Result(T, E) では payload を返す/受けるメソッド(unwrap_err・unwrap_or_else / or_else の block 引数・map_err / err)はエラー型 E の値を扱い、map / and_then / or は E を保つ(既定の Result(T) では E は Error)。map_err は E を別の型へ移す唯一のメソッド。
握り潰しは明示的 unwrap!(Err / None で panic)でしか書けない。逐次 bail は ? 演算子(language-spec.md §16.5)が定型の糖衣で、下地は match / return。Option フィールドを貫く連鎖は and_then で、不在/失敗にデフォルトを与える分岐は or / unwrap_or_else で書き、is_some / is_none ガード+unwrap! の手書きほどきを避ける。
12.3 エラー値の構築 (error)
error(kind, message)(型 { String, String | Error })は標準 Error 値(language-spec.md §16.3 の {kind: String, message: String |}、closed なデータオブジェクト)を構築する。オブジェクトリテラル {kind := "...", message := "..." |} と構造等価で、どちらで組んでもよい。型 Error(§11)↔ 構築関数 error の対応は Bytes/bytes(§5)・Range/range(§6.5)と同じ慣習。返り値は素の Error 値であり、Result に載せるときは Err(error(...)) と包む。引数は 2 つとも必須の String(非 String はエラー。静的型検査が検出する)。
e := error("not_found", "config.hika: no such file") e matches Error # true e == {kind := "not_found", message := "config.hika: no such file" |} # true (構造等価) validate := { n | if (n >= 0) { Ok(n) } { Err(error("negative", "n must be >= 0")) } }
kind の集合の設計(組込 kind は正規一覧・ユーザー定義は開いた String・受け手は default 分岐の慣習)は language-spec.md §16.4。
13. 順序比較 (compare / Ordering / Comparable)
順序の意味論 (全順序・原始型の順序・< <= > >= の導出・異型 panic・overload) は language-spec.md §9.4 を参照。本節は prelude が提供する値とメソッドを定義する。
13.1 compare
a.compare(b) は全順序の正準 universal method で、Ordering を返す(Less=a が前 / Equal=同順位 / Greater=a が後)。原始型 (Int/Float/String/Bool/Unit) は組込で持ち、object は compare スロットで持てる。Float は NaN にも定位置を与える全順序(language-spec.md §9.4)。非 Comparable なレシーバー・異型比較は panic(language-spec.md §9.4 / §16.2)。< <= > >= はこの compare から導出される。
13.2 Ordering
Ordering は payload を持たないタグ付きデータ(Less / Equal / Greater の 3 値。§12 の Option / Result と同じ一族で、enum Ordering := OneOf(Less, Equal, Greater) 相当の組込)。Less / Equal / Greater は arity 0 の値コンストラクターで、呼び出し不要でそのまま値(language-spec.md §1.2.1)。型値 Ordering で matches / 型注釈に使える(language-spec.md §17.4)。§12 と同じく、これらは Ordering の下に置かれるタグを prelude が先取り destructure して bare 公開したもので、修飾形 Ordering.Less / Ordering.Equal / Ordering.Greater も等価に使える。
Ordering のメソッド (then / reverse) の形・戻り値は自動生成のリファレンス reference/prelude.md を参照。then は o が Equal のときだけ無引数 block(Ordering を返す)を遅延評価し、多段比較の合成に使う。reverse は Less ⇄ Greater を入れ替え Equal は不変。
match でも分岐できる:
match o { Less => …; Equal => …; Greater => … }
多段比較は then で合成する(Equal のとき次のキーへ短絡):
a.k1.compare(b.k1)
.then { a.k2.compare(b.k2) }
.then { a.k3.compare(b.k3) }
13.3 Comparable
Comparable は「順序を持つ値」を表す型値で、構造的 interface {compare |}(language-spec.md §17.5)として prelude が束縛する(組込の特別型ではない)。v matches Comparable は v が原始型 (Int/Float/String/Bool/Unit)、または callable な compare スロットを持つ object のとき true(language-spec.md §17.4)。ユーザーも type MyOrd := {compare |} で同値の型を定義できる。sort / sort_by(§6)は要素・キーが Comparable であることを要求する。
13.4 List のソート
sort / sort_by / sort_with(§6 List メソッド)は 安定(同順位=Equal / 同キーの要素は元の順序を保つ)で、新しい List を返す(map / filter と同じく非破壊。closed list でも動く)。空 List はそのまま空、単一要素はそのまま。
sort(): 要素をcompareで昇順に並べる。要素は Comparable かつ相互に比較可能(異型混在は §9.4 のとおり panic)。sort_by {x | key}: 各要素からkeyを 1 回だけ 算出し(副作用も 1 回)、key のcompareで昇順。key は Comparable。sort_with {a, b | ...}:blockが返すOrdering(Less=aが前)で並べる。blockがOrdering以外を返すと panic。- いずれの
block内でもbreak/continueは不可(map/filter/foldと同じくエラー)。
降順は、単一キーなら xs.sort_by {…}.reverse()、per-key で向きが違う多段なら sort_with で compare(...).reverse() を then 合成する:
# 「ピン留め優先 → 更新日時降順」
notes.sort_with { a, b |
a.pinned.compare(b.pinned).reverse()
.then { a.updated_at.compare(b.updated_at).reverse() }
}
シーケンス (String / Bytes / Range) を並べ替えるには to_list (§6.6) で List に開いてから sort* する。
13.5 順序演算 min / max / clamp (大域関数)
2 値・3 値の順序演算 min / max / clamp を 大域関数として束縛する。いずれも compare の全順序(§13.1)だけを使う演算なので、数値専用でなく Comparable な同型の値に効く(String などにも使える)。シグネチャは境界付き型変数(language-spec.md §17.2)で書き、同名 a を各位置で共有するため引数は同じ型でなければならない(混在は §13.1 のとおり異型比較で panic)。
| 名前 | 形 | 型 | 意味 |
|---|---|---|---|
min |
min(a, b) |
{ (a: Comparable), a | a } |
a と b の小さい方。同順位は a を返す |
max |
max(a, b) |
{ (a: Comparable), a | a } |
a と b の大きい方。同順位は a を返す |
clamp |
clamp(x, lo, hi) |
{ (a: Comparable), a, a | a } |
x を [lo, hi] に収める(x < lo なら lo、x > hi なら hi、他は x)。lo > hi はエラー |
min(3, 7) #> 3 max(3.0, 7.0) #> 7.0 min("b", "a") #> "a" (String も Comparable) clamp(10, 0, 5) #> 5 clamp(-2, 0, 5) #> 0 min(1, 2.0) # error: 異型比較(Int と Float)で panic
List 全体の最小 / 最大は xs.min() / xs.max()(§6.6・Option を返す)を使う。2 値専用の min / max は畳み込みでなく直接 2 引数を比較する。数値専用の abs / sign は std:math(std/math.md §3)にある。
14. inspect — デバッグ表現 (universal method)
inspect は matches / compare と同系の universal method。全値に生え、受け手の型に依らず解決される (language-spec.md §2.5)。v.inspect() / v.inspect! は v の人間可読なデバッグ表現を String で返す (REPL のエコー・print の非 String 引数・${obj} 補間が内部で使う表現と同じ)。形の詳細は自動生成のリファレンス reference/prelude.md を参照。
- 構造的・型タグなし:
2→2/2.0→2.0/"hi"→"hi"(引用符つき・エスケープ済み) /[1, "hi"]/(1, 2)/{ mutable x := 1 |}/Some(1)/None。型は構造で判別する。inspectは値の表示専用であり、値の実行時型そのものを取り出す組込は Hikari に無い。 - 往復可能: 非表示文字は
\u{…}等でエスケープし (language-spec.md §7.2)、出力はできるだけ Hikari ソースへ貼り戻せる。 - 上書き: 受け手の
inspectスロットで上書きできる (language-spec.md §8.6) — 上書きの有無にかかわらずinspectという名前は全ての値で解決可能なまま保たれる (受け手定義に置き換わるか組込既定のままかの違いだけ)。上書きが効くのは明示v.inspect!で、REPL エコー等の自動表示は既定の組込表現を使う。
2.inspect! #> "2" "hi".inspect! #> "\"hi\"" (中身は "hi" の 4 文字) [1, "hi"].inspect! #> "[1, \"hi\"]" Some(1).inspect! #> "Some(1)"
15. copy — 複製 (universal method)
copy は matches / compare / inspect と同系の universal method。全値に生え、受け手の型に依らず解決される。意味論 (self 再束縛・可変性で決まる深さ・グラフコピー・Copyable) は language-spec.md §9.5 が定義する。v.copy() / v.copy! は独立したコピー、v.copy(overlay) は overlay のスロットだけを差し替えた複製 (copy-and-update) を返す。形の詳細は自動生成のリファレンス reference/prelude.md を参照。本節はその意味論を定める。
- 複製であって構築ではない: default 式 (§12) は再評価されず、その副作用も走らない。引数つき初期化や副作用つき構築は factory (本体付き 0 引数ブロック) の担当で、
copyはそれを置き換えない。 - 引数つき複製 (copy-and-update):
v.copy(overlay)は複製と同時に overlay のスロットの値だけを差し替える。q := p.copy!; q.x = 10の 2 手がq := p.copy({ x := 10 |})の 1 式に縮む。overlay は既存の object リテラルで渡し (新構文なし)、可変性は受け手側を保つ。詳細は language-spec.md §9.5。 - 上書き: 受け手の
copyスロットで上書きできる (language-spec.md §8.6) — 上書きの有無にかかわらずcopyという名前は全ての値で解決可能なまま保たれる。深いコピーの強制や不透明リソースの複製はここで定義する。ただし多重度型の値とそれを含む構造ではcopyそのものが静的な型エラーであり、上書きでこれを迂回することも認めない(language-spec.md §17.8)。 - Copyable:
copyできる値を表す型値で、構造的 interface{copy |}(language-spec.md §9.5 / §17.5)として prelude が束縛する(組込の特別型ではない)。v matches Copyableは通常値でtrue、意味ある複製を定義できない不透明な可変リソース (Future/ 外部ハンドル等) はfalse。copyを Copyable でない値に呼ぶと panic する。
p := { mutable x := 1, mutable y := 2 |}! # 更新したいスロットは mutable。`!` で完成値にする
q := p.copy!
q.x = 10 # q だけ更新
p.x #> 1 # 元は変わらない (状態は独立)
counter := { inner_bound b, mutable count := 0, inc := { b.count += 1; b.count } |}!
counter.inc!
d := counter.copy! # count=1 を引き継ぐ独立カウンター
d.inc! #> 2 # counter は 1 のまま
counter.count #> 1
受け手は完成値でなければならない。 ! を落とした綴り ({ … |} そのもの) は構築子で、その可変スロットには現在値が無いので (language-spec.md §4.1)、copy も = もそこでは働かない。完成値の可変状態へ届く自己参照は inner_bound である (language-spec.md §4.2.1) — inner はリテラル自身を指すので、そちらでは count を更新できない。
16. apply_when — 条件付き適用 (universal method)
apply_when は matches / compare / inspect / copy と同系の universal method。全値に生え、受け手の型に依らず解決される。「条件が成り立つときだけ値を加工し、成り立たなければそのまま通す」という分岐を、主語を一度だけ書いて表す。
x.apply_when cond f # 並置形 (推奨) x.apply_when(cond, f) # タプル形
cond が真なら f x の値を、偽なら x をそのまま返す。素の if (§8.1) による次の形と同値である。
if cond { f x } { x } # ≡ x.apply_when cond f
f は関数値でもブロックリテラルでもよい。
base.apply_when selected ansi.reverse # 既存の関数をそのまま渡す line.apply_when no_wrap { s | s.slice(0, w) } # その場のブロック
cond が偽のとき f は適用されない — f がブロックリテラルならその本体は評価されない。cond と f は他の関数と同じく呼び出し前に評価される値であり (§8 冒頭)、遅延されるのはブロックの本体である。cond が Bool でなければ panic。f が callable でなければ、cond が真のときに適用の段で panic する (偽なら f に触れないため成功する)。
16.1 型と f の制約
{ x: a, cond: Bool, f: {a | a} | a }
f は a を受けて a を返す 同型関数 (endo) に限る。if の 2 分岐が合流する以上これは必然である。f が別型を返す分岐は apply_when の担当ではなく、素の if (§8.1) か match (§8.4) で書く。
cond は Bool 値であって述語ブロックではない。x は評価済みの値として既に手元にあるため、x を参照する条件は呼び出し側でそのまま書ける (s.apply_when (s.length! > n) f)。この形では条件側に s がもう一度現れるが、述語をブロックで受ける設計にすると条件が主語と無関係な多数派 (selected / has_limit のような外から来る Bool) で { _ | cond } という空回りのブロックを強いることになるため、Bool を採る。
16.2 極性は when 側のみ
apply_when の対 (if cond { x } { f x }) にあたる apply_unless は持たない。条件を反転して x.apply_when cond.not! f と書く。Boolean.not (§2) があれば表現力は同じで、prelude に unless を持たない (when のみ、§8.2) のとも揃う。
16.3 制御透過
apply_when は conduit (language-spec.md §18.3) であり、f の中の return / break / continue は apply_when を貫いて 呼び出し側の 関数・ループに届く。when (§8.2) や take_while / drop_while (§6) と同じ透過プロファイルである。
意味は次の Hikari 定義と同じである (規範はこの等式であって、実装がこの形をとることは要求しない)。f は普通のユーザー関数なのでその呼び出し境界で return が unwrap される。Hikari で書くなら透過には capture で転帰を観測し制御値を forward で再送出する必要がある (language-spec.md §18.4、stop_at と同じ骨格)。
apply_when := conduit { x, cond, f | if cond { match capture(f, x) { Normal(v) => v c: Control => forward c } } { x } }
実装は他の universal method (compare / copy / inspect / matches / reference_equals) と同じく builtin で、ブロック起動を処理系の内部経路に委ねることで上と同じ透過を得る。when (§8.2) 等と同様、後から self-host へ移しても意味は変わらない。
16.4 上書き
受け手の apply_when スロットで上書きできる (language-spec.md §8.6) — 上書きの有無にかかわらず apply_when という名前は全ての値で解決可能なまま保たれる。
17. 効果 (effects)
prelude が束縛する組込のうち、効果 (language-spec.md §17.9) を持つものと、ブロック引数の効果を透過させるものを定める。ここに挙がらない組込・型メソッドはすべて効果を持たない。
17.1 効果を持つ組込
| 組込 | 効果 | 備考 |
|---|---|---|
print / println / eprint / eprintln |
Io |
§1 |
input |
Io |
§1。! で待つ地点にはさらに Susp が付く |
exit |
Proc |
プロセスの終了コードを決める。結果型は Never |
now |
Time |
§9.3。時計を読む |
sleep |
Susp |
§9.2。中断点 (language-spec.md §10.1) |
wait_any |
Susp |
§9.5。同じく中断点 |
panic は効果を持たない。 発散は「外界に対して何をするか」ではなく、その計算が値を返さないことである。結果型 Never (§11) がそれを表しており、効果ラベルは重ねない。同じ理由で Never を結果型に持つ組込一般に効果は付かない。
未完了 Future への ! は Susp を足す。 これは組込ではなく呼び出しの形に対する規則で、language-spec.md §17.9 が定める。解決済みかどうかは静的に分からないため、! は保守側に倒して常に Susp を持つ。
17.2 効果ラベルと型構築子
Io / Fs / Net / Proc / Time / Rand / Mut / Susp の 8 つの効果ラベルと、型構築子 Effect(T, …) / EffectConduit(F) は prelude 束縛の値である (§11 の型語彙に属する)。Comparable / Copyable が組込の特別な型ではなく prelude 定義の構造型であるのと同じ位置づけで、新しい予約語も記号も持たない。
Fs / Net / Rand は prelude の組込には現れない — これらを起こすのは標準ライブラリ (std/index.md「モジュールの効果」) だけである。ラベル自体は prelude が束縛するので、import を伴わないファイルでも型注釈に書ける。
17.3 別フローを起こす組込
fork (§9.1) と spawn (§9.9) は、渡されたブロックの効果をその呼び出し地点で計上する。したがって f! が同じ効果を二重に計上することはなく、! が足すのは Susp だけである。
両者は Mut の扱いだけが違う。
fork— 可変状態を共有する協調フローなので、ブロックのMutはそのまま透過する。spawn— 捕獲した可変状態を複製して呼び出し元から切り離す (language-spec.md §10.1) ため、子フローの書き込みは親から観測できずMutを起こさない。他の効果は透過する。
Future の合成オペレーター .map / .and_then (§9.6) はブロックを別フローで起動するので、下記の透過側に属する。.timeout / all / race は自身では効果を持たず、待ち合わせに伴う Susp だけを持つ。.cancel (§9.8) と .is_ready (§9.7) は効果を持たない。
17.4 効果を透過させる組込 (EffectConduit)
次の組込はブロックパラメーターを EffectConduit で宣言する (language-spec.md §17.9)。渡したブロックの効果が呼び出し元へ透過するので、純粋なブロックを渡せば呼び出しも純粋である。
線は「ブロックを起動するか」で引く。 ブロックを受け取って呼ぶ組込はすべて透過する — 呼ばれる以上その効果は呼び出し元で起きるからである。起動が条件付き (unwrap_or_else は None / Err のときだけ) でも変わらない。効果は「起こしうるものの上限」を述べるので、起動されうるなら上限に入る。
| 群 | 組込 |
|---|---|
| 制御構造 (§8) | if / when / while |
Sequence のメソッド (§6) |
each / map / filter / fold / find / any / all / count |
List 固有 (§6) |
flat_map / partition / take_while / drop_while / max_by / min_by |
| ソート (§13.4) | sort_by / sort_with |
Option / Result (§12) |
map / and_then / or_else / unwrap_or_else / map_err |
Ordering (§13.2) |
then |
Future (§9.6) |
map / and_then |
| universal method (§16) | apply_when |
効果透過は制御透過 (conduit、language-spec.md §18.3) とは別の軸である。 上表の組込のうち制御も透過するものとしないものがあり、sort_by はその代表 — key ブロックの副作用は 1 回だけ走る契約 (§13.4) なので効果は透過させるが、key ブロック内の break が呼び出し元のループへ抜けるとソートが途中で放り出されるため制御は透過させない。2 つの軸を 1 つのマークで束ねてはならない。
17.5 ハンドラー (handle)
handle は prelude 束縛の組込である (language-spec.md §17.9)。if / when / while と同じ特殊オブジェクト (§8) で、新しい構文形も予約語も持たない — 並置=関数適用なので、ブロックとハンドラーの 2 つを渡す。
handle { greet("world"); greet("hikari") } { println := { s, k | log = log + [s]; k(()) } |}
- 第 1 引数はハンドルされるブロック (引数を取らない)。
- 第 2 引数は操作スロットを並べたオブジェクト。スロット名が操作の素の名前 (修飾を落とした末尾の識別子。
fs.readならread) で、値がその操作を受け取るブロックである。操作であるのは prelude と std が操作として宣言したものだけで、効果を持つかどうかからは決まらない (language-spec.md §17.9)。prelude ではprint/println/eprint/eprintln/input/exit/now/sleep/wait_anyの 9 つがそれに当たる。どの std メンバーが操作かは各モジュールのリファレンスが項目ごとに明記する。 - 操作スロットの第 2 引数が継続で、その名前は書き手が選ぶ (
k/cont/resumeのいずれでもよい)。多重度はAtMostOnceなので、呼ばなければ脱出・2 回呼べば消費追跡が二重消費として報告する。
handle が扱うラベルは書かれた操作スロット名から決まり、handle 式の効果は「ブロックの効果から扱うラベルを除いたもの」と「操作スロット本体自身の効果」の和になる。網羅の検査はブロックが実際に起こす操作を対象にする (static-analysis.md §2.15)。
ブロック引数は EffectConduit ではない。 透過させるのではなく、扱うラベルを取り除いて残りだけを通すのがハンドラーだからである。§17.4 の表に handle が載らないのはこのためである。
handle は中断点を増やさない (language-spec.md §17.9)。継続の呼び出しは通常の関数適用であり、フロー切り替え規則 (§9) を拡張しない。操作スロット本体が中断すればそれは Susp として handle 式の効果に現れる。
17.6 効果を持たない組込
上に挙がらないものはすべて効果を持たない。とくに次は間違えやすいので明示する。
match(§8.4) とbreak/continue/return(§8.5 / §8.6) — 制御値の伝播であって外界に触れない。inspect(§14) — デバッグ表現の String を返すだけで、書き出さない。書き出すのはprintであり、そこでIoが付く。copy(§15) — 複製は新しい値を作るだけである。default 式を再評価しないので、default に埋めた副作用が走ることもない (§15)。compare/==と順序演算子 (§13) — 等価と順序は式のどこにでも現れるため、中断しないことが規則で要求されている (static-analysis.md §2.9)。効果を持たないことはその規則を型の側から言い直したものであり、compareの結果型がSuspを許さないことから中断しないことが従う。error(§12.3) — エラー値を組み立てるだけである。