二週間で簡単なインタープリタ言語を実装してみた (日記)

私は昔から言語処理系に興味があり、自分で言語を作ることを夢見てきました。 しかし、いざ言語を実装しようと思って言語処理系に関する本を読んでも何から手を付けていいか分からず、アセンブラもまともに読めないまま、数年が経ってしまいました。 大学時代は情報系ではなかったため、コンパイラの実験がある情報系の学科のカリキュラムを羨ましく思い、情報系の授業の教科書を手にとって見ても読む気が起きず、自分に作れるのは所詮、構文木をちょこっといじって変換するレベルのもの (例えばsjspなど) にとどまっていました。

そんな中、去年のRebuild.fmで、とても感銘を受けた回がありました。 LLVMのlinkerであるLLDを開発されているrui314さんの回です。 rebuild.fm セルフコンパイルできるC言語コンパイラを実装するという話のなかで、インクリメンタルに開発する重要性について話をされています。

qiita.com

この回を聞いてやっと、コンパイラの書き方がわかった気がしました。 コンパイラに関する本を読んでも実装の進め方がわからなかったのは、コンパイルフェーズごとに章に分かれていたからで、正規言語の難しい話を読んでは本を投げ出していたのです。 Rebuild.fmのrui314さんのお話を聞いて、まず最初は小さい言語をとにかくパースからコード生成まで通して動くようにして、そこから徐々に文法を増やしていくというインクリメンタルなアプローチが有力なのだとやっとわかったのです。 ここで最初の言語は、関数もforループも演算子もない、とてもプリミティブな文法だけもつ言語から始めるのです。 コンパイラの教科書を読んでもさっぱりわからなかった実装の進め方が、ようやくわかった瞬間でした。

最近、まつもとゆきひろさんの『言語のしくみ』が出版されたこともあり、言語実装の熱が高まっているように感じます。 私は今年は言語実装の年にしようと思っていて、その第一歩としてインタープリタ言語を書いてみました。 まずやったことは、mrubyやLuaなどの実装を手元に引っ張ってきて、ctagsを打って数日読んでみました。

Luaのファイル構成は分かりやすいので読みやすいです。mrubyはmrbgems/mruby-compiler/coreやsrc/vm.cなどを中心に見ていきました。ざっくり雰囲気を掴むくらいの気持ちで読みました。 あとはひたすら実装を進めていきました。

つまり参考にしたものはmrubyとLuaです。 書籍としては以下の本を挙げておきます (もうちょっとやわらかいインタープリタ実装の本ある気がするけど知らないです)。

大学時代に買ったけど当時はあまり理解できず、最近開いてみてようやく理解し始めたコンパイラの本。 この本だけ読んでてもよく分からないけど、自分でVMを書いてみてから改めてこの本を読んでみたら、納得する部分がたくさんありました (特に関数のコード生成について)。 大学で使われている教科書で少し硬い。

情報系教科書シリーズ コンパイラ

情報系教科書シリーズ コンパイラ

Direct threadingなどmrubyで使われている技術がよく分かる本 (すみません、最初の方をちら見しただけでちゃんとは読んではないです…)。

rui314先生をリスペクトしているので、私も日記形式でお送りしてみます。

1月3日

実家から戻り、インタープリタ言語を作る意識を高める。 ディレクトリを作り、yaccファイルを用意する。

1月7日

mrubyの構文木の実装を参考にして、ASTを全てnode*で実装することにする。

typedef struct node {
  struct node *car, *cdr;
} node;

とりあえずノードを追加するたびにmallocするような形で実装を進める。 文法は四則演算のみ。

1月8日

構文木のためのメモリープールを実装する。 ASTは全てnode*で表すことにしたので、この構造体のみ考えておけば良いのは楽。

四則演算のみの言語に対して、コード生成と実行器を実装する。 実装が簡単なスタックマシンで作る。

今ある文法は

  • 式 (expression) が並んだものがprogram
  • expressionは四則演算が使える
  • リテラルは整数 (long) または浮動小数点数 (double)
enum OP_CODE {
  OP_ADD,
  OP_MINUS,
  OP_TIMES,
  OP_DIVIDE,
  OP_LOAD_LONG,
  OP_LOAD_DOUBLE,
  OP_PRINT_POP,
};

OP_PRINT_POPはちょっとダサい…

1月9日

コードが読みやすいようにいくつか変数名をリネーム。 変数を作り始めるが難しくて散歩に出かける。

夜、変数を実装。新たに追加した文法は

  • 変数に代入できる
  • 四則演算のなかで変数を使える
  • print文
statement         : IDENTIFIER EQ expression
                    {
                      $$ = cons(nint(NODE_ASSIGN), cons($1, $3));
                    }
                  | PRINT expression
                    {
                      $$ = cons(nint(NODE_PRINT), $2);
                    }
                  ;

OP_PRINT_POPを削除してOP_ASSIGN, OP_PRINT, OP_LOAD_IDENTを追加。

 enum OP_CODE {
+  OP_ASSIGN,
+  OP_PRINT,
   OP_ADD,
   OP_MINUS,
   OP_TIMES,
   OP_DIVIDE,
   OP_LOAD_LONG,
   OP_LOAD_DOUBLE,
-  OP_PRINT_POP,
+  OP_LOAD_IDENT,
 };

OP_ASSIGNOP_LOAD_IDENTのoperandは、変数配列のindexが入っているので、実行器の雰囲気はこんな感じ。

+      case OP_ASSIGN:
+        e->variables[GET_ARG_A(e->codes[i])].value = e->stack[--e->stackidx];
+        break;
       case OP_ADD: BINARY_OP(+); break;
@@ -172,6 +214,9 @@ static void execute_codes(env* e) {
         break;
+      case OP_LOAD_IDENT:
+        e->stack[e->stackidx++] = e->variables[offset - GET_ARG_A(e->codes[i])].value;
+        break;

booleanリテラルを実装する。まだ演算はない。

1月11日

簡単なテストケースを書く。

foo = 3 * 4 / 5 + 6.7 * 7
bar = foo * 3.1 + foo / 7.2
print foo + bar

このスクリプトを実行すると207.281666667と表示される。 Python 2のコンソールで確かめながら実行結果を確かめる。

if文を実装する。Vim scriptに倣って、if … endifにする。 if文の実装に伴って、OP_JMP_NOTを作る。 各ステートメントの命令数をカウントしないといけないことに気が付き、codegen関数の返り値をvoidからuint16_tにする。

1月12日

else文を実装する。 無条件にジャンプするOP_JMPを作る。

if true
  a = 10
  print a
else
  b = 11
  print b
endif

このスクリプトは次のようなコードに変換されて実行される。

bool true
jmp_ifnot 5
long 10
let 0
load 0
print
jmp 4
long 11
let 1
load 1
print

for文の中にprogram counterをincrementするところがあるので、jmp_ifnotのoperandが1少ないように見えるがこれで正しい。

elseif文を実装する。 構文木を構築するときに新しいif文にしてしまうことで、コード生成を触らなくても実装できてしまうことに気がつく。

デバッグしやすいように、構文木LISPのような形式で表示できるようにする。

1月13日

while文を実装する。 これまでoperandはuint16_tだと思っていたが、これではジャンプ命令で負の値を指定できないという初歩的なミスに気がつく。 とりあえず暫定対処でOP_JMP_BACKを追加する。

>=, ==, <= など、数値の比較を行う演算子を実装する。

&&, || を実装する。これらは四則演算などの他の二項演算子とは異なり、右辺を評価しないことがある。 if文を作ったときに追加したOP_JMP_NOTは評価値をスタックからpopしてしまうため、仕方なしにOP_JMP_NOT_KEEPを作る。

if文とwhile文が実装されたので、なんとなく「言語らしさ」が出てきてかわいい。

1月14日

operandをuint16_tからint16_tに変更し、OP_JMP_BACKを削除する。 少しリファクタリングする。

最近は仕事が忙しくて進められていない。

1月16日

組み込み関数min(), max()を実装する。 関数の引数のASTを組み立てていて、ようやくnode*の良さが分かってくる。 関数の実装はべた書きでスタックを直接操作していて危ない。 関数呼び出しのoperandには、何番目の関数かというindexと、引数の数を指定するようにする。 ユーザー定義の関数の実装を妄想する。

1月17日

単項演算子 +, - を実装する。 組み込み関数abs()を実装する。

1月18日

endifとendwhileをendにする。RubyLuaっぽくてかわいい。

a = 10
while a >= 0
  print a
  if a > 5
    print 10
  else
    print 0
  end
  a = a - 1
end

ユーザー定義関数をとりあえずパースできるようにする。

+statement         : FUNC IDENTIFIER LPAREN fargs_opt RPAREN sep statements sep END
+                    {
+                      $$ = cons(nint(NODE_FUNCTION), cons($2, cons($4, $7)));
+                    }
+                  | RETURN expression
+                    {
+                      $$ = cons(nint(NODE_RETURN), $2);
+                    }

まだコード生成はどうすれば良いのかさっぱりわからない。

ファイルが大きくなってきたので分割する。

1月19日

simple virtual machineという意味でsvmと名付けていたが、support vector machineと紛らわしいので、minivmにrenameする。

ユーザー定義関数の実装に想いを馳せる。

1月21日

ユーザー定義関数のコード生成と実行器を実装する。 program counterをsave・unsaveするOP_SAVEPC, OP_UNSAVEPC、ローカル変数の領域を確保と解放を行うOP_ALLOC, OP_UNALLOCを実装する。 少し汚いけど、実行時には変数領域を逆さまに使うようにすると、確保した数を持たなくてよいので楽ということに気がつく。 関数の実行は、おおむね次のような流れになる。

  • 引数を順番に評価してスタックに積む
  • 現在のprogram counter (呼び出し前のpc) をスタックに積む
  • 関数呼び出しを行う
  • ローカル変数のための変数領域を確保
  • popして呼び出し前のpcをローカル変数に入れる
  • 引数をpopして後ろからローカル変数に入れていく
  • 関数の中身を評価する
  • return文で、ローカル変数の変数領域を解放し、呼び出し前のpcに戻す

しかし、関数の中からグローバル変数が見れないとか、再帰呼出しができない (関数自体はグローバル変数領域に確保されるが、関数の中身のコード生成時にローカル変数リストを見ているため) という問題に直面し、頭を抱える。昼寝する。

単項演算子 ! (not) を実装する。

1月22日

グローバル変数とローカル変数は全く性質が違うと悟る。 湯淺先生の本の関数のコード生成の節を読む。 ローカル変数に対するオペコード OP_LET_LOCAL, OP_LOAD_LOCAL_IDENT を追加し、グローバル変数とは区別するように実装し、ようやく関数の中からグローバル変数を参照したり、再帰呼び出しできるようになる。

a = 10
b = 20
func foo(x)
  b = 30
  return a * b + x
end

print foo(50)
print a
print b

このスクリプトが350, 10, 20と表示するようになる (昨日の時点ではグローバル変数領域を見れてなかったので、Undefined variable: aだった)。

フィボナッチ数を計算する (効率の悪い再帰呼び出しの) スクリプトが動き、とても興奮する。テストケースに追加する。

OP_UNALLOCOP_UNSAVEPCを削除してOP_RETに統一する。ローカル変数領域を明け渡すとともに、program counterを呼び出し元に戻す。

break文とcontinue文を実装する。 continueは簡単なのでまあいいとして、break文のジャンプ先がわからずに小一時間悩む。 ワンパスで生成までやっているので、while文の中身を生成しているときは飛び先のpcが分からない。 仕方なく、while文の先頭にwhile文の最後まで飛ぶジャンプ命令を追加する。

スタックの一番上をコピーして積むOP_DUPを追加して、 &&, ||を作ったときに追加したOP_JMP_IF_KEEPOP_JMP_NOT_KEEPを削除する。

パフォーマンス改善のために、即値を足したり引いたりするOP_IADDOP_IMINUSを追加する。 足し算は可換なので即値が左のときも適用できるはずだけど後回し。

そこそこコードを書けるようになったので、軽くスクリプトを書いてみたり速度比較をしてみる。 以下のスクリプトが動く。

func fib(n)
  if n <= 1
    return 1
  end
  return fib(n - 1) + fib(n - 2)
end

n = 0
while n < 38
  print fib(n)
  n = n + 1
end

このスクリプトを9.05sで実行できた。 これと全く同じことを行うスクリプトruby 2.1.9で9.10s, mruby 1.2.0で14.78s, Lua 5.2.4で12.83s, Python 2.7.12で26.30sかかった。 スクリプト言語としてそこそこの速度が出ている事がわかる。

なお、生成された中間コードは次のような感じになった。

long 3
let 0
jmp 20
ret 2
alloc 2
let_local 1
let_local 0
load_local 0
long 1
<=
jmp_ifnot 2
long 1
jmp -10
load_local 0
iminus 1
call 0 1
load_local 0
iminus 2
call 0 1
+
jmp -18
long 0
jmp -20
long 0
let 1
jmp 1
jmp 11
load 1
long 38
<
jmp_ifnot 7
load 1
call 0 1
print
load 1
iadd 1
let 1
jmp -11

こうやって改めて見てみると、やはり関数の最初にretがあったりwhile文の最初のjmpとかして意味不明かもしれない。 returnやbreakなど、生成時に本来飛びたいジャンプ先がわからずにこうなってしまった。 ワンパスで生成するのにこだわらなかったらもう少し素直なコードになるはずだけど、今回は深追いしないことにする。

これからやること

言語処理系としてやらないといけないことは、まだまだたくさんあります。 文字列や配列、辞書なども作りたいし、型とか関数引数の数のチェックなどをスキップしているので、誤った文法のスクリプトを流すと簡単にセグフォしてしまいます… 流石にそういうのは直したい。 構文エラー時のメッセージも雑すぎるのでなんとかしたいと思っています。 今の実装だとおそらくクロージャが作れないので、もう少しスコープをきちんと扱えるようになってから考えたいです。

それっぽく動くものはできたけど色々と気に入らない部分はあるので、今回の実装はいったん捨てて、また1から書いていくと思います。 参考程度にGitHubリポジトリを置いておきます。

まとめ

二週間で初めてのインタープリタ言語を実装できたので、今年中にあと23個くらい言語処理系を書くと思います。 今回はスタックマシンで作りましたが、レジスタマシンのコード生成はどうすればいいのかよくわからないし、並行処理のコード生成とか検討もつきません。 LLVMバックエンドを使ってみたり、アセンブラを吐いてみたり、GCを実装してみたり、あるいはgoroutineスケジューラの実装も読んでみたりと、やりたい事が山積みです。 Scalaのwebアプリケーションを運用している以上はJVMについて詳しくなりたいという思いもあります。

その最初のステップとして、簡単なインタープリタ言語を実装してみました。 とりあえず最初の一歩を踏み出せてよかったです。 小さい構文から始めて、少しずつ構文を足していくというやり方は、言語処理系を作る上で有用だと思います。 よく考えてみればそりゃそうかという方法ですが、rui314さんのお話を始めて聞いたときは目から鱗が落ちる思いがしました。

ASTにmruby方式 (LISPのようにcar cdrで表す) を採用したことは、良い面と悪い面がありました。 メモリー管理は1つの構造体について集中しておけばいいので楽です。 関数の引数部分のように、まさにリスト構造になっている部分は配列にするよりも、consで表すほうが楽だと思います。 悪い面としては、コード生成時にcarやcdrが何を表しているのかよく分からなくなるという問題があります。 パーサーの対応する部分を見れば分かるのですが、生成部分のコードだけを見ていたら何も分かりません。 次に書く言語では、この方式は使わずに実装してみたいと思います。

言語実装の楽しみは、バグっていたら全く動かないし、きちんと実装できたら、その文法で書けるあらゆるコード、あらゆるアルゴリズムが動くようになるというところにあると思います。あるところまで書いたら可能性が一気に広がる楽しさというのは昔Tweetしたことがあります。 この思いは今でも変わっていません。

言語処理系の分野は広大で、いくらでも学ぶ楽しみが広がっています。 いくらやっても学ぶことがあり、しかも実装してみて動くとすごく楽しいというのは、プログラマの趣味としてはうってつけではないでしょうか (趣味レベルに作る程度だと、本業で研究されているレベルまではいくらやっても到達しないままかもしれませんが…)。 実装が公開されている言語もいくつもあり、素晴らしい教材がたくさん見つかります。 第一歩として、スタックマシンのインタープリタ言語を実装してみました。 レジスタマシンやネイティブコード生成、LLVMバックエンドなど、色々な方式を試しながら、言語処理系作りを楽しみたいと思います。

2016年を振り返って

会社は二年目に入り業務にも慣れ、ある程度まとまった仕事を任せられるようになりました。 携わっているサービスのコードに詳しくなり、リファクタリングの方向性を示して改善を進めてきました。 難しい障害も乗り越えながら、引き継いだ手綱を何とか制御できるようになってきたという所感です。

今年は18記事書きました。特に反響の大きかったエントリーは次の3つの記事でした。 内容の方向性もバラバラであまり何したいかよく分からなくなっていますね。どういう技術を学んでいくか悩んでいた一年だったと思います。ブログには書いていませんが、Vimソースコードをいじったりmrubyのコードを読み込んだりしていた時期もありました。

一年に一つ言語を学べという教えを元に次に学ぶべき言語を模索するために様々な言語を触ってみるというチャレンジに取り組んだのですが、その後とくに新しい言語を触れていないのは反省しています。 少しRustのチュートリアルを進めましたが、実際に自分で何かを作ってみるところまでは進めませんでした。 観測範囲ではRustは徐々に人気が高まっているので来年には真面目に手を付けて扱えるようになりたいものです。

最近はVM関連の関心が高まっていて、インタープリタ言語の実装を調べているところです。 ソースコードだけを読んでいるとなかなか背景の技術まで理解できなかったりするのですが、mrubyに関しては最近出版された以下の本が参考になります。

mrubyのmrb_vm_execにあるoptableあたりのコードを最初見た時は何だこれはと思ったのですが、この書籍を読んでようやく意味がわかりました。 調べてみるとVMの実装では有名なテクニックらしいです。 来年は自分で言語を実装しながら色々と学びたいと思います。

Vimは今でももちろん毎日使っていますが、プラグインを制作したいという欲は沸かなくなっています。 既存のプラグインに対するissue報告に対処しながら、リファクタリングなどをのんびりやっていました。 特に、thumbnail.vimの大幅なリファクタリングは時間がかかりました。 thumbnail.vimは自主的に作った最初のプラグインですが、そのせいでコードは複雑に絡み合っており、冗長なtry catchや難解な再帰呼び出しを前に、長い間コードに手を付けられずにいました。 calendar.vimでの設計をベースに、絡み合ったコードを丁寧に解きほぐし、何とかきれいにまとめることができました。 設計のめちゃくちゃなコードを挙動を変えずにリファクタリングするのはとても大変ですね。

来年は、興味のあるVM実装周りの知識を深めながら手を動かして実装を進めていきたいと思っています。 また、良い言語だという予感はあるもののなかなかチュートリアルレベルを抜け出せていないRustを真面目に使ってCLIツールを作ってみたいと思っています。 HaskellScala、Go、JavaScriptそしてRustもスキルセットに揃うと、多くのシーンに柔軟に技術を選択できるのではないかなと思っています。

2016年もいい年でした。技術を深め研鑽しながら来年も頑張っていきましょう。

堤京介「終わらないよ。あきらめなければ、終わらない。俺は何度でも挑戦する。」

ef - a tale of memories.

珍しいSHA1ハッシュを追い求めて

SHA1ハッシュってあるだろう?」

放課後、いつものように情報処理室に行くと、高山先輩が嬉しそうな顔でそう言った。

「ええ、SHA1、ありますね」

SHA1って何桁か覚えているかい?」

「えっと…」

一年下の後輩、岡村が口を開いた。

「50桁くらいはありましたっけ…?」

先輩はパソコンに向かって何かを打ちはじめた。

現在、情報部の部員は三人しかいない。部長の高山先輩と、二年の自分と、後輩の岡村だ。いや、正確に言うと、先輩の学年にはもう少しいたのだが、もうほとんど部室に来ることはなくなってしまった。無理もない、この季節になると先輩たちは受験勉強で忙しくなる。

「例えば、こういうふうに… 適当なSHA1の長さを…」

echo -n | openssl sha1 | awk '{print length}'

部長だけは今も部活に来てこうやって色々なことを教えてくれている。本人曰く、普通に勉強したら受かるでしょ、らしい。そう言ってもあまり嫌味には聞こえないのが先輩のいいところだ。

「40だ。40桁だね」

「そうなんですね」

後輩の岡村は身を乗り出して先輩の端末操作を観察している。

「正確には160桁ですよね…」

僕は思わず口を開いた。

「そう、桁というより、160ビット、それを16進数で表現するから…」

先輩はそう言いながらプリント用紙の山から一枚手にとって説明しはじめた。

「こうやって4ビットずつ区切って0からfで表現するから、160ビットのハッシュを40桁で表現できるんだ」

「なるほど〜」

岡村は指を折りながら2進数表現を確認している。岡村の理解が追いついていることを確認しながら先輩は口を開く。

「つまり、SHA1ハッシュは16文字40桁で表現される。つまり16の40乗種類考えられるわけだね。これはもちろん2の160乗と同じ」

そう言いながら先輩はプリント用紙に簡単な式を書く。

「えっと、0.3かけると… これくらいかな…」

16^{40} = 2^{160} \simeq 10^{48}

「10の48乗…たくさんありますね…」

「そうだね、そこそこ多い」

先輩はシャーペンを顔の前で振りながらこう言った。

「そこでだ、SHA1ハッシュで、全てが数字になることはあるだろうか」

全て数字になるSHA1ハッシュ

情報部の活動は、いつもこんな感じで始まる。 誰かが問題を持ち寄って、それを下校時刻までにそれぞれ解く。 今日は部長の出題というわけだ。

空文字のSHA1ハッシュは da39a3ee5e6b4b0d3255bfef95601890afd80709 だ (これはさっき先輩がやってみせたようにecho -n | openssl sha1すると表示される)。これにはdやらaやらeなど、沢山アルファベットが含まれている。 つまり、SHA1ハッシュの16進数表現が、全て数字になるような元メッセージを求めよという問題なのだ。

早速、自分も作業に取り掛かった。 まずどれくらい存在するかを考える。 16文字の中で10文字が数字なので… と言いながらブラウザーのアドレスバーに (10/16)^40 と押してエンターを押した。

\displaystyle \left(\frac{10}{16}\right)^{40} = 6.84 \cdots \times {10}^{-9}

10の-9乗か… しかしSHA1ハッシュから直接元のメッセージを求めるのはできないので虱潰しに調べるしかない…

そんなことを考えながら、プログラムを書き始めた。 メッセージは適当にアルファベットと数字から作って、とにかくsha1ハッシュを計算して条件にあっていれば表示するだけのプログラムだ。 後輩の岡村の様子を見ると、どうやらSHA1ハッシュのアルゴリズムを確認しているようだった。 部長はシャーペンで何やら数式を書いている。

自分のプログラムは、しばらくして次のように出力しはじめた。

gflyP 9708825594493053358052040804954710052563
OqmSX 5405673447021682949913714302263814023323
pcBsd 0830855821698284247230340812332913765683
jPHSf 0747815384663891403181360803310055645039

おお、やはりあるのか…と思いながら、opensslコマンドでハッシュが間違っていないことを確認しながら、プログラムの出力を眺めていた。

「先輩、けっこうありますね…」

「おう、速いな」

「もう計算できちゃったんですか…」

岡村はシャーペンを机に放って、結果を見にやってきた。

「元メッセージはどう選んだんだい?」

先輩は自分が書いたコードを眺めながらそう言った。

「アルファベット大文字小文字もしくは数字です」

「なるほど…」

先輩は、シャーペンを手にとって何やら書き始めた。

「探したのは5桁のアルファベットもしくは数字のメッセージというわけだ。つまり62の5乗は…」

先輩はシャーペンを手に持ったまま、器用にアドレスバーに数式を入れる。

 62^{5} = 916132832

「およそ9億個ですね」

「ということは、そのうちSHA1が全て数字になるのは…」

岡村は必至に食いついてくる。

「9億に10/16の40乗をかけると…」

\displaystyle 62^{5} \times  \left(\frac{10}{16}\right)^{40} = 6.268 \cdots

「6個です。だいたい」

部長は、後輩の理解が追いついていることに満足そうに頷いた。 それは、自分のプログラムが6桁のメッセージの解を表示しはじめたのと、ほとんど同時だった。

gflyP 9708825594493053358052040804954710052563
OqmSX 5405673447021682949913714302263814023323
pcBsd 0830855821698284247230340812332913765683
jPHSf 0747815384663891403181360803310055645039
IL3rj 3243002591985408609566985352935152776909
MUeWp 8297833274599142233719359578426918109541
JG4Y80 0099530489181060830720140193389029330084
WiMtG0 2254364706744121285147874769100420502311
KMnsR0 4760110574803441139829661498017839123177
...

「つまり今考えているメッセージで5文字で、えっと」

部長がその言葉を遮ってこう続けた。

「アルファベット大文字小文字数字で5文字からなる元メッセージに対して、SHA1ハッシュが全て数字となるのは6つ、およそ期待値通りだね」

岡村も頷いた。

「でも、これに何の意味があるんですかね」

僕は思わず尋ねてしまった。

「意味というのは」

「いや、つまりですよ。SHA1というのは160ビットのバイナリー列って最初に言ってたじゃないですか。この16進数の表現は、なんというか、人間が勝手に4ビットずつ区切って勝手に0からfに割り当てただけじゃないですか」

「なるほど、たしかにそれはそうだね」

「つまり、SHA1ハッシュが数字だろうがアルファベットだろうがSHA1にとっては何の特徴にもならない気がするんです」

「なるほどね」

先輩は頷きながらそう言った。

「でも、それだからこそ、こうやって期待値通り、6個見つかったということにもなるね」

「そこなんですけど…」

岡村が怪訝そうな顔で口を開いた。

「それってつまり、16進数の表現の中で、文字がまんべんなく現れるということを仮定していますよね」

「たしかに」

「本当にそうなんですかね」

先輩はしばらく考えていたが、急に笑顔になって、こう言った。

SHA1の文字が本当に一様に分布するか、計算してみよう!」

その言葉と同時に、下校時刻を告げるチャイムが鳴った。

SHA1ハッシュの文字分布

家に帰ってから、改めて全て数字となるSHA1ハッシュの計算を再開した。 数字とアルファベット6文字の元メッセージは 62^{6} = 56800235584個あり、その中でSHA1が全て数字となるものは397個あった。 期待値は388.64個なので、少し多めに見つかっていることがわかった。 7文字の中でも計算しようとしたが、時間がかかってプログラムが終了しなかった (後日結果を見てみたら、24061個見つかった。期待値は24095.86個、その誤差は0.15%未満となった)。

夕食後、SHA1ハッシュの文字分布を集計するプログラムを組んだ。 元メッセージは先程の計算と同じように、アルファベット大文字小文字もしくは数字で選んだ。 元メッセージの長さを軸に集計すると、次のようになった。

|X| 0 1 2 3 4 5
0 5 (12.500%) 133 (5.363%) 9551 (6.212%) 595152 (6.243%) 36950623 (6.252%) 2290283302 (6.250%)
1 1 (2.500%) 157 (6.331%) 9648 (6.275%) 595439 (6.246%) 36949237 (6.251%) 2290285498 (6.250%)
2 1 (2.500%) 139 (5.605%) 9539 (6.204%) 596209 (6.254%) 36946427 (6.251%) 2290302477 (6.250%)
3 3 (7.500%) 140 (5.645%) 9598 (6.242%) 596671 (6.259%) 36949064 (6.251%) 2290259456 (6.250%)
4 1 (2.500%) 164 (6.613%) 9727 (6.326%) 596720 (6.259%) 36943267 (6.250%) 2290339043 (6.250%)
5 4 (10.000%) 143 (5.766%) 9586 (6.234%) 595214 (6.244%) 36942983 (6.250%) 2290318196 (6.250%)
6 2 (5.000%) 175 (7.056%) 9673 (6.291%) 595193 (6.243%) 36931008 (6.248%) 2290360368 (6.250%)
7 1 (2.500%) 163 (6.573%) 9558 (6.216%) 595012 (6.242%) 36937662 (6.249%) 2290455010 (6.250%)
8 2 (5.000%) 149 (6.008%) 9691 (6.303%) 595441 (6.246%) 36950679 (6.252%) 2290411313 (6.250%)
9 4 (10.000%) 172 (6.935%) 9658 (6.281%) 596341 (6.255%) 36943415 (6.250%) 2290385579 (6.250%)
a 3 (7.500%) 168 (6.774%) 9797 (6.372%) 596924 (6.262%) 36936091 (6.249%) 2290363615 (6.250%)
b 3 (7.500%) 131 (5.282%) 9611 (6.251%) 595164 (6.243%) 36942624 (6.250%) 2290301647 (6.250%)
c 0 (0.000%) 186 (7.500%) 9417 (6.124%) 596098 (6.253%) 36928976 (6.248%) 2290281914 (6.250%)
d 3 (7.500%) 176 (7.097%) 9571 (6.225%) 596065 (6.253%) 36936884 (6.249%) 2290307306 (6.250%)
e 4 (10.000%) 153 (6.169%) 9567 (6.222%) 595888 (6.251%) 36930409 (6.248%) 2290299939 (6.250%)
f 3 (7.500%) 131 (5.282%) 9568 (6.223%) 595589 (6.248%) 36934091 (6.249%) 2290358617 (6.250%)

右に行くほど、つまり沢山のSHA1ハッシュを調べるほど、全ての文字が均等に現れることがわかる。 ある特定の文字だけ沢山出現したり、少なかったりすることはなさそうだ。 この結果に満足して、僕は就寝についた。

SHA1SHA1がだいたい数字

「おお、たしかに収束しているようだね」

昨日の計算結果を見せると、先輩は満足そうにそう言った。

「100を16で割って6.25%ですね」

岡村も頷いている。

「まあ証明したわけじゃないが…」

先輩は軽く咳をしてから頭を掻いた。

SHA1の文字は一様に出現することを仮定して、いろいろな期待値をもとめてみよう」

そう言いながら、先輩はテキストファイルを順番に開きはじめた。

LvwEJe1 6051096335827944381165360280845795286423 111613f48230408056a71706b4174442640b6489
sG9wqMH 9788371029031493501751484666965269206614 e6279615888204167759744340b6c13236273b37
AIJsWgU 4833765415800580402215357543416053382134 823885871864d85103339b7110420321b64b7901
HFrIKpt 1515438875168251864313109613925545684403 820316645881e3020f35e48662206491972878c8
wc9U6dv 5898389906639826050665528530112945836398 5966347988220353b91383721534804a3a278c21

多くの数字が並んでいるが、一番右のSHA1には少しアルファベットが含まれているようだ。

「これはなんですか」

思わず僕は疑問を口にした。

「これは、SHA1ハッシュが全て数字となり、そしてそれをさらにSHA1ハッシュを取った時に、アルファベットが4文字以下となるものだ」

岡村は少し混乱した様子で紙を手に取った。

「つまり…まずは全てが数字で… さらにSHA1ハッシュを取ったら40文字の中で4文字以下がアルファベットとなるので、こうなりますね」

\displaystyle \left(\frac{10}{16}\right)^{40} \sum_{k=0}^{4} {40 \choose k} \left(\frac{6}{16}\right)^{k} \left(\frac{10}{16}\right)^{40-k} = 6.687 \times 10^{-13}

先輩は空中で指を振りながら、計算が間違っていないことを確かめながら口を開いた。

「そうそう、元メッセージは昨日と同様、アルファベットもしくは数字の7文字以下で探索したものだから… その数を掛けて…」

\displaystyle \cdots \times \sum_{i=0}^{7} {62}^{i} = 2.394\cdots

「期待値はこれくらいになる」

「あ、少し低くないですか」

岡村がすぐに指摘した。

「期待値が2.4で、見つかったのが5個ですか」

「まあこういうこともあるさ。次のファイルは」

HQC8su9 03003251839018941492807233288828427481a2 3395664741163489868183a7392270978a505005
VByKdTE 027985658547959903b826850439976465234220 1a3268963b445974667880579063493748834739

SHA1ハッシュと、そのさらにSHA1ハッシュをあわせた80文字の中で、アルファベットが3文字しか含まれないものだ。2つしか見つからなかった」

\displaystyle \sum_{k=0}^{3} {80 \choose k} \left(\frac{6}{16}\right)^{k} \left(\frac{10}{16}\right)^{80-k} \sum_{i=0}^{7} {62}^{i} = 3.173\cdots

「今度は期待値が少し高いんですね」

僕はファイルのSHA1を見つめ、目を凝らしてどこにアルファベットがあるのかを探しながらそう言った。

「でも、昨日考えていた、全てが数字になるものは期待値通りでしたよね」

「だいたいね」

「要するに、3個とか4個とかの時はあまり合わないけど」

「沢山見つかるほど、見つかった数と期待値の誤差が小さくなるということですね」

「それはまあ、そうだろうね」

岡村は自分のディレクトリに移動してテキストファイルを開いた。

「ところで僕も探索してみました」

ZDyHlIM 5954449490599416666154915409164008649984

そのファイルには一行しか書かれていなかった。

「これは?」

「全て数字なんですけど、7種類の数字しか含まれていないんです」

「なるほど…」

先輩も身を乗り出して後輩が見つけたSHA1ハッシュを眺めている。

「5と9と4と… 0と6、1、それから8で… 確かに7つの数字みたいだね」

「そうなんです。アルファベットと数字7文字以下の文字列のSHA1の中ではこの1つしか見つかりませんでした」

「貴重だね」

僕も感心して思わず軽く口笛を吹いた。

文字連続

先輩は、また計算結果が書かれたファイルを開いてみせた。

dGtWcS d6141925ca72293fba0b5f43000000000000f70b
9J2DJv8 a9fc511111111111125d3d87910d68ddfe350f24
QDi79qF c80b072b3f2c2b6b34cb404cfc2555555555555a
cE2sXYZ d59938cf4afe4effffffffffff6f8181ef568bc1
y1m5aTz b60a1e6666666666665f09a5d8a0c05eb382dca0

「これは、同じ文字が12個連続するところがあるSHA1ハッシュだ」

「おおお」

「それから、13個連続するのも1つ見つけることができた」

rcb0lQv 3333333333333455f30f9a9f761fb8e94b906a0f

「なんかすごい」

「つまり、12個以上連続するものがいま6個あるのだけど…」

そう言いながら先輩はシャーペンを持って期待値を計算しはじめた。

「連続している文字が16通りで、その位置が40-12+1通りだから…」

\displaystyle 16 \cdot (40 - 12 + 1) \left(\frac{1}{16}\right)^{12} \sum_{i=0}^{7} {62}^{i} = 5.900 \cdots

「だいたい期待値通りですね」

岡村は期待値の式を眺めながら数式の意味を追っている。

「他にもこんなものも見つかった」

先輩はそう言いながらファイルを開いた。

w5PPbh5 222222222220101b01d906e32e61373b45265361
bzajR5G 555555555558a91de2fb2fa98af5048bfd887c65
daycahL 2222222222281376525c8dbab5a18e3ca933d5c3
Q4dDYWa fffffffffffa03a907d9650eb91bc99642a86199
2o0XCwe 888888888886d55909a93a870fe44fd91aea5392

「先頭11文字が同じSHA1ハッシュだ」

「おもしろいですね」

GitHubのリンクで、こんなの見つけたらスクショを取りたくなりますね」

「そうそう、こんなSHA1はおもしろい、ただそれだけでいいと思うんだよね」

さらに珍しいSHA1を目指して

部室に夕日が差し込んでいる。まもなく下校時刻になる。

「色々な性質のSHA1ハッシュが集まりましたね」

岡村は感慨深くため息を付いた。

「そうだね。まず考えたのが全て数字、二回SHA1を取ったもの、それから岡村くんが見つけてくれた、7つの数字しか含まれていないもの、先輩が計算した連続文字…」

「いろいろあるね」

先輩はしばらく夕日を眺めてから、口を開いた。

「全て同じ文字となる確率はどれくらいかな」

岡村はアドレスバーに式を打ち込んで答えた。

\displaystyle 16 \cdot \left(\frac{1}{16}\right)^{40} = 1.095\cdots \times 10^{-47}

「10の-47乗ですね。だいたい」

「なるほど」

先輩は頷きながら暗算してこう続けた。

SHA1が全て0となる確率は、それを16で割ると、6、いや7かける10の-49乗くらいかな」

「そうですね」

岡村は式を調整しながら答えた。

「じゃあ、いくつSHA1を探したらどのくらいの確率で全て同じ文字のものを見つけられるかな」

先輩は少し意地悪そうにこう言った。

「えっと、さっきのを1から引いて… あれ」

どうやら検索エンジンの誤差で計算できないようだ。僕はすかさずWolframAlphaを開いて計算してみせた。

\displaystyle \left(1 - 16 \cdot \left(\frac{1}{16}\right)^{40}\right)^{10^{46}} = 0.8963\cdots

「10の46乗個探すと、10%の確率で見つかる計算になります」

「1秒に10の7乗個のSHA1を計算できるパソコンがあるとしよう」

岡村はすぐに計算して結果を答えた。

「およそ3かける10の31乗年かかりますね」

「宇宙ができてから確か138億年だよね」

僕がそう言うと、すぐに岡村は計算結果を割って答えた。

「宇宙が10の21乗個できちゃいますね」

「そりゃあ、大変だなぁ」

先輩が夕日を眺めながらそう言うと、下校のチャイムが鳴った。どのくらいの性質のSHA1なら現実的な時間で見つけられるか、そんなことを議論しながら、僕らは帰路に着いた。

git grepで仕事してる

私はコードを書く時に頻繁にgit grepを使っていて、一日に何回くらいgit grepを使っているのか気になったのでログを取ってみました。

2016 10/24 月: 61
2016 10/25 火: 36
2016 10/26 水: 19
2016 10/27 木: 80
2016 10/28 金: 51
2016 10/31 月: 96
2016 11/ 1 火: 47
2016 11/ 2 水: 53
2016 11/ 4 金: 84
2016 11/ 7 月: 56
2016 11/ 8 火: 33
2016 11/ 9 水: 19
2016 11/10 木: 71

これは私が会社のPCでエディタの中でgit grepしたログを集計したものです。 結構ばらつきはありますが、40〜50回くらい、多い日で100回近くgit grepしているということが分かりました。 仕事の時間を割って平均したら大体5〜10分に一回はgit grepしていることになります。 もちろんコンスタントに使っているわけではなく、コードを書く時は増えますしミーティングがあれば減ります。 ですから、会議の多い日や設計段階を考えている日などは少なくなります。

私はVimScala・TypeScript・Goなどを書いています。 例えば、サーバーサイドを触る時にはコードとテンプレートを行き来するためにgit grepしますし、フロントエンドのコードも大体サーバーサイドと似たような変数名でモデリングしているため、git grepで関連ファイルを開いています。 JSONのフィールド名でもgit grepしますし、サイトのこの部分を変更したいという時は、クラス名を拾ってgit grepしてhtmlファイルを開きます。 ファクタリングや変数名関数名の変更も、まず最初にgit grepし、全て変更してからコンパイルします (手元で継続的にコンパイルするのはつらい)。 昔はcexprを使ってquickfixに流していましたが、今はlexprでlocation listに流してファイルを開いています。

他の人のコードのレビューする時は、動作確認したあとに、変更箇所の関数が使われている場所を必ず見るようにします。 関数に限らず、オブジェクト名でも、CSSのクラス名でも、サイトのパスの一部でも、なんでもgit grepします。

ある一つのプロジェクトのコードを長いこと触っていると、大体のコードの見た目が頭に入ってしまいいます。 DBでgroup byして引くときのORMのコードパターンはこんな感じなので…みたいな曖昧な記憶でも、git grepで目的のコードを開けたりします。 他のケースとして、エラーメッセージが出た時には、そのメッセージでgrepしてメッセージIDを見つけて、さらにそのメッセージIDでgrepするという二段git grepが発動します。

外部ライブラリも大体手元に落としてきてgrepするようにしています。 自分たちのコードからctagsで外部ライブラリに飛び、さらにそのリポジトリの中でgit grepするということがよくあります。

とにかくgit grepに頼っているので、たぶん自分はgit grepで仕事しているんだと思います。

コードを書いたり読んだりすることばかりが仕事ではないので、単純には仕事量の尺度にはなりませんが、しばらくログを取ってみて集計してみるとおもしろいことが分かったりするかもしれません。ぜひログを仕込んで回数を調べてみて下さい。あなたは一日に何回くらいgit grepしていますか?

スマホが割れた日

その瞬間は、前触れもなくやってきた。

いつものように、仕事帰りに烏丸御池の交差点で信号待ちをしていた。ちょうど赤に変わったタイミングで時間があったので、スマホでニュースを眺めていた。信号が切り替わり、そろそろ渡ろうとしてスマホをポケットにしまおうとした瞬間だった。左手と右手がいきなりぶつかり、右手の力が緩み、スマホは宙に舞った。

何が起きたのか自分でもわからなかった。はっと我に返ったら、スマホは硬いコンクリートに打ち付けられていた。

一体左手で何をしようとしたのだろうか、今となってはもう思い出せない。右手に痒みを感じて掻こうとしたのか、スマホを左手に持ち替えようとしたのか、あるいはゴミが入った目を掻こうとしたのか。両手が別の目的を持って動線を描いた結果、衝突してしまった。

精密機器がコンクリートにぶつかる瞬間、明らかによくない音がした。すぐに拾い上げて状況を確認したが、画面には無数のひびが入り、悲惨なものだった。全くなんでもないですよ、最近のスマホは衝撃には強いんですよと、周囲の人に心配されないよう冷静を装いながら横断歩道を渡ったが、画面の割れたスマホを握る手は震えが止まらなかった。

家に帰ってから改めて詳細に観察してみた。画面はもう見るも無残な姿だった。インクを流して蝶の羽根に見立てるのも悪くないアイディアだ、それくらい無数のヒビが走っていた。しかし枠は意外と無傷だった。いや、ほんの少しの傷はついていたが、枠が地面に当たったとは思えなかった。おそらく画面を下にして落下したのだろう。何よりもの救いだったのが、画面が割れた以外は、動作に全く問題がなかったことだ。落ちた直後に拾い上げたときも電源はついたままだったし、画面が割れていてもタッチパネルの操作は通常通り行うことができた。ただ、画面から少し変な油の臭いがした。

暫くの間、様々な思いが頭を駆け巡った。二年間大事に扱ってきたものを、一瞬の不注意で落としてしまったことが悔しかった。あの瞬間、なぜ両手がぶつかってしまったのだろう。反射的に足で受け止めていれば衝撃は和らいだだろうか、本当にインクを流してやろうか、流石にこの見た目はお客さんに会うときには恥ずかしすぎるな、自分でネジを開けて交換して直そうか、細かい紙やすりで削ったらちょっとはマシになるのではないか、新しい機種が出ているわけだし自分で直してうまくいかなければ新しいものを買えばいいんじゃないか。

シャワーを浴びながら様々なことを考えたが、改めて机の上においている無残な画面を見て、餅は餅屋、根は張るかも知れないが専門家に任せるのが一番だと言い聞かせて、修理の予約を取った。なんだ、ジーニアスバーだなんて小洒落た名前をしているじゃないか。

快晴だった。阿蘇山の噴火のニュースを明け方まで聞いていて、よく眠れなかった。2万円までは払う覚悟で銀行でお金を下ろし、阪急電車に乗った。帰省の時くらいしか使わないから、夏の盆以来だった。土曜日の昼間にも関わらず、満席で立っている人も多かった。

いつもならスマホでニュースや技術ブログを眺めるところだったが、画面の割れたスマホを電車の中で見るのはなぜか自分が許さなかった。未成年での飲酒は流石に良くないんじゃないかと無粋なツッコミを入れながら、新海誠の作品を読み耽った。孤立感と憧憬、美しい女性たちの繊細な心情描写にすぐに惹き込まれていった。

梅田は久しぶりだった。いつも帰省のときには十三で折り返してしまうし、そもそも梅田に用事があるなんてことはなかった。もしかしたら就活でスカイビルに来て以来ではないか。そう思った瞬間、就活の時の様々な嫌な思い出が蘇り、慌てて心から追い出した。もうあの頃の自分とは違うんだ。

予約時刻までしばらく時間があったので、ヨドバシで暇を潰すことにした。京都でもそうなのだが、ヨドバシに入るとまず必ず地下一階に降りる。ラップトップなんてめったに買わないものだし相場感覚が分からなくなるものなので、たまにはこうしてスペックや価格を眺めて歩くのだ。SONYのラップトップを片手で持ち上げてみたり、こんな薄いものに未だにLANポートを作る技術に感心したり、いつまでも変わらないメモリー容量に落胆したりした。少し奥まったところに自作用のパーツも並んでいた。自作PCというのは作ったことないのだが、ラップトップのHDDをSSDに交換したときから、部品の価格にも目を向けるようになった。

京都のカメラコーナーは一階なのに、梅田は二階なんだなと呟きながらエスカレーターを登る。特に買いたいものはないのだが、唯一持っているNicon 1用のレンズは欲しくなることがある。財布に入っている現金で買えるのではないかという邪念を振り払い、カメラコーナーを後にする。途中で円偏光フィルターが目に止まった。大学で量子力学をやっていたものだから、偏光板や波長板などは研究室に身近にあった。円偏光を直線偏光にして偏光板を通して… あんなに一所懸命研究したのにだんだん記憶があやふやになっている。必死にBloch球に右円偏光と左円偏光を配置する。大丈夫だ、まだ思い出せる。このフロアの中でこのフィルターを量子現象として説明できる者は他にはいまい。なぜか勝ち誇ったような気分でヨドバシを後にした。

f:id:itchyny:20161008135759j:plain

陽射しはいよいよ増していた。道端に銀杏が落ちていなければ、夏かと勘違いするような暑さだった。タワークレーンにレンズを向けてみたが、こんな陽射しの中じゃ新海誠にはならないな、そんなあたり前なことを思いながら南下した。地下鉄で6分だと書いていたので、徒歩でのんびり行くことにした。

f:id:itchyny:20161008141221j:plain

大阪の地理には疎い。神戸に生まれ育った身として三宮の商店街の店には多少詳しいのだが、なんせ大阪まではでてくる機会すらなかった。だから心斎橋とか難波とか言われても位置関係はわからないし、大阪城にはどうやって行けば辿り着けるのかも知らない。しかし、今日の目的地はとにかく御堂筋を歩いていけば着くはずだ。道路標識が、和歌山へと続く道だと主張していた。陽射しの中で歩いていると気分も明るくなり、和歌山には行ったことないな、いつか行ってみるか、そういう気分になった。

大江橋淀屋橋を超えると金融街に入る。銀行支店が道に並ぶ。伏見町はここにもあるのか、水が良いところには酒蔵があると聞くがここはどうなのだろうか、道修町というのはどうしゅうではなくどうしょなのか、難読だなぁとか、1ブロック渡るたびに新しい発見がある。

f:id:itchyny:20161008143044j:plain

本町通を超えて阪神高速をくぐると、また街の景色が変化する。フランク・ミュラーエルメス、スワロフスキー、オメガ、カルティエルイ・ヴィトン。畏れ多くも店の入口に店員が待機していて入ろうものなら扉を開けてくれる親切なお店が立ち並ぶ。夏なんかはあそこに立つのは暑いだろうなぁ。スーツくらいきちんと決めていつか行きたいものだな。そんな風に思って通り過ぎていると、短パンジーンズ麦わら帽子の兄ちゃんが彼女らしき人物を連れて出てきたりしてずっこけそうになる。

心斎橋はおそらく生まれて初めて来た場所だ。高級ブランド店の並ぶ北側よりも、心なしか南側のほうが活気があるようにみえる。人の交通をうまく交わしながら、目的の店へと足を運ぶ。

f:id:itchyny:20161008171021j:plain

店内は混雑していた。店員の数と客の数が1:1くらいなのではと思うくらい、店員がたくさん働いていた。それでもどの店員に声をかけたらいいものかよく分からず、しばらくうろうろしていた。サポートは二階だと聞かされ、オシャレな螺旋階段を登った。

二階も同様に混雑していた。なぜか電源がつかないとかパスワードが分からないとか様々な事情でここに来るんだろう。一時間ほど歩いてきたので少し汗をかいており、座るのが憚られた。しかし、予約時刻を過ぎてもなかなか呼ばれず、貧血で倒れそうになり、仕方なく隅の方に座らせてもらった。

しばらく観察していたが、様々な人が働いていた。男性で長髪の人も、背が高い人も、韓国語や中国語が流暢な人も、足の不自由そうなスタッフもいた。上はみなスタッフのシャツを着ていたが、下はジーンズが多かった。そういう企業文化なんだろうなぁ、いいことだと思った。それにしても少し混雑しすぎていないだろうか。

対応してくれたのは、自分と同じくらいの背丈の男性だった。どうやら3500円ほどで画面の交換をしてくれるらしい。2万円は覚悟していたから、かなりありがたかった。一時間後にできますと言われたので、再び街に散歩に出かけた。用を足したかったので東急ハンズに入り、申し訳程度にボールペンを購入した。一階はすっかりハロウィン模様だった。作りたてのポップコーンの臭いに吐き気を感じながら、その場を離れた。

心斎橋は東西に地下街が発達していた。もう夕方だが今日は朝食をとったきりだったし、一時間ほど歩いたので空腹だった。昼夜兼用にしてしっかり食べよう。学生の頃なら牛丼屋で済ませていたんだろうな。そんなことを考えながら、店を選ぶ。西の端から東の端はかなり距離があり、地下街を歩くだけでも疲れてしまった。地下街の方向と東西南北が数度ずれているのがとても気になった。

結局、東端にある洋食屋を選ぶことにした。少し照明を落としており、こじんまりとした店だった。セットを頼んでから、また新海誠を読み始めた。いい具合の照明と、店内にかかるショパンのワルツが、物語を趣深く装飾してくれて最高の気分だった。中華料理屋を選ばなかった自分を密かに褒め称えた。ハンバーグがとても美味しくて、どうやって作ったのか聞きたいくらいだった。ご飯はおかわりできると案内されたが、ガツガツ食べる気分でもなかったので遠慮してしまった。

人通り忙しない商店街を抜けて、また店まで戻ってきた。数分したら、修理されたスマホが出てきた。間違いなく自分のスマホだったし、それはもう見事だった。素人が開けるときにつけがちなドライバー傷もなかった。保険がなかったら1万を超えていたという。これを修理してくれた技術者は、経験を積み、腕の立つ、顧客の製品を修理するという責任を負うことができる人間に違いない。それは素晴らしい仕事だ。対応してくれた人に対価を払いたい。保険は対価報酬の関係を不確実なものにする。そんなわけの分からない思考も、御堂筋に吹く秋風に飛ばされて、心は澄み切っていった。

もう梅田まで歩く体力は残っていなかったので、帰りは御堂筋線に乗った。ホームボタンが少し固くなっていたり、タッチパネルの滑りが少し悪くなっていることも、部品が新品である証拠だった。あのヒビの入った画面とはもうおさらばなのだ。いつもは必死にタイムラインを消化するのだが、そんな気力もなかった。

阪急京都線に乗り座席につくと、疲れがどっと出てきた。対応してくれた店員の笑顔、一週間は予約がいっぱいだと聞かされて唖然としているおじさん、洋食屋で手をそっと添えてお釣りを渡してくれた店員、修理を待つために店内で座り込んでいた中国人たち、限られた時間内で修理しなければいけないという使命を負って仕事をしている技術者たち、高級宝石店の扉の向こうに立っていた執事のような人たち。色々な人生や考えを想像し、その立場に立って見える風景を思い描きながら、電車の心地よい揺れに誘われ、眠りに引き込まれていった。