tr1とboost/tr1でis_permutationがつらい話

なんとも重箱の隅をつつくような話だ。

私が開発に参加しているプロジェクトがあり、そこでは(古くから開発されているので)C++98が使われている。だが先進的なものも積極的に使おうとするプロジェクトでもあるので、Boostやtr1を使ってもいた。CMakeでtr1があるかどうかを確認し、存在していたらそちらをusingもしくはtypedefするという方針である。例えば、

#ifdef HAVE_CXX11_ARRAY
#include <array>
namespace hoge
{
template<typename T, std::size_t N>
struct array_getter{typedef std::array<T, N> type;};
}
#else
#include <boost/array.hpp>
namespace hoge
{
template<typename T, std::size_t N>
struct array_getter{typedef boost::array<T, N> type;};
}
#endif

のような感じだ。C++98ではテンプレートエイリアスが存在しないため、構造体を使っている。まあ、この例ではstd::arrayboost::arrayは名前が同じなのでusing std::array;でもいいと思う。

ところで、ある日私はテストを書いており、std::is_permutationが使いたくなった。そのテストでは配列持っている値は保証できるが順番は保証できないたぐいのものだったので、正解配列のpermutationであればOKだろうという判断である。今思えばstd::setに内容をコピーして正解集合と比較すればよかったのだが、その時はstd::setの存在を完全に忘れていた。

で、最初にお話しした通りそのプロジェクトではC++98が使われている。よってC++11以降で追加されたstd::is_permutationは使えない。だがC++erの強い味方Boostは使っていいことになっているので、Boost.Algorithmboost::algorithm::is_permutationが使える。

はずだった。

手元で動くことを確認した後、Travisからfailedメールが来た。調べてみると、異なる2つのstd::tr1::tupleが定義されているようだ。しかし、何故だ?

主な変更箇所はis_permutationだったので、少し覗いてみることにした。TravisではBoost 1.54が使われていたので(!)、boost.orgからダウンロードし、boost/algorithm/cxx11/is_permutation.hppを開く。

#include <boost/tr1/tr1/tuple> // for tie

とある。どうやらstd::pairに対してtieを使いたかったようだ。そのためにboost/tr1/tr1/tupleをインクルードしている。しかし、ご存知の通りboost/tr1の実装とGCCtr1実装は異なるものである。さらに、boost/tr1の内容はstd::tr1名前空間に定義される。前述した通りそのプロジェクトはtr1の機能も使っている。よって衝突が起きる。この問題の行は1.56.0時点でなくなっている。

仕方がない、と思い、その時std::setのことを思い出せば良かったのだが、何故か「is_permutationくらい自分で実装するか」と思ってしまった。本来は値の同一判定をする関数オブジェクトなどを取れるようにしてかなりジェネリックに書かないといけないのだが、今回は単にテストに使うためだけのものなので、最も簡単なものでよかろうと思い、それらの機能は無視することにした。

そして実装した。テストコードは単一の.cppファイルなので、別段名前空間を分ける必要もなかろうと思い、以下のような関数を定義した。

template<typename Iterator1, typename Iterator2>
bool is_permutation(const Iterator1 first1, const Iterator1 last1,
                    const Iterator2 first2, const Iterator2 last2);

ところが今度は手元でコンパイルが通らなかった。オーバーロード解決がambiguousだというのだ。明らかに解決できるのは私が定義した関数だけだろう、と思ったのだが、数秒経って気付いた。手元で使っているGCCは7.3で、-std=c++98などを付けなければ自動的にC++14が選択される。そして-std=c++98は使っていなかった。なのでこれと同じ形をした関数定義がstd名前空間内に存在する。

さらに、C++にはArgument Dependent Lookup (ADL)がある。これは関数の引数が何かの名前空間(例えばstd)で定義されているものなら、関数の候補をその名前空間(例えばstd)から"も"探すというものだ。この利便性と危険性に関しては星の数ほど記事があるので適宜googleしていただきたい。

で、何が起きたかというと、このis_permutationに渡していたIteratorクラスは、どちらもstd::vector<T>::iteratorだった。これは当たり前だがstd名前空間で定義されている。なので、std名前空間にある関数が全てオーバーロード解決の候補になる。GCC7.3ではデフォルトC++14なので、引数がマッチするstd::is_permutationが存在する。ところで私が定義したis_permutation関数もマッチする。よってambiguousになる。

気付くかこんなもん!!!

解決策は簡単で、自作is_permutation関数を適当な名前空間で囲み、使うときに名前空間を指定して呼べばよい。

namespace hoge
{
template<typename Iterator1, typename Iterator2>
bool is_permutation(const Iterator1 first1, const Iterator1 last1,
                    const Iterator2 first2, const Iterator2 last2);
}

hoge::is_permutation(v1.begin(), v1.end(), v2.begin(), v2.end());

あるいは、もっと単純な解決策がある。is_permutationという名前にしないことだ。そうすれば標準ライブラリと名前が被って困ることはない。


ところで、何も関係ない話だが、この前留学生に「重箱の隅をつつくってどういう意味?」と尋ねられ、「重要でない小さなことばかり議論することだ」と答えた。重箱のことは知っていたようなので、感覚もわかってもらえたようだ。すると次に、「では、豆腐の角に頭をぶつけるとは?」と聞かれ、一体どこで知ったのか非常に気になったのだが、「それは罵倒語で、馬鹿にしつつgo to hellのようなことを言っているのだ」と答えた。彼がどこでこれらの言葉を知ったのかというと、どうやら「角」と「隅」の違いについて調べていたらしい。両方vertex周辺の領域を指すが、概ね、角は出っ張っているところを言い、隅は凹んでいるところを言う、ということについて調べていた時にわからないセンテンスが出てきたので聞いてみたらしい。

最後に、「でも、”豆腐の角に頭をぶつけて死んでしまえ”は冗談だと思われるかも知れないから、罵倒語としてはあまり有用ではない。もっと端的に表現するべきだ」とも伝えておいた。これで、日本人と喧嘩になった時も、彼の怒りが正しく伝わるだろう。

一番簡単な画像フォーマット

世の中には画像フォーマットが沢山ある。データを圧縮してサイズが小さくなるフォーマットや、様々なメタデータを持てるフォーマットなど色々だが、自分で作るとなると何が一番楽だろうか。 ある意味ではsvgだろうが、今回はビットマップということにする。svgでrectを置きまくる? いやそういう趣旨ではない。

なんだろうかとは言ったが、これには確実な答えがある。PNMだ。

PNM (画像フォーマット) - Wikipedia

このフォーマットは冗談抜きに中身をエディタで表示しても画像が見える。上記リンクのWikipediaから引用すると、一番単純な白黒画像(PBM)は以下のようになる。

P1
# This is an example bitmap of the letter "J"
6 10
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
1 0 0 0 1 0
0 1 1 1 0 0
0 0 0 0 0 0
0 0 0 0 0 0

カラーのPPM形式は以下だ。 アスキー形式だとコメントを入れられるところも人間が扱うことを考慮している雰囲気がする。

P3
# The P3 means colors are in ASCII, then 3 columns and 2 rows, then 255 for max color, then RGB triplets
3 2
255
255 0 0
0 255 0
0 0 255
255 255 0
255 255 255
0 0 0

というわけでこの説明を書き始めたのだが、たらたら思いつくままに書いているとわかりにくい文章になったので系統立てて書くことにする。 まず、PNMフォーマットというのは総称で、実際には大きく分けて3つの形式があり、それぞれが二値画像、グレースケール、RGBカラーに対応している。 さらに、それらのそれぞれがアスキーとバイナリの2つのフォーマットで保存することができる。持っている情報は同じだ。

二値画像、PBM

最初に紹介した形式だ。ファイルの最初はP1\nで始まる必要がある。その後画像のサイズが横 縦の順で書かれ、その後0または1が続く。 注意すべきことは、ビットが立っているところが黒で、それ以外が白ということだ。通常255が一番明るい色なのでこれは少し混乱を生むかも知れない。

P1
# This is an example bitmap of the letter "J"
6 10
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
1 0 0 0 1 0
0 1 1 1 0 0
0 0 0 0 0 0
0 0 0 0 0 0

ところで、これは全てのPNMアスキー形式に共通することだが、改行はフォーマット識別子以外のどこで行われても許される。これはWikipediaを読んでも書いていなかったので適当なPNM画像を作って改行を入れながらビューワで見ることにした。大抵どこに入れても見ることができた。規格を参照していないのでそうあるべきかはわからないが、そうなっている方が便利なのは間違いない。このせいでファイル読み込み関数が少し面倒になってしまうのだが。

PBMのバイナリ形式はこれもまた厄介で、ビットごとにピクセルが対応している。だが、画像の一列ごとにパディングが入り、LSBは放置される。要するに上の画像をバイナリにすると以下のようになるだろうということだ。

50 31 0a 36 20 31 30 0a 08 08 08 08 08 08 88 70 00 00
P  1  \n  6     1  0 \n 以下参照
0 0 0 0 1 0 0 0 -> 0x08
0 0 0 0 1 0 0 0 -> 0x08
0 0 0 0 1 0 0 0 -> 0x08
0 0 0 0 1 0 0 0 -> 0x08
0 0 0 0 1 0 0 0 -> 0x08
0 0 0 0 1 0 0 0 -> 0x08
1 0 0 0 1 0 0 0 -> 0x88
0 1 1 1 0 0 0 0 -> 0x70
0 0 0 0 0 0 0 0 -> 0x00
0 0 0 0 0 0 0 0 -> 0x00

それぞれの行の後ろに、8の倍数になるまで関係のないビットが追加される。一行の終わりでそのような処理がされるが、今回は行ごとのピクセルが6つしかないので、全てのバイトがパディング入りになる。

ところでご覧の通り、バイナリ形式であっても画像のサイズはASCIIで保存されている。これはおそらく、整数値をバイナリで保存するときにそのサイズや符号の有無を定義するのが難しいとかそういう理由だろう。ちなみにコメントもバイナリ形式に含めることができて(今知った)、バイナリであっても#に相当するASCIIコードから\nまでは飛ばされるらしい。

グレイスケール、PGM

グレイスケールも基本は同じだ。ただ、最大値を記しておける点が異なっている。この最大値と同じ値のとき、そのピクセルは白くなる。

Wikipediaから例を引用すると、

P2
# Shows the word "FEEP" (example from Netpbm man page on PGM)
24 7
15
0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
0  3  3  3  3  0  0  7  7  7  7  0  0 11 11 11 11  0  0 15 15 15 15  0
0  3  0  0  0  0  0  7  0  0  0  0  0 11  0  0  0  0  0 15  0  0 15  0
0  3  3  3  0  0  0  7  7  7  0  0  0 11 11 11  0  0  0 15 15 15 15  0
0  3  0  0  0  0  0  7  0  0  0  0  0 11  0  0  0  0  0 15  0  0  0  0
0  3  0  0  0  0  0  7  7  7  7  0  0 11 11 11 11  0  0 15  0  0  0  0
0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0

まあ、これだけだ。今回はバイナリの時にパディングが入ることはなく、書くピクセルが1バイトで保存される。要するに255階調しか色調は存在しないのだ。一応、2バイトへの拡張は存在するが、エンディアン関係の問題があって微妙な扱いになっているように書かれている。

カラー、PPM

同じだ。Wikipediaの例ではピクセルごとに改行されているが、そうしないといけないというわけではない。

P3
# The P3 means colors are in ASCII, then 3 columns and 2 rows, then 255 for max color, then RGB triplets
3 2
255
255 0 0
0 255 0
0 0 255
255 255 0
255 255 255
0 0 0

pnm++

というわけでこれは簡単なので自分で画像を作るときはいっちょ使ってみるかと思ったのだがC++用ライブラリがさほど充実していないように見える。ちゃんと検索していないだけかもしれない。恐らくみんなその場でざーっと書いてしまっているのだろう。それでいいと思う。ただ、私もそれを一応したので、ここに設計含めて説明を載せておこうとも思う。

まず、こんなに単純なフォーマットなのだから、コードはそんなに長くならないだろうと思った。1000行行かないのではないかと(これは後で盛大に超えてしまったので正確な推測ではなかった)。なので単一ファイルのヘッダーオンリーライブラリにすることにした。これならインストールなどの問題は限りなくなくなる。コピーして置いてインクルードするだけだ。こんなに簡単なことはない。

次に、C++なのだから、速度は最低限は出て欲しい。なので、データの局所性のために画像データは1次元配列で持つことにした。それをラップして二次元配列のように振る舞わせる。at(size_t, size_t)は導入するとして、operator[]が一行分のrangeをラップしたプロキシクラスを返すことにする。

そして、それぞれの画像フォーマットから出てくるデータは、pixelクラスをtemplateとして受け取るクラスにする。writeはそれでオーバーロードして、readpixel型をtemplateとして受け取ることにしよう。

ということで作った。ライセンスはMIT、要求はC++11、STL以外の依存はなし。適当にマンデルブロ集合などを描いて遊んでいる。

GitHub - ToruNiina/pnm: pnm format(pbm, pgm, ppm) IO for modern C++ (single header only library)

shellのfor文について

最近技術系のことをしっかりまとめる時間が減ってきた。とりあえず小粒なものを書いておこうと思う。

2ヶ月前くらいに、AからJまで10個のアルファベットを順に取り出したいことがあった。つまりこういうことがしたい。

grep 'A' something.dat | some_command | ...
grep 'B' something.dat | some_command | ...
grep 'C' something.dat | some_command | ...
# ...
grep 'J' something.dat | some_command | ...

これを行コピーして書き換える者は「怠惰」ではない。 一応、bashには配列があるが、文法が微妙に複雑で、使おうと思った時は大抵忘れてしまっている。結局毎回「bash 配列」などで検索する羽目になる。

declare -a alphabets=("a" "b" "c")

これはわかりにくくないか?

ところで、bashforは意外と柔軟だ。

for a in A B C D E F G H I J; do
    grep ${a} something.dat | some_command | ...
done

これが動く。

まあA B C D E F G H I Jを打つのは面倒なことに変わりはない。個人的には、普段ループに使っているseqで動けば一番いいのだが、と思っていた。 例えばこれは以下のようにすれば何とかなるだろう……と思ったが動かなかった。

for i in `seq 65 74`; do
    grep $(printf "%c" ${i}) something.dat | some_command | ...
done

どうやらこうすると最初の1文字目が出力されるだけでASCIIコードに対応する文字が出るわけではないようだ。 他にはhexにしてから0x4cなどのようにして変換させるという方法があるが、どんどん可読性が落ちていってしまう。

そして最近、以下が動くことを知った。

for a in {A..J}; do
    grep ${a} something.dat | some_command | ...
done

最初の苦労は何だったんだ?

その上、以下のようなことが起きる。

$ for a in {A..C}{1..3}; do
    echo ${a}
done
A1
A2
A3
B1
B2
B3
C1
C2
C3

今までの苦労を返して欲しい。

ところでfishでこれをする方法を探したが、どうもなさそうに見える。かなしい。

Better document differences from bash · Issue #2382 · fish-shell/fish-shell · GitHub

自作プロダクトにIssueが立ったのでManjaroを触った話

背景

私が作った個人プロダクトの中で、特に宣伝していない割にはそれなりに(見ず知らずの人に)スターを貰って使ってもらえているものがあるのだが、そこにIssueが立った。

internal compiler error with gcc-7.3.1 · Issue #9 · ToruNiina/toml11 · GitHub

ちなみにこれはTOMLという設定ファイルのパーサである。

このIssueは、Arch系でリリースされているgcc-7.3.1だけで(本家では7.3.0までしかリリースされていない)生じるInternal Compiler Errorに関するものだ。それまでは出ていなかったICEが非公式リリース7.3.1で突然発生しているのだからコンパイラのバグかと思ったのだが、微妙な処理系依存動作に乗っかっていたのかもしれない。どちらにせよworkaroundできた方がいい。

それで報告されたエラーメッセージを見た所、おそらくSFINAE関係だろうと推測できた。他のライブラリが問題なくコンパイルできているのだとしたら、その箇所でやっていた妙なことといえば、constexpr関数をtemplateのデフォルト引数に使っていたことだ。

constexpr int func() {}

template<typename T, int I = func()>
class X{};

というような感じだ。 おそらくここでおかしくなっているのだろうと何の根拠もなく直感した。

そしてboostなどがコンパイルできるのなら、structを使った普通のメタ関数は普通にコンパイルできるのだろう。そう思ってconstexpr関数を使っているところを無くした。リファクタリングも兼ねてメタ関数を整理もした。とりあえずTravis.CIとappvayorのテストは通った。

そして、予想があっているかどうかを確認するため、VMを立てた。

本題: Manjaro on VirtualBox

Arch系で簡単に使うとなると、Archそのものは厳しいので、Manjaroだろう。使ったことはないが記事で見たことがある。そしてIssueの報告者もManjaro使いだったのでこれ以外の選択肢はないだろう。

というわけでVMを立ち上げてManjaroをインストールした。gccのバージョンを確認する。

[manjaro@manjaro ~]$ g++ --version
g++ (GCC) 7.3.1 20180312

よし、ということで件のプロダクトをcloneしてcmake……と思ったが、cmakeがない。確かArchのパッケージマネージャはpacmanとか言ったな、と思いググってインストールしようとしてみる。

[manjaro@manjaro ~]$ pacman -S cmake

と思ったが何やらエラーが出ている。「database file for "core" does not exist」というようなエラーだ。 よくわからなかったまま十分ほどgoogleし、どうやらManjaroのインストール後は、最初にpacmanのデータベースファイルを更新しなければならないらしいとわかった。

$ pacman -Syy

を実行すると、どうやらどこかのサーバからパッケージのリストのようなものがダウンロードされている感じがして、インストールできるようになった。

こういうので時間を食ってしまうのは仕方がないが少しつらい。

結果

で、Issueはどうなったのかというと、直っていた。workaround成功だ。というわけでArch使いの方も安心して我がプロダクトを使っていただきたい。

GitHub - ToruNiina/toml11: TOML for Modern C++

プログラミングパラダイムとは制約をかけることだという見方

ふと感じたのだが、プログラミングパラダイムというのは、プログラムに制約をかけることなのではないか。

基本的に、現代で書かれるプログラムはどれも非常に長く複雑で、人間の脳みそに収まりきらない。実行パスをトレースできない規模のものはザラにあるし、長さがそもそも覚えられないものもある。

このようなプログラムを頭から完全に理解するのは不可能だ。大規模なプログラムを頭から完全に理解するというのは単に文字列として覚えるという意味ではなく(そもそもそれが不可能だと思うが)、様々な入力や内部状態に応じてどの実行パスを通りどの処理が実行されて、それに伴ってどの程度のメモリが消費され、どのスレッドがどの領域のMutexを取得して……。プログラムを文章として読むだけではなく、頭の中でその挙動をエミュレーションできる必要があり、さらに爆発的に増えていく状態や入力の組み合わせの全てに対して実行してみる必要があるのだ(頭の中で)。そもそもそのようなことをしようとすることが時間の無駄のように思える。だいたい、活発に開発されているプログラムは同時並行で異なる部分が編集されていったりする。すると知ったそばから内容が変わっていたりする。こうなると理解するのはもう不可能だ。

なのにどうやってプログラムは開発されているのだろう。理解できていないものを適当に変更したらたいていの場合壊れるのだが、壊れずに動いているプログラムが多いのは何故だろう。それは、主だったプログラミングパラダイムや積み上げられてきた知見がどれも、組み合わせ爆発を抑えていくようなものになっているからだ、とふと思ったのが今回のこの記事だ。

オブジェクト指向的な考え方が何かというと、大雑把には、単一の仕事に対して責任を持つクラスを作り、それに仕事をさせるためのインターフェースを設計し、そのクラスはその仕事を完璧にこなし、外に内輪の情報は何も漏らさないというものだろう。これは、単一の仕事についてそれをこなすために必要な処理を全てブラックボックスに閉じ込めることで、プログラム全体の状態数を減らすという戦略だと言えなくもない。

関数型はもっとわかりやすく、状態が作られることがないので、その部分の複雑さは一切ない。その瞬間にその関数に渡ってきた入力が全てなので、見るべきものが非常に小さくなっていることになる。

このような観点から見ると上の2つは同じことだ。単一の仕事について、その処理をまとめ、それに付随する内部状態が全体に波及しないようにする。簡単な話じゃあないか。

特定の部分について考えているときに他の部分については考えずに済むなら、頭から爪先まで理解している必要はない。読んでいる部分に集中すればよい。

そう考えてみると、goto-lessプログラミングもそのような見方で捉えられなくもない気がする。というのも、gotoがあるプログラムは今読んでいる行に突然全く別の内部状態で処理が遷移してくるかもしれないと怯えながら読まなければならないので、これはつまりプログラムの全状態の組み合わせを全ての行で考慮しなければならないことを意味する(これは若干誇張しているが)。なので、この強すぎる武器gotoを使わないようにする、または使う場面を非常に強く制限することで、考慮すべき状態数を減らしたのがgoto-lessプログラミングだと言えるだろう。

これらは人間の小さな脳みそが組み合わせ爆発に対抗する手段だ。そういう発想があれば、どのパラダイムも納得がいくようになるのではないか。

古い標準ライブラリ実装を新しいコンパイラで使って困った話

背景

C++11ではconstexprメンバ関数は暗黙にconst指定される。

template<typename T>
class X
{
  public:
    constexpr T get_member() /* const */ {return member_;}
  private:
    T member_;
};

この仕様はある程度面倒を引き起こしていた(参考:http://boleros.hateblo.jp/entry/20130604/1370364968 )。

しかし、C++14ではめでたくこの仕様はなくなった。

事例

さて、絶妙に古いコンパイラが入っているマシンでC++14コードをコンパイルしようとしたのだ。バージョン番号は忘れてしまったが、GCCではC++14対応がなかったものの、clangは対応していたので、clangで普通にコンパイルすれば通るだろうと思った。実際、簡単なコードは通っていた。

しかし、別のコードをコンパイルしようとすると驚くべきことにstd::complex::real()が呼べないという問題が発生する。Clang曰く、「const std::complex& に対して const 指定されていないメンバを呼び出した」という。std::complex::real()はそもそもconstだ。非const版は引数を一つ取る。

これは何かがおかしいと思い標準の実装を見てみた。すると以下のようになっていた。

# if //C++11以上かどうか確認のマクロ
constexpr T real() {return _M_real;}
# else
...

const指定がない。この標準ライブラリ実装はC++14以前のもので、つまりconstexprなので暗黙にconst指定されていたのだ。なので陽にconst指定されてはいなかった。だがclangはC++14に対応しており、私も-std=c++14を渡していたのでこの関数は非constになっていた。よってconst性をviolateしてしまい、呼べなくなっていたのだ。

標準ライブラリ実装を分ければよかったわけだが、そこを横着したための問題ということになる。

結論

コンパイラ野良ビルドし、環境が用意している古いコンパイラとライブラリ実装は全て無視しよう。

追記(4/23)

📰2018-04-22のニュース - ゆなこん Yuna Computer System で取り上げて頂いたのだが、読み返してみるとこの記事がかなり言葉足らずだったことに気づいたので、少し追記しておこうと思う。

この時、何も考えずにコンパイラだけ変更してコンパイルしたので、古いGCC実装の標準ライブラリが新しめのclangによってコンパイルされると言う状況が完成しており、上記のような問題が起きたのだった。

GCCからしてみればC++14以前なので、constexprメンバは暗黙constでよかった。clangからしてみればC++14対応は済んでいたので、constexprメンバは暗黙constではなかった。誰も悪くなかった。悪かったのは何も気を使わずに横着してその2つを組み合わせた私だったと言うわけである。

と言うわけで、野良ビルドしてPATHや使うライブラリ実装に気を使っておけばこう言うことにはならないので、野良ビルドしよう。

プログラミングに挫折していた頃のこと

読者諸兄は、雪が降り積もって行くその過程を眺めていたことがあるだろうか。

雪が降り積もる時、雪は初めから着実に蓄積してゆくのではなく、初めは地面に触れては融け、触れては融けを繰り返す。そうして無為に見える時を過ごした後、地面が十分に冷えた時を境に、そこから初めて雪が積もり始めるのである。


唐突に思い出したのだが、私は3回、プログラミングに挫折している。

最初の一回は、まず条件分岐を理解していなかった。今で言うScratchのような、タイルを敷き詰めることでプログラムを書くような教育用言語を触ったのだが、動いて欲しいと思っている内容をコードに落とせなかった。小学校の頃だったので、そもそも自我がなかったせいではないかと思う。最後の一回は、制御フローはわかってきていたのだが、データ構造という概念がなく、ひたすらに冗長になってしまい嫌になっていた。配列を知らずにint a1, a2, a3 ...; のようにしてしまうやつだ。あと、今なら二分木かハッシュ、または find を使うところで無限に else if を書いたりしていた。そして小規模なプログラムの行数が1000行近くに達し、何もかもがわからなくなるという事態を引き起こしていた。

悪名高いポインタを知ったのは、プログラミングを4回目に始めた時だ。つまり今に繋がっている、そこから一度も投げ出していない(一年以上コードを書かなかった時期を投げ出していたと定義する)一連の流れの最初の頃である。ポインタで詰まった記憶はない。データがメモリにあるというモデルは頭の中に既にあったので、それを指しているもの、と非常にすんなりと理解した。たまに聞く p += sizeof(T) のような引っかかり方もしなかった。というのも当時読んでいた本に、ポインタに1足すと次の要素を見るようになる(sizeof相当の値をアドレスに足す)とexplicitに書いてあったからだ。その後順調にリンクリストや木構造の実装を知り、ポインタの便利さを理解し、のめり込むようになって行った。

さて、では真ん中の一回、挫折したのは何故だったか。あれは中学の頃だったと思う。条件分岐を理解して、非常に簡単なプログラムが書けるようになっていた。すると次にしたくなることはループだろう。確か、自分がやめていいとシグナルを送るまで回り続けるようにしようとして、私は以下のようなコードを書いた。

main:
    // do something...
    call main

これは走らなかった。「ネストが深すぎます」というエラーが出たのだ。当時の私は「ネスト」という言葉を知らなかったので、検索してみた。どうやら入れ子構造を作りすぎているということらしいとはわかったのだが、当時の私はその何が悪いのかわからなかった。

今ならわかる。関数の再帰が深すぎるので、スタックオーバーフローを引き起こす可能性を憂慮し、言語処理系が止めていたのだ。だが当時の私はスタックなどと言うものを知らない。しかも、これはある意味「正しい」コードだ。動くはずだ、と私は思っていたし、動かない理由を思いつけなかった。これはプログラムという抽象的なものが有限の資源しか持っていない現実のマシンで動いていることを意識しないと辿り着けない答えのような気がする。特に、当時の自分は callgoto か何かのように理解していたはずなので、これが動かない理由は真剣にわからなかった。よって回避策も考えつかなかった。私は簡単なプログラムを動かすことにも失敗し、しばらくプログラミングを投げ出していた。

今も、当時あの言語処理系が末尾最適化をするぐらい賢かったらどうなっていただろうと考える。プログラミング経験が4, 5年伸びていたかもしれない。ぴったりハマってから今まででこれだけ知識が増えたことを思うと……いや、起きなかったことを考えても仕方ないのだが……。

どんなことでも、一度ピタッと嵌るまでが難しい。最初は何がおかしいのかもわからないし、自分が何をわかっていないかもわからないからだ。一度ある程度点と点が繋がるところまで来ると、その後は何をわかっていないのかくらいはわかるようになり、爆発的に学習が進む。不思議な話ではあるし、じゃあどうやったら最初に目の粗い網が張れる程度に物事が繋がるところまで行けるのかと言われると、私は忘れてしまったのでよくわからない。

不思議なことに、何かよくわからないままにもがいて、あるいは放置して別のことを勉強しているうちに、戻ってきた時突然すんなりと理解できるようになっていたりする。何故なのかはわからない。関係ないと思っていた分野に実は根底が同じものがあって素地ができていたか、時間経過で昔の知識が整理されて見通しがよくなったか、それとも単に局所最適に囚われて変な見方をしていたせいでわからなくなっていたのが解放されたのか。理由はわからないが、これまでも何度もこう言うことがあり、聞いてみると知人の多くが同じ経験をしていることがわかった。

冬が好きな知人の一人は、この現象を積雪に譬えた。雪が積もる日も、初めは雪は地面に触れると融けてしまう。しばらくそれが無為に続いたのち、ある瞬間、地面が十分に冷えて、それ以降は雪が融けずに積もり始める。無為に見える過程でも、見えないところで変化は進行している。そしてあるところでそれがわっと外に見える形で出て来ることがある。大事なのは、無為に見える間に諦めてしまわないことなのだと。

さて、今日も地面を冷やそう。