プログラミング学習の論点が変わるとき
昔はいろんな言語に手を出していた。C++から始まって、Java、Ruby、Python、C、HTML、CSS、JavaScript、PHPなどのポピュラーなものから、LISP、Haskell、OCaml、R言語などの比較的マイナーなものまで。
しかし最近はめっきりC++とR言語以外は使わなくなった。研究とバイトで使うのがその2つしかない(上に他に道楽的に手を出す時間がない)ことも理由の一つだが、それ以上に自分のプログラミング学習における論点がシフトしたことが大きな要因だと思う。
プログラミングを始めた当初は、とにかくプログラミングの一ステートメントを書くこと自体が課題だった。やりたい作業を実行するためにはどの関数が必要なのか、なぜ予想したかたちでデータが返ってこないのか、この配列/リスト/データフレームというデータ構造はどう操作できるのか、それが「学習」の中心事項だった。だから、他の言語で似たような構造をたくさん見ることが、結果的に学習に貢献していた。X語を勉強したことがY語の勉強にもなっていた。
いまは、プログラミングの100ステートメントの配置について課題がある。何十通りも書き方がある中で、どれが最も効率的に(書くのにかかる時間や、特にその後のメンテナンスにかかる時間の面で)書けるのか、そこに関心がある。
たとえば次のような関心だ。
- 研究用に使うスクリプトは、少しだけ処理を変更して(たとえば10行の処理で5行目だけAではなくBというデータに注目して)同じ処理をしたいことがある。これは関数として抽象化して、引数でAかBを与えるべきだろうか?
できるんだからすればいいじゃん、と昔の僕は思っていた。しかし事態はもう少しゆっくり考えてみる必要がある。
「最初は1つの処理で済むと思っていたものが、実はAかBかを可変にしたい」というニーズが生じたことで、
someProcedure()
を
someProcedure(selected="A")
someProcedure(selected="B")
と渡す、と作成すると実はあとあと大変なことになる。というのも、研究が進むと「じゃあ5行目だけじゃなくて7行目も変えたらいい結果が返ってくるんじゃないだろうか?」とか、「8行目はどうだろうか?」と無数に可変にしたくなってくる。結果として、
someProcedure(selected="A")
は
someProcedure(selected="A", selected2="C")
someProcedure(selected="A", selected2="C", selected3="D")
someProcedure(selected="A", selected2="C", selected3="D", selected4="E")
someProcedure(selected="A", selected2="C", selected3="D", selected4="E", selected5="F", ... selectedN="XYZ")
と、際限が付かないくらいに長くなる。
これで、関数が
someProcedure
someProcedure2
someProcedure3
...
someProcedureN
と無数にあった場合はもっと最悪なことになる。コピペ・コンテストだ。
そうなると、次はデフォルト引数(selected2を与えなかった場合に、既定として使われるselected2の値)が問題になるし、一度書いた関数の呼び出しが。
someProcedure(selected="A", selected2="C")
だった場合、
someProcedure(selected="A", selected2="C", selected3="D", selected4="E")
にしたあとでも同じ動作をするように保証するべきか、という話も生じてくる。つまり、保存しなければいけない状態はどれか?という複雑性が生じてくる。
これに対して、現状僕がR言語で(現在はR言語でスクリプトを書いている)取っているベストプラクティスは、処理の分岐に関わるフラグを集約した list型の変数 procedureOptionList を宣言することだ。こうすると、
getProcedureOptionList <- function( ){
list(a = '/Users/update/160421/result.html',
b = c("a", "b")[1],
c = c("c") )
}
procedureOptionList <- getProcedureOptionList()
someProcedure(procedureOptionList){ # 引数の数を書き換える必要がない
procedureOptionList$a # どの処理フラグについても自由にアクセスできる
procedureOptionList$b
}
のように、処理フラグが追加されても、関数の引数は procedureOptionList の1つだけで一定なので、書き換えなければいけないデフォルト引数の箇所はおおよそゼロになる。この一工夫を導入してから、「あっ、さっき追加した変数、関数Aのデフォルト引数には設定したけれど、関数Bには設定していなかった」と言ったような、ケアレスミスによるエラー遭遇率が格段に減った。新しい処理フラグを追加するときも、「getProcedureOptionList関数のなかに追加すればいいんだ」と意思決定と行動もグンと早くなり、認知負担は激減した。深夜2時に寝ぼけて書いていてもミスる可能性が減っただろう。プログラミングで「メンテナンスを容易にする」というと、それは「理由の分からないバグが出ていて何時間で直せそうかわからなくて怖いのに、締切は5時間後に迫っていて、しかも2日寝てなくて頭は朦朧としてる」みたいな最悪の状態でもギリギリ間違えないくらいの簡単なタスクにできる工夫を事前に随所に散りばめてある、という意味合いのことである。
このような話題は、入門書では扱われない。list 型の変数は、たんに「可変長の要素を持てる」や「 vector 型と異なり、要素それぞれの変数の型は同一でなくてもよい」などと文法的な説明がなされるだけだ。実際にどこで使うと破壊的にミスが減るのか、未来のバッドコンディションなあなたを助けてくれるのか、というのは範疇外である。
そしてこのような一工夫は、どちらかというと言語依存だ。もちろんRの list でできることを 別言語のデータ構造で実現することは容易だろうが、依存とはそういう意味ではなくて、プログラムの書き手がその言語の文法を前提として考えるという意味だ。「listは可変長の要素を持てる。では引数に指定したら、引数がn個に増えたとしても一生書き換えなくて済むんじゃないか?」という判断を、言語非依存に「可変長の要素を持てるデータ構造を、引数として使うことで拡張性を維持したまま複雑性を減らす」と学べるとは信じがたい。そのような知見はむしろ、Rで一度そういうことを経験したから他の言語でもできないか、というときにそうやって抽象化されるものではないか。
だから、自分がよく使う言語の、よく使う文法の、まだ気づいていない活躍の場に対して論点が集中している。Pythonではどうだとか、OCamlだとこう書けるとか、そういう話はどうでもよくなった。そんなことの比較に認知資源を割けるほど、目の前のプログラムは浅くない。自分が入門書やネットの記事で見てきた、浅い検討しか成されていないプログラムで「学んでしまった」書き方を、批判的に見て別案(繰り返すが、別案は1つではなく、数十通り存在する)を出し、それが実際に動くか試すのは、それだけで作業時間が潰れる。しかしそうしないと、新しく追加した引数を100個の関数にコピペし続けて、一つでも抜かしたらエラー、みたいな知的でも何でもない、無駄に集中力だけ必要な作業になってしまう。(ちなみに、たまに「研究用に使うプログラムなんて大したことないから、別にすぐ書けるようになるよ」みたいなことを言う人がいるが、そういう人が書いたプログラムを見るとこのような検討がなされていないことが多い。きっとものすごい時間を投下して修正しているか、それともバグがあるまま研究して間違った結果を出力しているかのどちらかだと思う。こういう間違いは公開したらすぐに他人には気づかれるので、ソースコードを公開する文化が根付かない遠因の一つにもなっていると思う)
言ってみれば、自分が普段使う言語のよく使う関数や型をほぼ暗記したあとでも、いやむしろその後こそ、より一層プログラミングは学ぶことが増える。日本的な暗記学習の弊害なのかもしれない。ググらなくても思い出せることと、目の前の複雑性を軽減させるために適切な場所に使えることは、けっこう違う。