プログラミングを上達させているのは「プログラムを書く」ことではない
それは、「プログラムを動かした結果を考察する」ことである。
この事実はプログラミングの学習高速化にとって致命的に重要な事実であるが、驚くほど認識されていない。私自身もプログラミングを本格的に学び始めて、約2年経った今日にやっと明確に認識できた。
この事実は数々の指摘を含んでいる。
- コンパイラよりもインタプリタを使ったほうが学習が早い。
(∵結果を考察する回数が増えるから。)
irb, python, JavaScriptコンソール, ocaml。
C++はClingのような不完全なインタプリタしか存在しない点で非常に学習に向いていない。 - インタプリタが実質存在しない言語でも、結果が表示されるまでのインタラクトの時間を短縮すればよい。
たとえば結果を見るまでに5個のコマンドを実行する必要があるなら、それを1つのコマンドにまとめてしまって、自分が今動かそうと思っているプログラムの結果とそのソースコードの関係以外について考える時間を0に近づける。これが「結果を考察する回数」を増やす上にその1回1回の質を高めることに繋がる。 - 文法についてググっている瞬間は全く別の作業をしているに等しい。できる限り、ググった中で「結果を考察できそうなコード」を探して動かし、結果を出したあとに記述を読むべき。結果さえ分かれば、そこからどんな文章が書けるのか分かるはずで、それ以外の文章が書かれていたらそれは書き手の妄想に過ぎない。
- 自分が書いていないプログラムでも、結果が考察できれば学習に役立つ。
たとえばautoconfやconfigure(中身は単なるシェルスクリプト)、makeやsudo make installと言ったプログラムについて車輪の再発明をすることはないが、数行足して出力ファイルの違いを見て、といったことで実用可能なレベルの知識は身に付くはず。 - ソースコードを読むという行為でプログラミングについて何か学べるとするなら、それは「1.自分が既に知っている『動かした結果』について、その結果をもたらした部分を知る」もしくは「ソースコードをわずかにいじってその前後でのアウトプットの変化を見る」ことによる。この後者は全く「プログラムを動かした結果を考察する」と同じ行為で学んでいる。この瞬間こそをインタプリタ化できないか?
「1000時間、一度もコンパイルせずにソースコードを書き続けた場合」
「1000時間、一度もソースコードを書くことはせず、インタプリタだけを使った場合」
後者のほうが身についている物は確実に多いだろう。たとえ複数行にわたる関数を定義する経験が少なくとも。
前者はセミコロンに対する感覚すら身についていないだろうと予想される。
「自分が認識した範囲で行った変更」(=ソースコードを書く、環境変数など設定変更を行う)の後にプログラムが何を返すか、という2つの情報の組(tuple)こそが、学習で蓄積しているデータ、学習データだろう。
それ以外のこと、検索結果にいいのが出てこない、実行するまでにロード時間が必要、エディタが使いづらい、ウィンドウの切り替えが煩雑、などの種々は、排除すべき夾雑物に過ぎない。
数学の「まちがい」とプログラミングの「まちがい」は違う
数学の「まちがい」は1+1=3や 10 mod 4 = 3 のような、本来の定義とは異なる値を求めてしまうことを指す。
プログラミングにおける「まちがい」はそれよりも少しだけ構造が複雑だ。人間が定義とは異なることをしてしまうのは数学の「まちがい」の場合と共通だが、コンピュータが定義通りの事をいつも・どこでも・必ず遂行する、という点が決定的に違う。*1
コンピュータが下支えをしている訳である。
なので面白いことが起こる。人間が間違えて、例えばWarshall-Floydを実装しようとしてfor文をk, i, jの順ではなくi, j, kの順に回してしまうと、それはWarshall-Floydのアルゴリズムの定義からは異なってしまうが、実行される処理自体は物理現象として「正しい」。再現性は100%なので決定的で、決して気まぐれなところはない。いつまでも物理現象としては正しいことをしていて、求めている概念としては間違えた処理をしていてくれる。
おそらくそれというのは学習にとっては最高の環境なんじゃないのかなあと最近は思い始めた。学習というのはつまりは仮説の修正だと言える。あのパン屋は日曜休日だと食べログに書いてあったから開いてないかと思ったとか(実際には店主がきまぐれに開けることがある)、このオートマトンは数万個のテストケースを通ったからまあ正しいと仮説を立ててもなんら心配しないですむでしょうとか、世の中で知識とされていることは単に可能性の高い仮説に過ぎない(ただひとつ、数学の「証明」を除いて)。
仮説を修正する上で、自分の理解が間違っていて、相手(コンピュータ)の理解が正しいことが確実に仮定できるというのは非常に有利だ。探索空間がものすごく狭まる。自分が書いたプログラム(=仮説)だけを読めばいい状況というのは、相手の動作が正しいかを逐一確認しなければならない状況と比べると格段に修正が早い*2
紙に書いた数式ではそれができない。
紙に書いた数式を、例えば採点者や教授に見せたとして、その相手がまちがえない確率はヒューマンエラーの起きる確率を割らない。前提さえ満たせば誤答率0%であるコンピュータというのは、計算において強力な思考補助手段にほかならない。
論破について
感情の強さに妥当性を託す話し方は有効なのだろうか。
相手が信じていれば有効だ。根拠がなくても信頼されていれば「▲▲さんが言ってるなら」という気持ちが率先して相手を説得してくれる。
だが、相手が信じてない場合は別だ。普段あまり自分の意見を聞いてくれてない相手に、どうしても譲れない点(害をもたらす特定の疑似科学、たとえば瀉血の否定とか)を主張するとき、説得の是非はほぼ最初の10数秒の主張内容で決まると思う。
以前に、とある先輩が全く約束を守らないで(しかも社会人との約束)、関係者だった僕が社会人の人から(その先輩)はどうしたんだと詰め寄られたことがある。
彼は最近ちょっと自分探しのようなことをしていて、責任を負うことが苦手になっているから、申し訳ないですが時間を置いて下さい(関係者からしばらく外させて下さい)、と述べた。
そのときに相手方が言った言葉が今でも忘れられない。
「彼に必要なのは時間ではなく治療だと思います。このような行動は正常な精神状態ではできません」
時間にしてたった3,4秒、
そのあとに僕が述べた「いや、病気というほどのことでは。彼はとてもいいアイデアを出してますし」という反論についても「(具体的な病名)の人は(特定の期間)においては通常かそれ以上の能力を発揮します。アイデアを出せることは彼がそのような治療を必要としている病気にかかっている可能性を排除しません」と10秒ほどで論破された。
あの議論を続けることもできただろう。ただ僕はそこで納得してしまった。
もしあそこで先方が僕の「時間が必要」論に対して「でもねえ…」などの曖昧な態度で接していたら、僕は意見を変えなかった。その後に続ける具体的な事例をいくつか持っているつもりだったし、それで納得行かなかったにしても「彼を見守ってあげましょう、ね?」という根拠の無い感情論で押し切っていたことが想像できる。
少なくとも当時の僕はそれ以外に議論のしかたを知らなかった。
「こっちのペースに持ち込めばきっと納得してくれるだろうに」と思いながらも全くそこまで辿りつけずに相手が好き勝手して結局自滅する、みたいな例を見ることがある。
僕は彼と話すまで、その先輩が病気(と言われるほどの症状)だと認識していなかった。本当に相手の行動を変容させたいのなら、冒頭10数秒、そこで相手の認識に風穴を開けなければいけない。
初心者に勧めるプログラミング言語は何か
僕は、プログラムをやりたいという人に出会ったら、JavaScriptを必ず勧めている。
以下では僕がこれまでに扱ったことのある言語について、なぜそれを初心者に勧めないのかを列挙する。
なおここで初心者と書いたのは、全くの初心者を表す。メールやウェブサイト閲覧はしているが、キーボードで文章やフォーム以外のものを入力した経験がない人を指す。
そして、目標は「どれかの言語でオブジェクト指向プログラミングをできるようにすること」である。そのため、LISPは不本意な扱いをしている。
C++:
- コンパイルの存在
この理由だけで全く以て初心者には適さない。「人間が書いたものを、複数の翻訳を繰り返して、ついに機械が読める命令にまで変換する」という事実は、初心者の理解には全く適さない。
彼らにまず覚えてもらうべきなのは、そのような物理的な現実ではない。「人間が書いた命令を機械が行なってくれる」という仮想的な図式である。そのために、初心者が学ぶ言語はインタプリタ言語であること、もしくは実行時コンパイラの使用が可能であることが必須である。
この図式は、プログラムを書く上で絶対に必要になることである。なぜかと言えば、自分が指示した命令を機械が行なってくれることに興味を抱く人格(それが「これは役に立つ」という純粋に実利的なものだろうと、「面白いな」という実利を度外視した好奇心だろうと)が自分の中に生まれない限り、その人は講義をやめたら二度と書かないからである。
Ruby:
- オブジェクト指向プログラミングの強制
少なくとも規模が1000行を超えるまでは、絶対にオブジェクト指向プログラミングを教えてはならない。なぜかと言えば、それはコードが大規模になってきたから必然的に生まれた考え方であって、それらがなくても代入や再帰などの文法は教えられるからだ。10行規模のコードを書いている段階の初心者に、絶対にオブジェクト指向プログラミングの話はしてはならない。
ただし、Rubyの場合はC++よりはまだ推奨でき、もし最終目的が文字列処理であればJavaScriptではなくRubyが最善手となると思う。なぜならオブジェクト指向プログラミングを強制すると言っても、数十行段階のコードで扱うのはどうせnewだけだからだ。これはおまじないと言えば、あとはいくらでも大事なことを教えることができる。そしてRubyは、その「大事なこと」が非常に直観的な記法で書ける。 10.times do とか[1,2,3,4].eachとか、これ以上わかりやすいイテレータは存在しないんじゃないかというほどだ。
ただしその場合も、Matzが書いたRubyの公式ドキュメントはクラスやインスタンスや継承といったオブジェクト指向プログラミングの用語ばかりでまずもって読めないため、苦戦を強いられるだろう。 - UNIXの知識を前提とされる
これはドキュメントを読む際に、上記の「オブジェクト指向プログラミングの用語ばかり」なことと相まって、Rubyの理解を難しくさせている。
Lisp:
- 制御構造がオブジェクト指向プログラミングの学習に連続しない
と思う。ここらへんは勉強不足なので言及できない。
HTML:
- 「プログラミング言語だ」と思えるまでの道筋、長すぎ
すなわち、<img id="dog_img">や<p class="paragraphTitle">のように、class名やid名を用いて、JavaScriptやCSSからそれらを変更する、DOM操作の段階に至るまでに覚えることが多すぎる。その段階に至れば十分プログラミングらしいことができるが、それまでは殆ど何も大切なことを伝えられない。
ただし当人が好きでいじってみたいというのであれば止める必要は全くないだろう。最終的にどこかで触れるようになっておくべき言語なので無駄ではない。
CSS:
- LESSなどを用いない限りただの設定ファイルに過ぎない
Java:
- おまじないが最初は多すぎる
- PythonとRubyはどれくらい初心者に勧められるかといえば、同じくらい勧められる。しかしどちらがより日本人には勧められるかと考えた場合、やはりRubyに軍配が上がる。
Ruby言語開発者であるまつもとゆきひろは、自分の作ったRubyの文法に関するエッセイを執筆している(『コードの未来』など)。これが自国の言語である日本語で読める、という利点は破壊的に大きい。
プログラミングを続けていれば、いずれ英語で内容を理解するしかない時がいやでも必ず訪れる。だがだからこそ、現存する日本語資料でどこまで理解できるのかが大事になってくる。
英語で理解したことは検算・暗記保持・伝達が容易でない。論文を読んで理解したと思っても、それを試すためにはテストコードを書かなければ怪しい。間違って理解している可能性は日本語よりよほど高い。加えて文面を覚えておくことも日本語よりよほど難しい。キーワードを思い出すのは、英語よりも日本語のほうが圧倒的に早い。この言語の要点を3つにまとめると?とか、この文法を使ってはいけないのはどんな局面?とかいう、読書中に何度も出てくるような疑問に、英語を読んでいるときに答えるのは至難の業である。そして理解したことを他人に「これ読むと分かるよ!」と言っても、他人が読めない場合が生じる。
英語で理解することは、翻訳されていない情報を得られる利点とともに、かなり大きな欠点を持っているということは、あまり口にして語られない(たぶんあまり意識されていない)。
それはあまり初心者のプログラミングの勉強とは関係ない。
いつか解かれるべき「サブ問題」として、後回しにすべきだ。
『コードの未来』は非常に面白い。クロージャや、イテレータ、オブジェクト指向プログラミングをなぜするかなど、開発者の悩んだ末の知見は、文法を学ぶ意欲をもう一度立ち直らせてくれる。僕が読んだことあるのはC++の開発者の本とRubyの開発者の本だけだが、他の言語開発者の本も同様な意欲を与えてくれるものだと信じている(C++の開発者ストラウストラップはデンマーク人だが、この本は翻訳が良い)。
以上を踏まえて、僕が初心者に勧める言語は、JavaScriptである。
JavaScriptは上記の要件を多く満たす。
インタプリタ言語であり、実行時コンパイルが可能、オブジェクト指向プログラミングを強制されず、初心者は最初は手続型プログラミングで思い思いの100行プログラムを書ける。そして1000行を超えたぐらいでデータの管理がもはや自分の力量を超えていることを悟り、オブジェクト指向に開眼する…という筋立てである。
加えてJavaScriptには以下の利点もある。
- 環境構築が不要なほぼ唯一の言語
InternetExplorer、GoogleChrome、マックならSafariと、とにかくブラウザさえ開けば学習環境が完結する。最初の1時間はテキストエディタすら必要がない。JavaScriptコンソールは偉大すぎる。 - 学習結果の公開が容易
たとえばこんなプログラム作ったよ!と動かさせるのは、RubyやPythonだと相手がプログラマでも無い限り難しい。JavaScriptであればJsFiddleや無料レンタルサーバーでいくらでも公開が可能である。
ということで、僕はJavaScriptを勧めている。ここまで内容を整理してきて、いくつかのことに気付いた。
- 僕は結局、手続型プログラミングとオブジェクト指向プログラミングの2つのパラダイムしかまともに経験していない。LispはN-queenのプログラムを一度書いた程度なので、関数型プログラミングを知っているとは口が割けても言えない。
- 同時に、手続型プログラミングはオブジェクト指向プログラミングより前に経験されるべきだ、という信念にも似た主張を持っている。というか、手続型プログラミングであれば、日常感覚から連続させて理解してもらえると思っているようだ。たとえば、
1+1 = 2;
2+2 = 4;
4 +4 = 8;
と書いていて、
function doublize(n) { return n + n; }
を思いついて、
doublize(1)
doublize(2)
doublize(4)
と書くことが、
1+1 = 2;
2 - 2 = 0; /// ミス
4 +4 = 8;
のような、「不必要なミス」を減らせるなど。抽象化を概念から説明しないでも、日常生活で使っているのだからそれを指させばよい。 - 非構造化プログラミングや逐次型プログラミングは一瞬で通り過ぎるものと考えているよう。たとえば上の例で3行で終わったように。
- 文法を理解することを比較的重視している。これはいまC++を書いていることもあって、「この文法を理解していないとこの機能が実現できない」という局面にしょっちゅう出くわすからだと思う。JavaScriptを習っていたときの僕はもっと、「いけいけどんどんとりあえず動けばそれでよし」にやっていたはずだ。これもまたオブジェクト指向プログラミングへの変更の兆しの一つなのかもしれない(1000行を超えると「動けばそれで」な姿勢だと数時間かかるようなデバッグが連続して開発意欲が激減する)。
- 全員がプログラミングに興味を持つことはないと納得している。昔の僕はもうすこし「みんな興味を持つはずだ!」と思っていた。しかし学習が進むにつれて、自分でも興味を持てないような内容が頻出することに気付き、これは一部の好きな人がやるものだなと思うようになっていった。まあ、最初は興味がなかったことに、自然に興味がわき出すのが、ひとつプログラミングの面白いところではあるのだろうけど。リンカについて勉強したいと思うようなことなんて、1年前の僕だったら絶対になかっただろう。
ちなみに、JavaScriptで1000行オーダーを超えたら?と言われれば、速度重視ならC++、効率重視ならRubyを勧めるだろう。
主張をまとめる。
- 初心者が覚えるべきは「人間が書いた命令を機械が実行してくれる」という図式
- その図式に興味が生じない限り、その人がプログラミングを続けることはないし、それならそれでよい
- その図式を最も端的に伝えるのはJavaScript
- オブジェクト指向は1000行のオーダーになるまでは絶対に使わせないこと
- オブジェクト指向になったら、RubyやC++を検討するとよい
df-pnアルゴリズム学習記(2)
[進捗]
『ストラウストラップのプログラミング入門』を読んでいる。ほんとこれが無かったら実装するのは無理だった。いまは速さを全く追及していないので、とにかく安全に動いてテスト容易なプログラムにしていきたい。
それにしてもvectorが便利だ。とくに
vector<char*> testBoards;
testBoards.push_back("tsumeShogi/1_1.txt");
testBoards.push_back("tsumeShogi/1_2.txt");
...
for(unsigned int i=0; i < testBoards.size(); i++){
bd.readBoard(testBoards[i], bd);
...
}
のように、テストする盤面をvectorで管理したことによる開発効率化は計り知れない。テストの追加が非常に楽に済むようになった。これはものすごい改善につながる。
それと、以前に有識者から習った発想が役に立った。「どういう風に切り分ければテストが容易になるのか」という点で考えた結果、以下の3点を思いついた。
- 関数単位でテストする。特にreturn a == 1 ? true : false; のように全出力がテストできるものはこれで以降のバグ探し領域から除外できるのが嬉しい。
- 関数内のまとまった仕事を書くごとにテストを書いて実行する、消して次の仕事を書いたらまたそのテストを書いて実行して、のようにやっていくと、関数がそれ以上切り分けられない複数の仕事を行うものである場合に、大方正常な動作をするだろうと判断できる。
もちろん理想は全入出力をテストすることだが、実際にはコスパは逓減するばかりなので、この「完成時には消えているテストコード」程度が妥当と判断した。これはユニットテストにすら分類されないだろうから、あくまで石橋を叩いて渡る程度の気休めである。 - 構成はトップダウンに行い、実装を完了させるのはボトムアップに行う。つまり、一番上の例えばdf-pnならdfpn_root()関数から書き始めるが、テスト用に/実際に呼び出すのはそれが一番最後になるということ。
呼び出さなければ型チェック程度のことしか行われないので、そこでバグが出ることは少ない。呼び出さないにしても書いておくと、実装を完了させるその小さな関数が何を満たせばよいのかがより明確になる。戻り値がintなのかや引数のどこが参照になるのかなど。
とりあえず、たまに5手詰まで解けるようになったので嬉しい。
しばらくはテストを助ける関数を作ることにする。内部で何を行っているのか、いまいちつかめていないので、それを視覚化する関数を実装する。
このような関数は今のところ、board.print()とmoves.print()は当然として、gdbで実行時にabortした盤面をファイルに出力できるdump()が強力である。思いついて試しに作ってみたらものすごく便利。このような関数は、先走ってアルゴリズムを改良するよりも遠回りのようで近道なので、作る。
df-pnアルゴリズム学習記(1)
詰め将棋ならdf-pnアルゴリズムとどこかで聞いた。
今日から3日かけてこのアルゴリズムを理解して実装してみる。
理解するために飛び回るのは
CiNii 論文 - df-pnアルゴリズムの詰将棋を解くプログラムへの応用
CiNii 論文 - 詰将棋を解くための探索技術について(<レクチャーシリーズ>コンピュータ将棋の技術〔第2回〕)
の2つの中だ。
これらの参考文献のうち、前者はdf-pnの考案者による論文だが前提知識を多々必要とする様子だった。
よって(将棋の駒の動かし方以外の)前提知識を必要としない後者を中心に今日は読む。
ポイントだと感じたところ。まだ実装していないので、間違って理解している可能性が高い:
- 「合法手」の意味が指し将棋と詰め将棋で違う。前者では「将棋のルールに違反しない手」を指すが、後者ではプレイヤによって意味が違う。
攻め方の「合法手」: 王手をかける手
玉方の「合法手」: 王手から逃げる手
これを理解していないとなぜ終端OR節点、終端AND節点の2つでそれぞれdn(n) = 0 とpn(n) =0となるのか理解できない。
終端OR節点でdn(n) = 0 となる理由: 「王手をかける手」がないから。
終端AND節点でpn(n)=0となる理由: 「王手から逃げる手」がないから。
理解せずともとりあえず実装はできる…と夢を見たいが恐らく実装もできないと思う。すなわち合法手生成の関数をプレイヤごとに別々に用意しなければいけないと気付かないといけない。たとえば
genmove_semeru()
genmove_mamoru()
のように実装する。genmove(semeruOrMamoru)でもいいけど。指し将棋とは決定的にここが違う。 - よって、
終端OR節点でpn(n) = 無限
終端AND節点でdn(n) = 無限
となっているのは、便宜である。実際にはpn(終端OR節点) = 3やdn(終端AND節点) = 1程度かもしれない。この「無限」は「整数値として考える意味がなくなった」ことを表す記号であり、おそらくは計算上でつじつまが合うからというだけの理由でこう書かれている(実装のときに、条件分岐などで便利なのだろう)。 - 先端節点と終端節点は違う。先端節点とはまだ子節点(子ノード)があるかもしれない節点のこと。先端節点についてgenmove(semeruOrMamoru)したときに0が表示されたときに、その先端節点が終端節点になる。
ここまでが一まとまりに理解したい事柄で、以下は個々別々に覚えておきたい事柄。
- 子節点に向かう合法手が少ない節点ほど、探索を優先させるべきとみなされる。
- 各節点が、{証明数、反証数、値}の3つ組をそれぞれ持つということを強く意識しておきたい。AND/OR木におけるこの値(true,false,不明)1つだけをminimax木における評価値と考えておくと、あれじゃあ証明数と反証数ってなんで必要なの?という痛い目に合う。実際には証明数と反証数という(2つの)評価値がminimax木における(Ⅰつの)評価値に対応しており、trueやfalseや不明という値というのは証明数と反証数のどちらかが0になったときをそう総称しているだけに過ぎない。
と理解しているのだがちょっとこの理解は信用できない。というのは上記の参考文献の前者において、GenerateLegalMoves()を呼び出す前に既に末端ノードを判定しているからだ。ということはgenmove()された合法手の数以外に末端を見分ける識別基準が存在するのかも。ここがまだ怪しい。