emruby: emscripten でブラウザで動く MRI

(この記事は Ruby 25th anniversary のための寄稿です)

Ruby をブラウザで動かすというと Opal ですが、他の選択肢として、C で書かれた Ruby 処理系を emscripten で JS に変換するという選択肢もあります。
しかし調べてみたところ、mruby を WebAssembly に変換した記録は見つかりましたが、MRI でやった例が見つけられなかったので、やってみました。

手順

https://github.com/mame/emruby/ に書いてあるとおりです。

ビルドコマンドは次の通り。

$ emconfigure ./configure CFLAGS="-m32 -s EMULATE_FUNCTION_POINTER_CASTS=1"
$ emmake make V=1 miniruby.js EXEEXT=.js

emconfigure は emscriptenコンパイラとして使う前提で configure を動かすためのラッパ。しかしあまり出来はよくなくて、64 bit 環境で動かすと SIZEOF_LONG が 8 と推定されてしまった(JS 上で sizeof(long) は 4 になるので食い違う)ので、手動で "-m32" を与えています。そのため -m32 でコンパイルできる環境が必要で、ubuntu なら apt-get install libc6-dev-i386 などする必要があります。
また、MRI は関数ポインタについてあまり行儀がよくない(3 引数の関数に 4 つの引数を渡すとかがそこらじゅうにある)ので、"-s EMULATE_FUNCTION_POINTER_CASTS=1" を与える必要があります(参考:emscripten の Function Pointer Issues)。
emmake は make のラッパ。単に miniruby.js を生成するだけです。

所感など

  • 動作するまでに細かい問題がいくつかありましたが、Ruby 側を少しだけいじって対応しました(変更 1変更 2変更 3)。Ruby が公式に emscripten に対応するのは難しそうな印象ですが、本体に無害なパッチならこっそり取り込めると思うので、プルリクお願いします。
  • Kernel#emscripten_run_script を生やしています。(パッチ)
  • miniruby ではなく ruby が動かしたい。(プルリクウェルカム)
  • ファイルシステムを調べて irb くらい動くようにしたい。(プルリクウェルカム)
  • Run ボタンが一回しか押せない。プロセス再実行する方法が見つけられなかった。
  • miniruby.js が 29 MB もある。WebAssembly だと 5 MB くらいになるようですが、Chrome で素直に動かなかったのでよく見ていません。
  • WebAssembly で Ruby が Web フロントエンド市場に(今度こそ)進出できるといいなあ。Opal は JS に合わせて Ruby の仕様を歪めてるのがあんまり好きじゃなくて、emscripten/WebAssembly なら幸せになるかと思ったけど、まだよくわかりません。

SA-IS 法のメモ

suffix array を線形時間で構築する SA-IS 法についてのメモです。

SA-IS 法の解説はわりと世の中にいっぱいありますが、実際のプログラムにする方法がよくわからなったり、どうしてそれでうまく行くのか書かれてなくて気持ち悪かったりするものが多くて、自分の望む感じのものがありませんでした。アルゴリズムは当然プログラムで見たいし、厳密な証明は要らないけど直観的な理由は知りたい。

ということで、自分なりの理解を書いてみました。

suffix array とは

文字列の suffix とは、文字列の途中から最後までの部分文字列のことです。題材の文字列を "mmiissiissiippii" とすると、次の 17 個の部分文字列です。

 0 mmiissiissiippii$
 1 miissiissiippii$
 2 iissiissiippii$
 3 issiissiippii$
 4 ssiissiippii$
 5 siissiippii$
 6 iissiippii$
 7 issiippii$
 8 ssiippii$
 9 siippii$
10 iippii$
11 ippii$
12 ppii$
13 pii$
14 ii$
15 i$
16 $

最後の $ は番兵です。

これを普通に辞書順にソートします。文字には普通に全順序があり、番兵は他のどの文字よりも小さいとします。

16 $
15 i$
14 ii$
10 iippii$
 6 iissiippii$
 2 iissiissiippii$
11 ippii$
 7 issiippii$
 3 issiissiippii$
 1 miissiissiippii$
 0 mmiissiissiippii$
13 pii$
12 ppii$
 9 siippii$
 5 siissiippii$
 8 ssiippii$
 4 ssiissiippii$

このときのインデックスの列、[16, 15, 14, 10, 6, 2, 11, 7, 3, 1, 0, 13, 12, 9, 5, 8, 4] を suffix array といいます。今回説明しませんが、suffix array は検索や圧縮などでいろいろと役立つ数列です。

suffix array を求めるプログラムを Ruby でナイーブに書くと、これだけです。

s = "mmiissiissiippii".bytes + [0]

p (0...s.size).sort_by {|i| s[i..-1] }
#=> [16, 15, 14, 10, 6, 2, 11, 7, 3, 1, 0, 13, 12, 9, 5, 8, 4]

このプログラムは、ソートに O(n log n) 、しかも各比較に O(n) もかかるので、全体で O(n^2 log n) もかかります。SA-IS はこれを O(n) でやってしまいます。すごい。

SA-IS 法の超概要

流れとしては、次のような感じのアルゴリズムです。

  1. 適当な「種」を元に、induced sort というのをやる → 間違った suffix array が得られる
  2. 間違った suffix array を使って、正しい「種」を得る
  3. 正しい「種」を元に、induced sort をもう一度やる → 正しい suffix array が得られる

「種」が何かはあとで説明するとして、最初の目標は induced sort というものを理解することです。しかし induced sort を理解するには、「L 型と S 型」「LMS」「ビン」という概念をまず理解する必要があります。順に説明します。

L 型と S 型

文字列 s のインデックス i から始まる suffix を s[i..] と書くことにします。s[0..] = "mmiissiissiippii$" です。

各インデックスを、次のルールで L 型と S 型に分けます。

  • 最後(番兵)のインデックスは S 型
  • s[i..] > s[i+1..] だったらインデックス i は L 型(i は i+1 より Larger)
  • s[i..] < s[i+1..] だったらインデックス i は S 型(i は i+1 より Smaller)

「インデックス i が L 型」と言ったり、「s[i..] が L 型」と言ったりします。なお、すべての suffix は長さが異なるので、s[i..] == s[i+1..] になることはありません。

例題の文字列を分類すると次の通り。

mmiissiissiippii$
LLSSLLSSLLSSLLLLS

これを計算するには、文字列を逆順に走査して決めていけばいいです。基本的に s[i] と s[i+1] の文字を比べて決めていきます。

# 題材の文字列
s = "mmiissiissiippii".bytes + [0]
k = 256 # 文字種の数

# L 型か S 型か
t = [nil] * s.size

# 最後は S
t[-1] = :S

(s.size - 2).downto(0) do |i|
  # s[i] < s[i+1] なら明らかに s[i..] < s[i+1..] => i は S 型
  # s[i] > s[i+1] なら明らかに s[i..] > s[i+1..] => i は L 型
  # s[i] == s[i+1] の場合、s[i..] <=> s[i+1..] の比較結果は
  # s[i+1..] <=> s[i+2..] に準ずる (つまり t[i + 1] と同じ)
  case
  when s[i] < s[i + 1] then t[i] = :S
  when s[i] > s[i + 1] then t[i] = :L
  else                      t[i] = t[i + 1]
  end
end

LMS と LMS 部分文字列

インデックス i - 1 が L 型、i が S 型になっているとき、i を LMS(Left-most S, 最も左の S)と言います。

例題で言えば、2 番目、6 番目、10 番目、16 番目が LMS 。印をつけた位置。

          1111111
01234567890123456
mmiissiissiippii$
LLSSLLSSLLSSLLLLS
  ^   ^   ^     ^ <= LMS のインデックス

ついでに、LMS から次の LMS までの部分文字列を LMS 部分文字列と言います。これはだいぶ後で出てきます。

mmiissiissiippii$
LLSSLLSSLLSSLLLLS
  ^   ^   ^     ^
  iissi           <= LMS 部分文字列
      iissi       <= LMS 部分文字列
          iippii$ <= LMS 部分文字列
                $ <= LMS 部分文字列

ビン

まず、文字列 s と同じ長さの配列 sa を用意します。ここに suffix array の数列を入れるのが目標です。

# 作業領域
sa = [nil] * s.size

冒頭の suffix array を見ると、i で始まる suffix は 1 番目から 8 番目、m で始まる suffix は 9 番目から 10 番目、というように、suffix の先頭の文字ごとに範囲が決まっています。この範囲を、文字の「ビン」といいます。たとえば、「文字 i のビン」は 1 番目から 8 番目です。

このビンは、文字列 s の中の各文字の出現回数を数えればわかります。

# s に出現する文字種ごとのカウント
bin = [0] * (k + 1)
s.each {|ch| bin[ch + 1] += 1 }

# 文字種を累積することで bin のインデックスを決める
sum = 0
bin = bin.map {|v| sum += v }

これによって、文字 ch で始まる suffix は、sa の bin[ch] 番目から bin[ch+1] - 1 番目のどこかに入れればよい、ということになります。

それから、L 型の suffix と S 型の suffix が同じビンに入る場合(つまり先頭の文字が同じ場合)、必ず L 型の方を前に入れることになります。理由は→*1

ということで、配列 sa を次のように表現することにします。

 0 $ S : --
 1 i L : --
 2 i L : --
 3 i S : --
 4 i S : --
 5 i S : --
 6 i S : --
 7 i S : --
 8 i S : --
 9 m L : --
10 m L : --
11 p L : --
12 p L : --
13 s L : --
14 s L : --
15 s L : --
16 s L : --

左端の数字は sa 内のインデックス、その右の文字はビン、さらにその右の文字は L 型か S 型かを表してます。たとえば、i で始まる S 型の suffix は 3 番目から 8 番目のどこかに入ります。induced sort では、このルールを満たすように suffix のインデックスを入れていきます。

induced sort

いよいよ最初の目標であった induced sort の説明に入ります。induced sort は 3 つの段階からなります。

  1. LMS のインデックスをビンの終わりから詰めていく
  2. L 型のインデックスをビンの頭から詰めていく
  3. S 型のインデックスを(LMS を含めて再度)ビンの終わりから詰めていく

とりあえずアルゴリズムを説明して、それからそれがだいたい何をやっているか説明します。

induced sort: ステップ 1

ステップ 1 は、LMS のインデックスを詰めていきます。LMS の 2 、6 、10 、16 を、それぞれ i 、i 、i 、$ のビンに終わりから入れます。

 0 $ S : 16 $
 1 i L : --
 2 i L : --
 3 i S : --
 4 i S : --
 5 i S : --
 6 i S :  2 iissiissiippii$
 7 i S :  6 iissiippii$
 8 i S : 10 iippii$
 9 m L : --
10 m L : --
11 p L : --
12 p L : --
13 p L : --
14 p L : --
15 s L : --
16 s L : --

右端に、インデックスから始まる suffix を参考に書いています。

ステップ 1 を Ruby で書くとこんな感じ。

# インデックス i が LMS かどうか
def lms?(t, i)
  i > 0 && t[i - 1] == :L && t[i] == :S
end

# LMS のインデックスだけ取り出したリスト(「種」)
lmss = (0...s.size).select {|i| lms?(t, i) }

# ステップ 1: LMS のインデックスをビンの終わりの方から入れる

count = [0] * k # ビンごとにすでに挿入した数をカウントする

# 挿入する順序は適当
lmss.reverse_each do |i|
  ch = s[i]
  # ch を入れるビンの終わり (bin[ch + 1] - 1) から詰めていれる
  sa[bin[ch + 1] - 1 - count[ch]] = i
  count[ch] += 1
end

実はこのとき、LMS をどういう順序で挿入するかが、超概要で言っていた「種」です。正しい順で LMS を挿入すれば、induced sort によって正しい suffix array が計算できてしまいます。しかし、この時点で正しい順序はわからないので、適当に 2 、6 、10 と並べました。正しくは上から 10 、6 、2 の順にならないといけないので、不正解です。最終的な結果も間違ったものになります。しかし不思議なことに、その間違った結果から、正しい「種」を抽出できるのです。詳しくは後で。

induced sort: ステップ 2

ステップ 2 は、sa を正順に走査して L 型の suffix を埋めていきます。

まず、sa に入っている最初のインデックスは 16 です。その 1 つ前のインデックス 15 は L 型なので、ステップ 2 で扱う対象です。s[15] は i なので i のビンに入れます。このとき、ビンの頭に詰めます。

 0 $ S : 16 $    <===== 今見ているところ
 1 i L : 15 i$   <===== 挿入位置
 2 i L : --
 3 i S : --
 4 i S : --
 5 i S : --
 6 i S :  2 iissiissiippii$
 7 i S :  6 iissiippii$
 8 i S : 10 iippii$
 9 m L : --
10 m L : --
11 p L : --
12 p L : --
13 p L : --
14 p L : --
15 s L : --
16 s L : --

このとき、「挿入位置」は必ず「今見ているところ」より後に来ます。理由→*2

次は、入れたばかりの 15 です。14 も L 型なので入れます。これを繰り返していくと、最終的に次のようになります。

 0 $ S : 16 $
 1 i L : 15 i$
 2 i L : 14 ii$
 3 i S : --
 4 i S : --
 5 i S : --
 6 i S :  2 iissiissiippii$
 7 i S :  6 iissiippii$
 8 i S : 10 iippii$
 9 m L :  1 miissiissiippii$
10 m L :  0 mmiissiissiippii$
11 p L : 13 pii$
12 p L : 12 ppii$
13 s L :  5 siissiippii$
14 s L :  9 siippii$
15 s L :  4 ssiissiippii$
16 s L :  8 ssiippii$

ステップ 2 を Ruby で書くとこんな感じ。

# ステップ 2: sa を昇順に走査していく

count = [0] * k # ビンごとにすでに挿入した数をカウントする

sa.each do |i|
  next if i == nil
  next if i == 0
  next if t[i - 1] == :S

  # sa に入っているインデックス i について、i - 1 が L 型であるとき、
  # 文字 s[i - 1] に対応するビンに i - 1 を頭から詰めていれる
  ch = s[i - 1]
  sa[bin[ch] + count[ch]] = i - 1
  count[ch] += 1
end

induced sort: ステップ 3

ステップ 3 は、sa を逆順に捜査して S 型の suffix を埋めていきます。LMS も S 型の一種なので埋めなおします。

まず、sa の最後に入っているインデックスは 8 です。その 1 つ前のインデックス 7 は S 型なので埋めます。s[7] は i なので i のビンに入れます。このとき、ビンの終わりから詰めていれます。

 0 $ S : 16 $
 1 i L : 15 i$
 2 i L : 14 ii$
 3 i S : --
 4 i S : --
 5 i S : --
 6 i S :  2 iissiissiippii$
 7 i S :  6 iissiippii$
 8 i S :  7 issiippii$   <===== 挿入位置
 9 m L :  1 miissiissiippii$
10 m L :  0 mmiissiissiippii$
11 p L : 13 pii$
12 p L : 12 ppii$
13 s L :  5 siissiippii$
14 s L :  9 siippii$
15 s L :  4 ssiissiippii$
16 s L :  8 ssiippii$    <===== 今見ているところ

ステップ 2 と同じように、「挿入位置」は必ず「今見ているところ」より前に来ます。理由も同様です。

なお、もともと入っていた 10 を書きつぶしていることに注意。最初から入っていた LMS は("$" を除いて)いったん全部書きつぶされ、その後で再挿入されます。理由は次の節で。

これを繰り返すと、最終的に次のようになります。

 0 $ S : 16 $
 1 i L : 15 i$
 2 i L : 14 ii$
 3 i S : 10 iippii$
 4 i S :  2 iissiissiippii$
 5 i S :  6 iissiippii$
 6 i S : 11 ippii$
 7 i S :  3 issiissiippii$
 8 i S :  7 issiippii$
 9 m L :  1 miissiissiippii$
10 m L :  0 mmiissiissiippii$
11 p L : 13 pii$
12 p L : 12 ppii$
13 s L :  5 siissiippii$
14 s L :  9 siippii$
15 s L :  4 ssiissiippii$
16 s L :  8 ssiippii$

確かに LMS の 2 と 6 と 10 が(元と異なる位置に)挿入されています。

こうして得られた sa は、ほぼソートされているように見えますが、一部間違っています。例えば、s[4..] = "ssiissiippii$" が 15 番目、s[8..] = "ssiippii$" が 16 番目に来ていますが、この順序は逆ですね。これは後で直します。

Ruby はこちら。

# ステップ 3: sa を逆順に走査していく

count = [0] * k # ビンごとにすでに挿入した数をカウントする

sa.reverse_each do |i|
  next if i == nil
  next if i == 0
  next if t[i - 1] == :L

  # sa に入っているインデックス i について、i - 1 が S 型であるとき、
  # 文字 s[i - 1] に対応するビンに i - 1 を終わりから詰めていれる
  ch = s[i - 1]
  sa[bin[ch + 1] - 1 - count[ch]] = i - 1 # 上書きすることもある
  count[ch] += 1
end

induced sort の結果の考察

induced sort をしたとき、その結果はどんな性質を満たしているでしょうか。

ステップ 1 はビンソートなので、少なくとも各 suffix の最初の文字についてはちゃんとソートできています。しかし、各 suffix の 2 文字目以降はソートされていないかもしれません。次の図はステップ 1 の結果で、ソートされていないかもしれないところを ... で省略して表示しています。

 0 $ S : 16 $
 1 i L : --
 2 i L : --
 3 i S : --
 4 i S : --
 5 i S : --
 6 i S :  2 i...
 7 i S :  6 i...
 8 i S : 10 i...
 9 m L : --
10 m L : --
11 p L : --
12 p L : --
13 s L : --
14 s L : --
15 s L : --
16 s L : --

ステップ 2 では、すでに入っている suffix より 1 つ長い suffix を入れていきます。このとき、先頭 n 文字についてソートされていた suffix を 1 つ伸ばした suffix は、先頭 n + 1 文字がソートされていることが保証されます。理由→*3

これを繰り返すと、ステップ 2 完了時点で次のようになります。

 0 $ S : 16 $
 1 i L : 15 i$
 2 i L : 14 ii$
 3 i S : --
 4 i S : --
 5 i S : --
 6 i S :  2 i...
 7 i S :  6 i...
 8 i S : 10 i...
 9 m L :  1 mi...
10 m L :  0 mmi...
11 p L : 13 pii$
12 p L : 12 ppii$
13 s L :  5 si...
14 s L :  9 si...
15 s L :  4 ssi...
16 s L :  8 ssi...

ステップ 3 はステップ 2 と全く同様です。

 0 $ S : 16 $
 1 i L : 15 i$
 2 i L : 14 ii$
 3 i S : 10 iippii$
 4 i S :  2 iissi...
 5 i S :  6 iissi...
 6 i S : 11 ippii$
 7 i S :  3 issi...
 8 i S :  7 issi...
 9 m L :  1 mi...
10 m L :  0 mmi...
11 p L : 13 pii$
12 p L : 12 ppii$
13 s L :  5 si...
14 s L :  9 si...
15 s L :  4 ssi...
16 s L :  8 ssi...

ということで、... になっていない部分についてはソートされていることが保証されます。これが induced sort によって言えること。

さて、もしもステップ 1 の段階で、LMS が正しい順序(「種」)で挿入されていたとします。ステップ 2 とステップ 3 の「先頭 n 文字についてソートされている suffix を元に、1 つ長い suffix を先頭 n + 1 文字についてソートされた状態で挿入する」という性質から、正しい「種」から始めて最終的に得られる sa は、すべての suffix の全体についてソートされていることになります。それはすなわち、正しい suffix array です。

ということで、どうにか正しい「種」を得るというのが残る課題です。実はこれは、上記の「間違った induced sort の結果」を使って取り出すことができるのですが、その前に正しい「種」とは何かを考察します。

正しい「種」

正しい「種」を取り出すナイーブな実装としては、LMS の suffix を列挙して、辞書順にソートすればいいです。

LMS の suffix を列挙したもの。

 2: iissiissiippii$
 6: iissiippii$
10: iippii$
16: $

↓ソート

16: $
10: iippii$
 6: iissiippii$
 2: iissiissiippii$

この [16, 10, 6, 2] が求める「種」です。induced sort のステップ 1 で、この「種」を逆順に走査して、各ビンの最後から詰めていくと次のようになります。

 0 $ S : 16 $
 1 i L : --
 2 i L : --
 3 i S : --
 4 i S : --
 5 i S : --
 6 i S : 10 iippii$
 7 i S :  6 iissiippii$
 8 i S :  2 iissiissiippii$
 9 m L : --
10 m L : --
11 p L : --
12 p L : --
13 p L : --
14 p L : --
15 s L : --
16 s L : --

この状態でステップ 2 と 3 を実行すれば、めでたく正しい suffix array が得られます。

ただ、ナイーブな実装では O(n) が達成できません。そこで、このソートに SA-IS 法を再帰的に使うというのが、SA-IS 法の肝です。

SA-IS 法の再帰

LMS の suffix の列を SA-IS 法でソートするために、suffix の文字単位を「粗く」するのがポイントです。

"mmiissiissiippii$" を LMS 部分文字列(LMS を導入したときについでに説明してました)で分割すると、["iissi", "iissi", "iippii$", "$"] になります *4 。ここで、"iissi" や "iippii$" を 1 つの「文字」とみなし、この列を「文字列」と考えます。そして、この「文字列」のすべての suffix を並べてみます。

0: ["iissi", "iissi", "iippii$", "$"] ( 2: iissiissiippii$)
1: ["iissi", "iippii$", "$"]          ( 6: iissiippii$)
2: ["iippii$", "$"]                   (10: iippii$)
3: ["$"]                              (16: $)

最後の "(2: iissiissiippii$)" や "(16: $)" は、各 suffix が元の文字列のどの suffix に対応するかを表しています。

各「文字」の順序関係("$" < "iippii$" < "iissi")に注意しつつ、この「文字列」の suffix の列をソートします。

3: ["$"]                              (16: $)
2: ["iippii$", "$"]                   (10: iippii$)
1: ["iissi", "iippii$", "$"]          ( 6: iissiippii$)
0: ["iissi", "iissi", "iippii$", "$"] ( 2: iissiissiippii$)

注目すべきは、元文字列のインデックスの列。めでたく、[16, 10, 6, 2] という正しい「種」が得られています。

ところで、今おこなったソートは、"iissi" などを「文字」とみなした「文字列」の suffix array を作ったのと同じです。よって SA-IS 法が再帰的に適用できます。再帰すると全体で O(n) にならなくなりそうですが、「文字列」の長さは元の文字列の長さに比べて半分未満になってるので、O(n + n/2 + n/4 + ...) = O(2n) = O(n) ということで、線形時間が保たれます。すごい。

ちなみに、このアルゴリズム構成技法を再帰減速(recursive slowdown)といいます。余談ですが、これを遅延評価でやった暗黙的再帰減速(implicit recursive slowdown)という技法もあります。こういうのが面白いなと思った人は、次の本がとても面白いかもしれません。

純粋関数型データ構造
Chris Okasaki
KADOKAWA (2017-04-28)
売り上げランキング: 82,013

「文字」に番号を振る

さて、"iissi" や "iippii$" を 1 つの「文字」として扱うと言いましたが、実際にこういう部分文字列を切り出すと、比較に O(n) かかってしまうのでダメです。そこで、次のように番号を振ることを考えます。

"$"       => 0
"iippii$" => 1
"iissi"   => 2

この番号付けは、"$" < "iippii$" < "iissi" という順序関係を維持しています。そして、番号同士なら比較が O(1) でできます。よって、番号で置き換えてから SA-IS 法を再帰すれば OK です。

ただ、この番号を振る方法は自明ではありません。下手をしたら番号を振るために O(n^2) とかかかってしまいます。

この番号付けのために、1 回目の induced sort の(間違った)結果が使えます。再掲。

 0 $ S : 16 $
 1 i L : 15 i$
 2 i L : 14 ii$
 3 i S : 10 iippii$
 4 i S :  2 iissi...
 5 i S :  6 iissi...
 6 i S : 11 ippii$
 7 i S :  3 issi...
 8 i S :  7 issi...
 9 m L :  1 mi...
10 m L :  0 mmi...
11 p L : 13 pii$
12 p L : 12 ppii$
13 s L :  5 si...
14 s L :  9 si...
15 s L :  4 ssi...
16 s L :  8 ssi...

ここから、LMS の suffix だけを取り出してみます。

 0 $ S : 16 $
 3 i S : 10 iippii$
 4 i S :  2 iissi...
 5 i S :  6 iissi...

... を除くと、LMS 部分文字列ばかり出てきました。これは偶然ではありません。理由→*5

よって、この情報を活用することで、さっきのような番号付けを実現できます。隣り合う LMS 部分文字列を比べて、異なる場合は別の番号を、同じ場合は同じ番号を順に振っていけば OK です。具体的には次のような感じ。

  • "$" に番号 0 を振っておく
  • "$" と "iippii$" は異なるので、"iippii$" に番号 1 を振る
  • "iippii$" と "iissi" は異なるので、"iissi" に番号 2 を振る
  • "iissi" と "iissi" は同じなので、新しい番号は振らない

これで所望の番号付けができました。ちなみに、このときは LMS 部分文字列を直接比較しますが、文字の比較回数の合計が O(n) なので、問題ありません。

実装

実際にほしいのは、単なる番号付けではなく、["iissi", "iissi", "iippii$", "$"] の各 LMS 部分文字列を番号で置き換えたもの、つまり [2, 2, 1, 0] です。これを一気に求めます。

# induced sort の結果から LMS の suffix だけ取り出す
sa = sa.select {|i| lms?(t, i) }

# LMS のインデックス i に対して番号 nums[i] を振る
nums = []

# sa[0] の LMS は $ と決まっているので、番号 0 を振っておく
nums[sa[0]] = num = 0

# 隣り合う LMS を比べる
sa.each_cons(2) do |i, j|
  diff, d = false, 0

  # 隣り合う LMS 部分文字列の比較
  s.size.times do |d|
    if s[i + d] != s[j + d] || lms?(t, i + d) != lms?(t, j + d)
      # LMS 部分文字列の範囲で異なる文字があった
      diff = true
      break
    elsif d > 0 && (lms?(t, i + d) || lms?(t, j + d))
      # LMS 部分文字列の終端に至った
      break
    end
  end

  # 隣り合う LMS 部分文字列が異なる場合は、番号を増やす
  num += 1 if diff
  
  # LMS のインデックス j に番号 num を振る
  nums[j] = num
end

# nums の中に出現する番号のみを並べると、LMS 部分文字列を番号に置き換えた列が得られる
nums = nums.compact

sa.each_cons(2) の中で sa.size.times を使っているので、一瞬 O(n^2) の二重ループのようにも見えますが、よく考えるとループの実行回数は最大でも元の文字列全体を 1 回走査するのと同じなので、O(n) に収まります。

さて、これで得られた nums に SA-IS 法を再帰適用します。ただ、もし隣り合う LMS 部分文字列が全部異なるものだった場合、つまり番号の重複がない場合は、いちいち再帰するまでもなく、ビンソート(各ビンのサイズは 1)の考え方で suffix array を直接求めることができます。

if num + 1 < nums.size
  # 番号の重複があるので再帰
  sa = sa_is(nums, num + 1)
else
  # 番号の重複がない場合、suffix array を容易に求められる
  sa = []
  nums.each_with_index {|ch, i| sa[ch] = i }
end

そして、これで得られる sa は上記で言う [3, 2, 1, 0] に相当する数列なので、これを LMS インデックスの列 [16, 10, 6, 2] に変換します。

lmss = (0...s.size).select {|i| lms?(t, i) }
seed = sa.map {|i| lmss[i] }

そして、この「種」を元に induced sort を再度行えば、ついに完了です。長かった。

まとめ

suffix array を O(n) で作るアルゴリズム SA-IS 法を説明しました。

世にある解説が自分にはいまいちわかりにくいものしか見つからなかったので書いてみましたが、やっぱりこれも他の人にとってはわかりにくいものになっているのかもしれません。コメントをくれたら改良を考えます。

おまけ:プログラム全体

# インデックス i が LMS かどうか
def lms?(t, i)
  i > 0 && t[i - 1] == :L && t[i] == :S
end

def induced_sort(s, k, t, lmss)
  # 作業領域
  sa = [nil] * s.size

  # s に出現する文字種ごとのカウント
  bin = [0] * (k + 1)
  s.each {|ch| bin[ch + 1] += 1 }

  # 文字種を累積することで bin のインデックスを決める
  sum = 0
  bin = bin.map {|v| sum += v }
  
  # ステップ 1: LMS のインデックスをビンの終わりの方から入れる

  count = [0] * k # ビンごとにすでに挿入した数をカウントする

  lmss.reverse_each do |i|
    ch = s[i]
    # ch を入れるビンの終わり (bin[ch + 1] - 1) から詰めていれる
    sa[bin[ch + 1] - 1 - count[ch]] = i
    count[ch] += 1
  end

  # ステップ 2: sa を昇順に走査していく

  count = [0] * k # ビンごとにすでに挿入した数をカウントする

  sa.each do |i|
    next if i == nil
    next if i == 0
    next if t[i - 1] == :S

    # sa に入っているインデックス i について、i - 1 が L 型であるとき、
    # 文字 s[i - 1] に対応するビンに i - 1 を頭から詰めていれる
    ch = s[i - 1]
    sa[bin[ch] + count[ch]] = i - 1
    count[ch] += 1
  end

  # ステップ 3: sa を逆順に走査していく

  count = [0] * k # ビンごとにすでに挿入した数をカウントする

  sa.reverse_each do |i|
    next if i == nil
    next if i == 0
    next if t[i - 1] == :L

    # sa に入っているインデックス i について、i - 1 が S 型であるとき、
    # 文字 s[i - 1] に対応するビンに i - 1 を終わりから詰めていれる
    ch = s[i - 1]
    sa[bin[ch + 1] - 1 - count[ch]] = i - 1 # 上書きすることもある
    count[ch] += 1
  end

  sa
end

def sa_is(s, k)
  # L 型か S 型かのテーブル
  t = [nil] * s.size

  # 最後は S
  t[-1] = :S

  (s.size - 2).downto(0) do |i|
    # s[i] < s[i+1] なら明らかに s[i..] < s[i+1..] => i は S 型
    # s[i] > s[i+1] なら明らかに s[i..] > s[i+1..] => i は L 型
    # s[i] == s[i+1] の場合、s[i..] <=> s[i+1..] の比較結果は
    # s[i+1..] <=> s[i+2..] に準ずる (つまり t[i + 1] と同じ)
    case
    when s[i] < s[i + 1] then t[i] = :S
    when s[i] > s[i + 1] then t[i] = :L
    else                      t[i] = t[i + 1]
    end
  end

  # LMS のインデックスだけを集めた配列
  lmss = (0...s.size).select {|i| lms?(t, i) }

  # 適当な「種」:seed = lmss.shuffle でもよい
  seed = lmss

  # 1 回目の induced sort
  sa = induced_sort(s, k, t, seed)

  # induced sort の結果から LMS の suffix だけ取り出す
  sa = sa.select {|i| lms?(t, i) }

  # LMS のインデックス i に対して番号 nums[i] を振る
  nums = []

  # sa[0] の LMS は $ と決まっているので、番号 0 を振っておく
  nums[sa[0]] = num = 0

  # 隣り合う LMS を比べる
  sa.each_cons(2) do |i, j|
    diff, d = false, 0

    # 隣り合う LMS 部分文字列の比較
    s.size.times do |d|
      if s[i + d] != s[j + d] || lms?(t, i + d) != lms?(t, j + d)
        # LMS 部分文字列の範囲で異なる文字があった
        diff = true
        break
      elsif d > 0 && (lms?(t, i + d) || lms?(t, j + d))
        # LMS 部分文字列の終端に至った
        break
      end
    end

    # 隣り合う LMS 部分文字列が異なる場合は、番号を増やす
    num += 1 if diff
  
    # LMS のインデックス j に番号 num を振る
    nums[j] = num
  end

  # nums の中に出現する番号のみを並べると、LMS 部分文字列を番号に置き換えた列が得られる
  nums = nums.compact

  if num + 1 < nums.size
    # 番号の重複があるので再帰
    sa = sa_is(nums, num + 1)
  else
    # 番号の重複がない場合、suffix array を容易に求められる
    sa = []
    nums.each_with_index {|ch, i| sa[ch] = i }
  end

  # 正しい「種」
  seed = sa.map {|i| lmss[i] }

  # 2 回目の induced sort
  sa = induced_sort(s, k, t, seed)

  sa
end

# 題材の文字列
s = "mmiissiissiippii".bytes + [0]
k = 256 # 文字種の数
p sa_is(s, k)

なお、このプログラムは Ruby らしく大変富豪的に書かれています(配列作りまくり)。元論文の C プログラムは、ビンの位置以外の配列をすべて SA という 1 つの配列の再利用で解決していてかっこいいです。

出典

  • Ge Nong, Sen Zhang, and Wai Hong Chan. Two Efficient Algorithms for Linear Time Suffix Array Construction

追記(2018-02-10):コメントでの指摘を受けてバグ修正。「隣り合う LMS 部分文字列の比較」のループ終了条件が間違ってました。

*1:L 型は、1 文字目が 2 文字目より Larger です(例:"ba...")。S 型は、1 文字目が 2 文字目より Smaller です(例:"bc...")。明らかに "ba..." < "bc..." なので、確かに L 型は S 型より前に来てます。1 文字目と 2 文字目が同じになっている文字列の場合もまあ同じように証明できます。

*2:インデックス i に対して i-1 が L 型のときだけ挿入する。L 型の定義から s[i-1..] > s[i..] 。つまり s[i-1..] の挿入位置は s[i..] より後になる。

*3:インデックス 10 の suffix "i..." は先頭 1 文字についてソートされています。今回は、インデックス 10 を元に、インデックス 9 の suffix "si..." が sa[15] の位置に挿入されました。これに注目してみます。もしも "si..." を s[15] に入れることが間違いだとしたら、sa[15] は文字 s のビン内なので、"si..." より大きいか、または小さい文字列が入るべきだったことになります。もし仮に、s[15] の位置には "si..." より大きい文字列(たとえば "sz...")が入るべきだったとすると、s[13] から s[14] はすでに埋まっていることから、"si..." はいったいどこに行けばいいのかわからない、ということになるので矛盾。s[15] の位置に "si..." より小さい文字列(たとえば "sa...")が入るべきだったとすると、a の文字のビンの中に "a..." のような文字列があったはずです。そしてステップ 2 では上から順番に処理しているので、その文字列が先に処理され、sa[15] にはすでに "sa..." が入っていたはずです。しかし実際には sa[15] にそういう文字列が入っていなかったので、矛盾。ということで、インデックス 5 の suffix "si..." が入るのが正しいということになります。

*4:先頭の "mm" は LMS 部分文字列ではないので捨てます。また、LMS 部分文字列の切れ目で 1 文字重複してます。

*5:特定の suffix に注目して induced sort の動きを見てみます。ステップ 1 で sa[8] (sa の 8 番目) に入れられたインデックス 10 に注目してみましょう。ステップ 2 では、インデックス 10 を元に、L 型インデックス 9 が s[14] に入れられました。そのインデックス 9 を元に、L 型インデックス 8 が s[16] に入れられました。次のインデックス 7 は S 型なので、ステップ 2 はここで終わりです。ステップ 3 では、インデックス 8 を元に、S 型インデックス 7 が s[8] に入れられました。インデックス 7 を元に、S 型インデックス 6 が s[5] に入れられました。次のインデックス 5 は L 型なので、ステップ 3 はここで終わり。要するに、LMS から始めてその前の L 型を順次追加し、さらにその前の S 型を順次追加する、スタートの LMS からみて 1 つ前の LMS にたどり着いた時点で終了、という動きになっています。LMS から 1 つ前の LMS までの範囲(すなわち LMS 部分文字列)についてソートされる、ということになります。

『テスト駆動開発』を読んで

テスト駆動開発
テスト駆動開発
posted with amazlet at 17.10.12
Kent Beck
オーム社
売り上げランキング: 563

オーム社さまから電子書籍を贈本いただきました。ありがとうございます。

本書はテスト駆動開発(TDD)の原典で、たいへん有名な本です。が、自分は食わず嫌いで読んだことがありませんでした。

というか、TDD 自体もちゃんと理解したことがありませんでした。なんだろう、なんか怖かった。

そんな自分が今回この本をいまさら読んでみたら、なるほどこれは確かにいい本でした。なんというか、語りたくなる感じ。ということでご紹介。

紹介

テストとプログラムを交互に書いていく開発方法 TDD を、例題を用いて実演していく本です。

TDD というと「プログラムより先にテストを書く」というところだけ強調されますが、正直それではよくわからないのでした *1 。本書によると、

  1. テストを 1 つ書き足す
  2. それをパスする仮実装を追加する
  3. 仮実装をまともな実装にする

を細かく繰り返す、というものだそうです。仮実装は本当にテキトーで、テストの期待値をそのままプログラムに埋め込んだりします。「仮実装をまともな実装にする」は、本書では「テストとプログラムの間で重複を省く」と表現されています。というのも、プログラムに期待値を埋め込むことで、その期待値がテストとプログラムの間で重複するので。この重複をいい感じに省くことで、徐々にまともな実装になっていく。

テスト駆動開発」という名前ですが、テストを書くための方法ではなく、あくまで設計開発の方法ということがよくわかりました。テストはあくまで実装のガイド。テストはそえるだけ。

構成としては、第 1 部は通貨の足し算や掛け算を扱うプログラムを Java で、第 2 部はテスティングフレームワークPython で、それぞれ TDD の実演で開発してみせます。上記の繰り返しそのときどきの思考が、なんというか非常に生々しく書かれていて、ライブ感に溢れています(訳者あとがきで「まるで Kent Beck とペア・プログラミングしているかのような体験をしました」と書かれていて、まさにそんな感じ)。第 3 部は TDD にまつわる色々な話題を集めていて、まえがきによると「リファレンスとして使うのがよいだろう」だそうですが、自分の印象としてはエッセイ集みたいな感じです。ただ、第 3 部はまだ流し読みしかしていないので、あとでちゃんと読む。

本書自体も(読んだことのなかった自分には)面白い内容でしたが、この翻訳には訳者の t_wada さんによる「訳者解説:テスト駆動開発の現在」という付録がついてます。TDD の原著が出てから現在までの歴史と現状が非常にコンパクトにまとまっていて、さらに現代で TDD とどう向き合っていくべきかが書かれています。ここの良さを説明するのは困難ですが、とりあえず、自分が TDD を強迫観念みたいに感じていた理由と、別に怖くない(人もいる)というのがよくわかりました。

所感

まあ、自分が TDD を実践したくなったかと言うと、そうでもないです。TDD の真のポイントは TODO 管理だと思うんですよね。仮実装が一部仮のまま次のテストに進むことがあったり、テストとプログラムの間で頻繁にコンテキストスイッチしたりするので、TODO を忘れずにこなせる人でないと厳しそう。普段の生活でも TODO 管理が辛いのに。

とはいえ、テストをパスする状態を維持するというのは強く共感しました(というか、こういう本などが布教した考え方なんだろう)。Quine を書くときも、まずはでかくて不格好でもとにかく動く Quine にし、そのあとは動く状態を維持しつつ形や長さを改良する修正を少しずつやっていきますよね。うん。あと、こういう頻繁にテストを実行するやり方では、コンパイルに数秒もかかるような言語処理系では無理だよねー。

それから、TDD が別に万能ではないことがちゃんと書かれているのが好印象でした。設計のひらめき自体は TDD では得られない(83 ページ)とか、アサーションで使う述語自体が間違ってたら TDD は無力になる(27 ページ)とか。あと、通貨換算の責務は Expression ではなく Bank に持たせたい(83 ページ)と言いつつ、その後はせっせと Expression#reduce を実装してたり、最適化を実装しようとしたものの抽象度が壊れるから諦めて(128 ページ)、「パフォーマンスの懸念もあるが、実際の使われ方を見てから最適化を考えるので十分だと思う」(135 ページ)とか強がるあたり、ニヤニヤしながら読めた。そんな気取らない作風でした。

まとめ

TDD の考え方がよくわかる本でした。自分と同じように「なんか TDD とか聞くけどよくわかってない」という人は読んでみるといいです。t_wada さんによる付録で、原著以降の BDD などの歴史や現状、TDD の用法・用量までよくわかるおまけつき。TDD くらいすでに知ってるよ、という人も、この付録は読む価値があると思います。

*1:中にはちゃんと説明してくれている人もいたんだろうけど、自分の方がちゃんと聞く気がなかった。

RubyKaigi 2017 の予稿:An introduction and future of Ruby coverage library

RubyKaigi 2017 の 2 日目に "An introduction and future of Ruby coverage library" というタイトルで発表します。事前資料の公開が推奨されていたので、簡単ですがどんな内容かを書いておきます。

要約

カバレッジ(テストカバレッジ、コードカバレッジ)は、ソースコードのどの程度の割合がテストによって実行されたかを表す指標で、「テストの良さ」に関するヒントを与えてくれるものです。Rubyでは、品質を保証する手段がいまのところテストしかないので、カバレッジをうまく使うことは重要です。

本発表では、我々が経験や見聞を通じて得たカバレッジとの付き合い方を紹介します。簡単に言うと、カバレッジは「コードに対する網羅率」に過ぎず、「仕様や設計に対する網羅率」ではないことを理解した上で、前者だけでなく後者も上げるためのヒントとしてカバレッジを活用することです。(なお、カバレッジの定義から丁寧に説明するので、前提知識は必要ありません。)

また、Rubyカバレッジ測定の現状として、SimpleCov や coverage.so などを紹介した上で、2.5 に向けての coverage.so の拡張計画を説明します。具体的には、関数カバレッジと分岐カバレッジの測定を可能にする予定で、そのために現在検討中の coverage.so の仕様を紹介します。他言語のカバレッジ管理ツールとの連携のために必要な情報を提供するように設計していて、実際に LCOV や Cobertura で可視化できることをデモする予定です。

解説

Ruby3x3 で導入される(とされている)型システムに注目が集まっていますが、プログラムの堅牢性を高める手段は、なにも型だけではありません。コードレビューだって立派な品質保証手段ですし、普通の型よりも強力に検証する手段もあります。しかし、現時点で一番普及している手段は明らかに、テストです。そこでテストの度合いのヒントとなるカバレッジは重要なはずですが、テストに比べると普及度が低いように感じています。
カバレッジが十分に活用されていない理由はいろいろ考えられますが、「カバレッジの(うまい)活用方法があまり知られていない」「Rubyカバレッジ測定ライブラリが貧弱」という 2 つの問題を緩和するために、今回発表してみることにしました。

2008 年にコミッタになって最初にやった仕事が、Ruby 本体のテストカバレッジ向上でした。主にそのころに得た知見と反省などを元に、漠然と考えていた「カバレッジのうまい活用方法」を明文化してみたので、共有したいと思います。

そのころに作った Ruby カバレッジ測定ライブラリ coverage.so は、便利なラッパ SimpleCov が登場したおかげでそこそこ使われているようです。しかし、当時 coverage.so の仕様を適当に決めてしまったために、行カバレッジ以外をサポートしづらく、放置していました(すみません)。今回一念発起し、関数カバレッジと分岐カバレッジをサポートする方向で仕様の検討と実装の試作をし、昨日コミットしました。まだ細かいところが詰めきれていないのですが、2.5 のリリースまでには決めたいと思っています。

なお、超絶技巧みたいな話は今回はありません。ない予定です。ないんじゃないかな。期待しないでください。

宣伝

拙著『Ruby でつくる Ruby』が、ジュンク堂書店の RubyKaigi 出張店で販売される予定です。サイン会もあるみたいなので、ぜひお買上げを!なお、RubyKaigi 出張店は RubyKaigi 1 日め(9/18(月・祝))だけなので要注意です。サイン会も 1 日めですが、本さえあればいつでもサインするので適当に捕まえてください。

さらに、ラムダノートがRubyKaigi 2017 便乗、サイン入り書籍プレゼント企画!(connpass)をしています。RubyKaigi に参加できない方はこっちに応募してみてください!

『Ruby でつくる Ruby』の購入方法

書籍『Ruby でつくる Ruby』が発売されて 2 月ほど経ちました(当時の告知)。いろいろなところでわりと好意的な感想が聞こえてくるので、うれしい限りです。
本書はラムダノートにとっての最初の出版ということで、発売当初は直販サイトでしか入手できませんでした。しかし最近は、販路開拓が進んだようで購入手段が増えてきました。まだ買ってない人のために、簡単にご紹介。

ラムダノート直販サイト

『Ruby でつくる Ruby』表紙
ref: https://www.lambdanote.com/products/ruby-ruby
直販サイトです。よく知らないサイトで物を買うのは不安かもしれませんが、実体は Shopify という有名なオンラインストア用サービスなので、そんなに怖くないです。
購入方法にこだわりが無いなら、ここで買うことをオススメします。なぜなら、出版社と著者への利益が最大なので。

Amazon.co.jp

RubyでつくるRuby ゼロから学びなおすプログラミング言語入門
遠藤侑介
ラムダノート
売り上げランキング: 63,507
言わずと知れたアマゾンです。最近詐欺で有名になったマーケットプレイスですが、ラムダノート直営なので心配ありません。中古販売などではなく、ちゃんと新品が届きます。
誰かレビュー書いてください。

ジュンク堂書店池袋本店

東京・池袋にあるジュンク堂書店です。6 階に、ふつうに現物が置いてあります。サンプルもあるので中も見えます。
今のところ、現物をその場で買って帰れる唯一の手段です。「ネット通販はイヤでござる」という人はぜひこちらで。

honto

ref: https://honto.jp/netstore/pd-book_28490380.html
honto カードの honto です。自分は honto で買ったことはないのですが、とっても大手なのでまあ大丈夫でしょう。
店舗在庫状況を見ると、「在庫あり」になっているのはジュンク堂書店池袋本店だけ。ここに載ってる他のお店でも取り寄せられるのでしょうか?誰か挑戦して報告くれたらうれしいです。

ラムダノート出張販売

ラムダノートが行商するイベントに行って買う。
近いうちに行商するという予定は聞いていないので、期待しない方がいいです。「ラムダノート社長の鹿野さんに会いたい!」という動機でもない限り、おすすめしません。

おまけ

『プロフェッショナル SSL/TLS』のほうも同じ手段で買えるはず。アマゾンのアフィリエイトリンクを置いておきます。

プロフェッショナルSSL/TLS
Ivan Ristić 齋藤孝
ラムダノート
売り上げランキング: 133,698

"Purely Functional Data Structures" の邦訳『純粋関数型データ構造』が発売されます

純粋関数型データ構造
Chris Okasaki
KADOKAWA (2017-04-28)
売り上げランキング: 266

伝説の名著、"Purely Functional Data Structures"(通常 PFDS)を翻訳しました。4 月末にアスキードワンゴから発売されます。

20分でわかる Purely Functional Data Structures』などを通じて日本に PFDS を布教した @kinaba との共訳です。ちなみに編集さんはアスキードワンゴ編集長の鈴木嘉平さん。

関数型プログラマ向けの紹介

「リストの結合が O(n) で遅いな」とか「まともなキューはどうやって作るの」とかいった問題に一度は直面したことがありますよね。純粋関数型プログラミングではどうしても無理、と思って諦めている人もいるのではないでしょうか。

この本の技法を使えば、「結合が O(1) のリスト」や「挿入・削除が O(1) のキュー」など、嘘みたいな計算量を達成できてしまいます。もちろん、破壊的更新を使わずに。ああ、15 年前の自分に教えてあげたい*1

結合が O(1) のリストがほしいだけであれば、既存実装の Data.Sequence あたりを使っとけば良いです。しかし、こういう出来合いのソリューションでは困るケースもあります。たとえば Data.Sequence は償却計算量なので、リアルタイム性が重要だと困るとか。

この本は単なる「結合が O(1) の列の実装方法」ではなく、「効率的な純粋関数型データ構造を実装するための汎用技法」を教えてくれます。効率的な列やキューの実装は、その技法の適用デモとして説明されます。なので、特定のケースで必要なデータ構造もいざとなれば自作できるようになります。

ということで、関数型プログラマを目指すならこの本の内容は確実に抑えておくべきでしょう。プロの方はすでに抑えてると思いますが、ハンドブックとしてお手元に是非。

Rubyist 向けの紹介

言うなれば、「すべてのオブジェクトが freeze した世界で、どこまで効率的なデータ構造を作れるか」を追究した本です。

破壊的更新が使えないのでデータ構造を実装するのが難しくなることが多いですが、逆に簡単になることもあります。たとえば、文字列の結合。普通は、メモリコピーが発生するので遅いですよね。しかしすべての文字列が freeze されていれば、コピーの必要はありません。2 つの文字列を並べた配列として表現すればいいです(こういう実装を Rope と言います)。こういう感じの考え方をひたすら深めていったような本です。

本書自体は Standard ML とかいう馴染みの薄そうな言語で説明されていて、Rubyist に全力でおすすめできる本ではないのですが、ふつうの Ruby から一歩踏み出したい方は ML 系の教科書と合わせて読んでみていただけると嬉しいです。

(『Ruby でつくる Ruby』を読んで「木って、すごく面白いな!」と思ったような人は素質あるかも)

裏話

Ruby でつくる Ruby』とほぼ同時公表となったこの本ですが、どちらもラムダノート鹿野桂一郎さんが絡んでたりします。

鹿野さんには TAPL の終わりごろから PFDS の翻訳企画を持ちかけていました。鹿野さんがラムダノートの社長と化したタイミングで再度持ちかけたら、アスキードワンゴの鈴木嘉平さんが企画しているとわかったので、繋いでくれて今回の告知に至る。*2

一方、ラムダノートでやる企画なくなったなーと思っていたら、鹿野さんから「ちょっと変わった Ruby 入門ってどうだろう」と持ちかけられ、『Ruby でつくる Ruby』につながっていきました(非公式あとがき参照)。

ということで

Ruby でつくる Ruby』と合わせて、みなさん是非読んでみてください!

*1:原著はそれより前(1998 年)に出版されているわけですが。

*2:さらっと言いましたが PFDS の翻訳も容易ではありませんでした。遅延評価の force とか suspension とか意外に定訳がない単語が多かったり、アホな誤訳をちょくちょくかましていたり。共訳の @kinaba 殿やレビュアーの方々に救われて出版にこぎつけました。感謝の限り。

書籍『Ruby でつくる Ruby』が発売されます

『Ruby でつくる Ruby』表紙
ref: https://www.lambdanote.com/collections/frontpage/products/ruby-ruby

おかげさまで、ASCII.jp で連載していた『Ruby で学ぶ Ruby』が紙の本になる運びとなりました。わーい。『Ruby でつくる Ruby ― ゼロから学びなおすプログラミング言語入門』と微妙にタイトルが変わったので注意。

一番大切なことを先に言っておくと、書店に並ぶ予定はありません。ラムダノート株式会社という出版社の直販サイトで購入できます。アスキーじゃないの?と聞かないでください。追記:今はいろいろ購入方法が増えました。『Ruby でつくる Ruby』の購入方法をご参照ください。

ラムダノートは『型システム入門』のときにもお世話になった編集の鹿野さんが立ち上げた新しい出版社です。鹿野さんと高尾さんの編集コンビは、技術書については知る限り最強なので、何か書きたいネタがある人はラムダノートに持ち込むといいですよ。

どんな本?

一言で言えば、RubyRuby インタプリタをつくる本です。Ruby インタプリタとは、Ruby プログラムを動かすためのプログラムのことです。この開発を通して、Ruby という言語を外側と内側の両面から学んでいけます。

環境のセットアップから説明していくので、Ruby について何も知らなくても、いっそプログラミングをしたことがない人でも読めるように書いたつもりです。そこからはじめて、たった 144 ページで、Ruby インタプリタを「自作」していきます。もちろん完全な Ruby インタプリタではなく、いろいろ端折った簡略版の Ruby(MinRuby と呼んでいます)のインタプリタですが、いちおう「ブートストラップ」ができます。つまり、自分で書いたインタプリタで、自分で書いたそのインタプリタ自身を動かせるということです。ここまでできれば胸を張って「インタプリタを自作した」と言えるレベル。本の内容については、連載開始時の記事連載終了時の記事もご参照ください。

あと、記念すべきラムダノートの初出版本 2 冊のうちの 1 冊ということで、なんか採算度外視の豪華な本になっています。なんと言っても、本文が全ページフルカラーです。絵本なのかな。hirekoke さんのかわいい(そしてエキセントリックな)挿絵が全部載っています。(なお、もう 1 冊の初出版は、"Bulletproof SSL and TLS" の翻訳本「プロフェッショナル SSL/TLS」です)

連載時からの改善点

ところで、自分にとって初の連載記事であった『Ruby で学ぶ Ruby』は、連載の恐怖を知る機会にもなりました。それは、「締め切り」、ではなくて、「公開しちゃった部分を変えられない」ということです。もちろん Web 連載なので typo なんかは直せるんですが、「これ、あのときに合わせて説明しておけばよかったな」とか「この用語、ちゃんと説明しないまま使ってしまっているなあ」などというレベルのミスは最新版で中途半端に補足するしかなくて、文章がツギハギだらけになっていく。今回、鹿野さんと高尾さんの全面バックアップの下、その手のツギハギを一掃しました。おかげで、かなり読みやすくなったと思います。

付録として自力構文解析も載せたいと思っていたのですが、こっちは残念ながら紙面の都合で載せられませんでした。ざっと見積もって、144 ページが 200 ページ超になりそうだった。一応、興味のある人向けに、further reading というか調べると良さそうなキーワードを足しておきました。この本が売れれば続編を書く機会が得られるのではないかと思うので、なにとぞ、なにとぞ!

注意点

  • 書店に並ぶ予定はありません(再掲)。ラムダノートの直販サイトから購入してください。追記:今は『Ruby でつくる Ruby』の購入方法をご参照ください。
  • 直販サイトがイヤな人は、ラムダノートが行商をするタイミングを狙いましょう。とりあえず、4/9(日)に秋葉原で行われる技術書典 2 のラムダノートのブースでは販売される見込みです。
  • 電子書籍はありません(予定もありません)。理由は大人の事情によるらしい(ぼくもよくわかってない)。ちょっともどかしいのですが、この本が売れれば電子書籍になる可能性もあるみたいです。