2週間前の日曜日から、Rust の勉強を始めました。
仕事では PHP と Python と TypeScript が中心で、Rust はずっと「いつかやる」リストに入っていたものです。今回、重い腰を上げたのには理由があります。
最近、新しいライブラリやフレームワークを触るとき、AI に聞けばだいたい動くコードがすぐ出てきます。それで仕事は回る。でも、1か月後に同じことをもう一度やろうとすると、何ひとつ覚えていない。そういうことが、ここ1年でずいぶん増えた気がしていました。
AI のおかげで「できる」ことは増えたのに、「分かる」ことは増えていないのではないか。
それを確かめたくて、ひとつルールを決めて Rust を学んでみることにしました。AI には、答えを言わせない。 今日は、その2週間の日記を、ほぼそのまま載せます。日記なので、ここから先は少し口調がくだけます。
10月11日(日) 1日目:ルールを決める
ルールは3つにした。
- AI に答えを聞く前に、自分の仮説を必ず1行書く。 合っていなくてもいい
- AI には「答え」ではなく「ヒント」を頼む。 「直し方を教えて」ではなく「どこを見ればいい?」と聞く
- 1週間後に、何も見ずに覚えていることを書き出す
AI の側にも、こういう使い方のための機能がそろってきている。ChatGPT には「学習モード」、Gemini には「ガイド付き学習」、Claude にも学習モードがある。Claude Code には「Learning」という出力スタイルがあって、これをオンにすると、Claude が全部を書かずに、大事な部分に TODO(human) という印を残して、そこを人間に書かせてくれる。
今回は Claude Code の Learning スタイルを使うことにした。
教材は公式の「The Rust Programming Language」(通称 The Book)。昔からの定番で、無料で読める。
夜、第1章と第2章の数当てゲームまで。ここはまだ平和。
10月12日(月) 2日目:最初の壁
さっそく所有権でつまずいた。
let s = String::from("hello");
let t = s;
println!("{}", s);
コンパイルすると error[E0382]: borrow of moved value: s と怒られる。
いつもなら、エラーをそのまま AI に貼って「直して」と言うところ。でも、ルール1があるので、まず自分の仮説を書いた。
仮説:
let t = s;で中身がコピーされずにtに移ったので、sはもう空っぽ扱いになっている?
それから Claude に「この仮説は方向として合ってる? 答えは言わないで、合ってるかどうかと、次に読むべき場所だけ教えて」と聞いた。
返ってきたのは「方向は合っています。The Book の 4.1 節の『Move』と『Clone』の部分を読んでみてください。なぜ整数ではこのエラーが起きないのかも考えてみると、理解が深まります」という返事だった。
整数では起きない。言われてみればそうだ。そこから Copy トレイトの話にたどり着くまで、40分かかった。AI に直してもらえば10秒で終わる話だ。
でも、40分かけてたどり着いた「整数はスタックにあって、コピーが安いから Copy で、String はヒープを指しているから所有権ごと移る」という理解は、たぶん忘れない気がする。
10月14日(水) 4日目:イライラする
正直、今日はかなりイライラした。
借用のルール(可変参照は同時に1つだけ)で詰まって、仮説を3回書いて3回とも外れた。Claude のヒントも、今日はなんだか遠回りに感じる。「もういいから答えを言ってくれ」と、何度か打ちかけた。
途中で、心理学者のロバート・ビョークの「望ましい困難(desirable difficulties)」という言葉を思い出した。学習中に適度な困難があるほうが、そのときは遅く感じても、あとで記憶に残りやすい、という考え方だ。逆に、すらすら進む学習は、そのときは「分かった」気がしても、定着しにくい。
イライラしているということは、たぶん今、ちゃんと学んでいるということだ。そう思うことにした。
結局、4回目の仮説で当たった。ループの中で、ベクタへの可変参照を持ったまま、同じベクタを読もうとしていた。分かってしまえば、当たり前のことだった。
10月17日(土) 7日目:1週間テスト
ルール3の日。何も見ずに、この1週間で覚えたことを紙に書き出した。
書けたこと:
- 所有権の3つのルール
- ムーブとコピーの違いと、
Copyトレイト - 不変参照はいくつでも、可変参照は1つだけ
-
Optionとmatchで、null の代わりに「ないかもしれない」を表す
書けなかったこと:
- ライフタイム注釈の書き方(読んだはずなのに、何も出てこなかった)
-
Stringと&strの使い分けの基準(なんとなくしか説明できない)
比べてみて、はっきりした傾向があった。自分で仮説を立てて外した部分は、全部書けている。 書けなかったのは、AI のヒントを読んで「なるほど」と思っただけで、自分の手で一度も間違えていない部分だった。
ここで思い出したのが「テスト効果」の研究だ。Roediger と Karpicke が2006年に発表した実験で、同じ文章を何度も読み返すより、読んだあとに思い出す練習(テスト)をしたほうが、1週間後の記憶がよく残っていた。思い出すという行為そのものが、記憶を強くする。
今日の紙1枚が、まさにそのテストだった。
10月20日(火) 10日目:「ハマりノート」を作る
1週間テストで書けなかったものを、どうやって覚え直すか考えて、小さな道具を作った。
ハマったこと、間違えたことを「質問と答え」のカードにしておいて、忘れかけたころに出題してくれる CLI だ。出題の間隔は、SuperMemo というソフトのために1980年代に考えられた SM-2 というアルゴリズムで決める。思い出せたカードは次の出題までの間隔がどんどん伸びて、忘れていたカードは翌日にもう一度出てくる。
この部分は Rust ではなく、慣れた Python で書いた。勉強のための道具づくりで勉強が止まったら本末転倒なので。
#!/usr/bin/env python3
"""自分の「ハマり」を、間隔反復(SM-2)で復習するための小さな CLI。
python3 hamari.py add "所有権が移動した変数を使うと?" "E0382。clone するか参照を渡す"
python3 hamari.py review # 今日が期限のカードだけ出題する
"""
import json
import sys
from datetime import date, timedelta
from pathlib import Path
DECK = Path.home() / ".hamari.json"
def load():
return json.loads(DECK.read_text(encoding="utf-8")) if DECK.exists() else []
def save(cards):
DECK.write_text(json.dumps(cards, ensure_ascii=False, indent=2), encoding="utf-8")
def schedule(card, q, today):
"""SM-2:q は 0〜5 の自己評価。3 未満なら最初からやり直し。"""
if q < 3:
card["reps"], card["interval"] = 0, 1
else:
card["reps"] += 1
if card["reps"] == 1:
card["interval"] = 1
elif card["reps"] == 2:
card["interval"] = 6
else:
card["interval"] = round(card["interval"] * card["ef"])
card["ef"] = max(1.3, card["ef"] + 0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))
card["due"] = (today + timedelta(days=card["interval"])).isoformat()
return card
def add(question, answer):
cards = load()
cards.append({"q": question, "a": answer, "reps": 0, "interval": 0,
"ef": 2.5, "due": date.today().isoformat()})
save(cards)
def review():
cards, today = load(), date.today()
due = [c for c in cards if c["due"] <= today.isoformat()]
for card in due:
input(f"\nQ. {card['q']}\n(思い出せたら Enter)")
print(f"A. {card['a']}")
q = int(input("自己評価 0〜5(5=即答、3=なんとか、0=まったく): "))
schedule(card, q, today)
save(cards)
print(f"\n{len(due)} 枚を復習しました。残りのカードは {len(cards) - len(due)} 枚です。")
if __name__ == "__main__":
if sys.argv[1:2] == ["add"] and len(sys.argv) == 4:
add(sys.argv[2], sys.argv[3])
elif sys.argv[1:2] == ["review"]:
review()
else:
print(__doc__)
1枚のカードを、毎回の自己評価を変えながら出題させてみると、間隔はこう伸び縮みする。
| 回 | 自己評価 | 次の出題まで | 次の出題日 |
|---|---|---|---|
| 1 | 4(少し考えて思い出せた) | 1日 | 10月12日 |
| 2 | 5(即答) | 6日 | 10月18日 |
| 3 | 4 | 16日 | 11月3日 |
| 4 | 2(忘れていた) | 1日 | 11月4日 |
| 5 | 4 | 1日 | 11月5日 |
| 6 | 5 | 6日 | 11月11日 |
忘れたら容赦なく振り出しに戻る。でも、「やさしさ係数」にあたる EF は少し下がるだけで、ゼロからにはならない。人間の記憶も、たぶんこんな感じなんだと思う。
カードの答えは、AI に書かせずに自分で書くことにした。答えを自分の言葉で書く作業が、それだけで1回分の復習になるから。
10月24日(土) 14日目:2週間を振り返る
今日で2週間。The Book は第10章(ジェネリクス、トレイト、ライフタイム)まで進んだ。AI に全部書いてもらっていたら、たぶん倍は進んでいたと思う。
でも、進んだ量の代わりに、手元に残ったものがある。
2025年に MIT のメディアラボが、エッセイを書くときに LLM を使ったグループ、検索エンジンを使ったグループ、何も使わないグループの脳波を比べた研究を公開していた。LLM を使ったグループは脳の各部位のつながりが最も弱く、書き終えた直後でも、自分のエッセイの一節を正確に引用できない人が多かったそうだ。参加者は54人と少なく、査読前の論文なので、結論を大きく受け取りすぎないほうがいい。それでも、自分が感じていた「AI に書いてもらったものは、自分の中に残らない」という感覚には、そこそこ心当たりのある結果だった。
論語に、こういう一節がある。
学びて思わざれば則ち罔し、思いて学ばざれば則ち殆うし
(学ぶだけで自分で考えなければ、ぼんやりして身につかない。考えるだけで学ばなければ、独りよがりで危うい)
2500年前の言葉だけど、生成 AI との付き合い方の説明として、これ以上のものを知らない。AI から答えを受け取るだけなら「学びて思わざる」になる。かといって、AI も本も使わずに自分の頭だけで考えていたら「思いて学ばざる」になる。
今回の2週間でやっていたのは、たぶんその間を歩くことだった。まず自分で考えて、仮説を書く。それから AI に、答えではなく方向だけを聞く。間違えたら、それをカードにして、忘れかけたころにもう一度思い出す。
「習うより慣れよ」という言葉もある。AI がいる今は、この「慣れる」の部分を、つい AI に肩代わりさせてしまう。動くコードはもらえる。でも、手が慣れることはない。慣れるところだけは、遠回りでも自分の手でやるしかないんだと思う。
10月25日(日) 15日目:この記事を書きながら
この記事を書く前に、日記もノートも見ずに、1週間テストでは書けなかったライフタイムの説明を書いてみた。
「参照が、参照先より長く生きないことをコンパイラに保証するための注釈。注釈そのものが寿命を延ばすわけではなく、複数の参照の寿命の関係をコンパイラに伝えるだけ」
ハマりノートでは3回出題されて、そのうち1回は忘れていたカードだ。合っているかどうかを、今度は答えを言ってもらう形で Claude に確かめた。
「おおむね正確です」と返ってきた。
「おおむね」の部分が何なのかは、来週の自分に考えてもらうことにします。カードを1枚、追加しておきました。
