Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第9章 命令的に書く — var・while・for・IO

気づいていたでしょうか。ここまでの8つの章で、あなたは一度も変数を書き換えていません。温度換算表も、無限の素数列も、じゃんけんの判定も、すべて「値に名前を付けて、式で組み合わせる」だけで書けました。

それでも、カウンタを回したい場面、途中経過を書き換えたい場面はあります。Lune の答えは現実的です — 命令的な書き方も、小さく使うぶんには良い道具。この章では可変束縛 var、ループ while と for、出力の置き場所 IO: を学びます。合言葉は「命令はブロックに閉じ込め、外へは値を返す」です。

9.1 ブロック慣用句 — 命令の島

まず Lune 流の「命令的コードの置き方」を見てください。counter.lune:

module counter

# var と while はブロックの中で使い、最後の式で結果を返す。
let answer =
    var i = 0
    var total = 0
    while i < 5:
        total = total + i
        i = i + 1
    total
$ lune --eval answer counter.lune
10

ブロックの中では変数が書き換わり、ループが回っています。しかし外から見れば、answer はただの Int です。第2章のブロック let と同じ形で、最後の式 total がブロック全体の値になります。命令的な過程は島の中に閉じ、島の外は今までどおり式の世界 — この形を保つ限り、命令的なコードはプログラムの残りを汚しません。

ついでに第4章の住人として一言: この let answer = ... も遅延です。誰かが answer を使うまで、ループは一周も回りません。

9.2 var — 正格な可変束縛

書き換えられる束縛は var で作ります。

var count = 0
count = count + 1

let との違いは2つあります。可変であること、そして正格(束縛した瞬間に右辺を評価する)であることです。tick()(第4章)で確かめられます。

lune> var t = tick()
ok
lune> tickCount()
1 : Int

束縛しただけでカウンタが進みました — let なら 0 のままだったはずです。書き換わる変数の値が「いつ評価されるか分からない」のでは推論できないので、var は遅延しない、と覚えてください。

letvar
再代入不可(不変)可
評価遅延(使うまで計算しない)正格(束縛時に計算する)

代入できるのは名前だけです。i = i + 1 と書くほかに、複合代入 += -= *= /= //= %= も使えます(i += 1 は i = i + 1 とまったく同じ意味です)。ひとつ落とし穴があって、/ は常に真の除算なので Int の変数への x /= 2 は型エラーになります(Double になってしまうため)。整数を整数のまま割りたいときは、床除算の x //= 2 を使ってください(// は 2.2 節)。

「不可」は、心構えではなく検査です。let に代入すると TYP0013 が出ます。

lune> let a = 1
ok
lune> a = 2
error[TYP0013]: `a` には代入できません: `let` の束縛は不変です
  --> <repl:2>:1:1
  |
1 | a = 2
  | ^ この束縛には代入できない
   = hint: 書き換えるなら `var a = ...` で宣言してください
   = help: 詳しくは `lune explain TYP0013 --lang ja` を実行してください
lune> a
1 : Int

ファイルでも同じです。letassign.lune を lune --check にかけると、代入している場所を指して同じ TYP0013 が出ます。

不変なのは let だけではありません。関数の仮引数も、for の変数も、パターンで束縛した名前も、代入できません。書き換えてよい束縛は var だけです。検査は一番近い束縛を見るので、外側に var x があっても、ブロックの中の let x は不変のままです。

9.3 while — 最小のループ

while 条件: は、条件が Bool である間ブロックを繰り返します。9.1節の counter.lune がすべてです — 条件は毎周評価され、while 式全体の値は常に Unit。つまり while は値を作る道具ではなく、var を書き換えるための道具です。

break と continue はありません。途中で抜けたければ、その条件をループ条件に織り込みます(while i < 5 && not(done): のように)。それが煩わしくなってきたら、それはたいてい再帰か fold で書くべき合図です(第3・8章)。

9.4 for — リストを歩く

リストの全要素に何かしたいだけなら、for が簡潔です。fortotal.lune:

module fortotal

let pairs = [(1, 10), (2, 20)]

# for のパターンには、let と同じく反駁不能なものが書ける。
let total =
    var result = 0
    for (left, right) in pairs:
        result = result + left + right
    result
$ lune --eval total fortotal.lune
33

for パターン in リスト: の形で、パターンには let と同じ規則(第5章)が適用されます — タプルのような反駁不能なパターンは分解でき、失敗しうるパターンは弾かれます。

lune> for Some(x) in [Some(1), Some(2)]:
...     println(x)
...
error[TYP0008]: for の束縛に反駁可能パターンは使えません: Some(x)
  --> <repl:1>:1:24
  |
1 | for Some(x) in [Some(1), Some(2)]:
  |                        ^^^^ このパターンはマッチに失敗し得る
   = hint: このパターンは None をカバーしていません
   = hint: `match` を使って Option[Int] の全ケースを場合分けしてください
   = help: 詳しくは `lune explain TYP0008 --lang ja` を実行してください

走査できるのは List[T] だけです。badfor.lune のようにリスト以外を渡すと、専用の診断が出ます。

error[TYP0006]: for の対象は List でなければなりませんが、Int でした
  --> badfor.lune:5:14
  |
5 |     for x in 42:
  |              ^^ 走査対象は List[T] でなければならない
   = help: 詳しくは `lune explain TYP0006 --lang ja` を実行してください

for もまた Unit です。そして背骨を1歩ずつ force しながら進むので、無限リストに for すると帰ってきません — 第8章の「消費し切る道具」の仲間だと思ってください。

9.5 IO — 出力と遅延の付き合い方

println はこれまで何度も使ってきましたが、遅延の世界には出力ならではの罠があります。見てください。

lune> let p = println("hi")
ok
lune> p
hi
() : Unit

let の行では何も出力されません。println("hi") はサンクに包まれ、p を使った瞬間に初めて実行されたのです。つまり遅延の世界では、「出力がいつ起きるか」は「値がいつ要るか」で決まる — 画面に順番どおり文字を出したいプログラムにとって、これは困った性質です。

だから出力は IO: ブロックに集めます。io.lune:

module io

# 出力は IO ブロックに集め、上から順に実行させる。
def report(): Unit =
    IO:
        println("one")
        println("two")

let run = report()
$ lune --eval run io.lune
one
two
()

IO: ブロックの中の文は、ブロックが実行されるとき上から順に走ります。「順序が意味を持つ副作用はここにあります」という宣言でもあり、読み手への目印でもあります(v0.1 の IO: は型システム上の厳密なエフェクト管理ではなく、この「置き場所」の規約です — 将来仕様では強化が予定されています)。

作法としてはこう覚えてください。計算は純粋な関数で、出力は IO の中で。判定ロジックを String を返す関数に切り出し、IO: の for で流し込む — この分離の練習が演習 9-2(FizzBuzz)です。

9.6 raise / throw — 最後の手段

実行時エラーを自分で起こす式もあります。

lune> let bad = raise "failed"
ok
lune> bad
error[RUN0006]: failed
   = help: 詳しくは `lune explain RUN0006 --lang ja` を実行してください

raise 式(同義の throw もあります)は評価されると実行時エラーを送出します。例によって let は遅延なので、束縛しただけでは何も起きず、使った瞬間に爆発します — crash() の自作版だと思えばだいたい正しいです。

そして重要な注意: try/catch はありません。つまり raise したエラーをプログラムの中で受け止める方法はないのです。回復可能な失敗は、第5章の単相の結果型や Result、第7章の T? — 失敗を値として返す道具で表してください。raise の出番は「ここに来たらプログラムを止めるのが正しい」という、本当に例外的な場面だけです。

まとめ

概念一言で
ブロック慣用句命令的コードは let 名前 = のブロックに閉じ、最後の式で値を返す
var可変・正格。代入は名前への = のみ
while 条件:条件は毎周評価。式全体は Unit。break/continue はない
for パターン in リスト:List[T] 専用(TYP0006)。パターンは反駁不能(TYP0008)
出力の順序遅延に流されないよう IO: ブロックに集めて上から実行
分離の作法計算は純粋な関数、出力は IO の中
raise / throw受け止める手段はない。回復可能な失敗は値で返す

演習問題

演習 9-1(★) while で 1 から 10 までの積(10!)を計算してください。

解答
module answers

# 演習 9-1: while で 1 から 10 までの積(10!)。
let product =
    var i = 1
    var acc = 1
    while i <= 10:
        acc = acc * i
        i = i + 1
    acc
$ lune --eval product ex9-1.lune
3628800

演習 9-2(★★) FizzBuzz を書いてください。1〜15 について、3 の倍数なら Fizz、5 の倍数なら Buzz、両方なら FizzBuzz、それ以外は数を1行ずつ出力します。判定と出力を分けること。

解答
module answers

# 演習 9-2: FizzBuzz。判定は純粋な関数に、出力は IO の for に分ける。
def fizzbuzz(n: Int): String =
    if n % 15 == 0:
        "FizzBuzz"
    elif n % 3 == 0:
        "Fizz"
    elif n % 5 == 0:
        "Buzz"
    else:
        show(n)

def main(): Unit =
    IO:
        for i in range(1, 16):
            println(fizzbuzz(i))

let run = main()

fizzbuzz は純粋な関数なので、REPL で単体テストできます(fizzbuzz(15) → "FizzBuzz")。出力の都合と判定のロジックが混ざらない — 9.5節の分離の作法です。

演習 9-3(★★) let t = tick() と var v = tick() を1つずつ定義して、tickCount() と併せて let と var の評価タイミングの違いを説明してください。

解答

定義直後の tickCount() は 1 — 進んだのは var の分だけです。let t のサンクはまだ眠っていて、t を初めて使った瞬間に 2 になります。第4章の strict let と同じタイミング、と整理できます(var = 可変 + 正格、strict let = 不変 + 正格)。

演習 9-4(★★★) 9.1節の counter.lune(0〜4 の和)を、var もループも使わずに書き直してください。そのうえで、どちらが読みやすいか自分の言葉で論じてください。

解答
lune> fold(range(0, 5), 0, fn a x -> a + x)
10 : Int

1行です。「積み上げる計算」は fold の独壇場で、ループ版の4行(カウンタ2本と更新2式)が背負っていた「i と total を取り違えない責任」が消えます。一方、途中で条件により打ち切るような制御は while が素直なこともあります。目安: データを一巡して集計するなら fold、状態機械を回すなら while。

演習 9-5(★・逆転問題) TYP0006 と TYP0008 を、それぞれ for を使った最小のコードで出してください。

解答

for x in 42: println(x)(TYP0006 — 42 は List ではない)と for Some(x) in [Some(1)]: println(x)(TYP0008 — Some(x) は失敗しうるパターン)。どちらも本文 9.4 節に実物があります。TYP0008 の hint が第5章の let のときと同じ言い回しであることも見ておいてください — for の束縛は let の親戚です。


より正確には — var と代入は documents/LANGUAGE_SPEC.md §7.3、while は documents/WHILE_LOOP_SPEC.md、for は documents/FOR_LOOP_SPEC.md(spine の force 規則も)、raise/throw と IO: は documents/LANGUAGE_SPEC.md §9.6–9.7。この章のコード例は books/examples/ch09/ にあり、すべて実際の CLI で検証されています。