プログラミング的ゾンビとプログラミングの学習について

公開:2020/10/20
更新:2020/10/20
10 min読了の目安(約9300字IDEAアイデア記事 12

背景

こちらのまとめを読んで、「数学的ゾンビ」と面白い考え方だなと思うので、プログラミング的ゾンビというのも考えられないかと考えてみた。そして、同時にプログラミングの理解だとかプログラミングの学習とか、そのところも同時に書いていければなーとかでいろいろ書いてみた。

プログラミング的ゾンビについて

プログラミング的ゾンビとは?

プログラミング的ゾンビというものを考えたとき、それはどういうものか?考えてみると以下の2つの点があてはまれば、プログラミング的ゾンビだと言えるだろう。

  • プログラムを書けるように見える
  • プログラムの内容や意味するところは分かっていない

と考えたとき、どのような人がプログラミング的ゾンビだろうか?と思うわけで、具体例を示していこう。

プログラミング的ゾンビの具体例

こうしたとき、プログラミングゾンビの一番典型的なのが、誰かが書いたコードをひたすらコピペして作る人たちだ。そういう人たちは、たぶんコピペのコードの内容をはっきりと理解していないことが多いと思う。そういう人たちはたぶん、時間がなかったり、そもそもプログラミングについて学習したこと無い、あるいは学習が足りない等の状況下で、何かプログラミングで解決しなければならないときに起こる。

それでそれ以外にもプログラミング的ゾンビの例として、どんな場合があるか考えてみると、解きたい問題と解決のコードをセットで覚えて、コードを書いているパターンだ。つまり、(問題, コード) の集合を覚えているということに近い。そして新しい問題に対して類似の問題を探り、コードをその問題が解決するように変形させていくパターンだ。

たとえば、1 ~ 100まで足すという問題があり、それに対するコードがあったとしよう

(print (do ((i 1 (+ i 1)) (sum 0)) 
        ((> i 100) sum) 
        (setf sum (+ sum i))))

それでももし、1 ~ 10000までの足すという問題があれば、単純に100を10000に書き換えるということをやっていく。これは、Common Lispのdoマクロというのが理解できなくても、1 ~ 10000まで足すという問題を解決するコードが書けてしまう。(本来はcommon lispではloopを使うと思いますが、ここではあえて、違う書き方をしてみました。)実際に、プログラミング教育を私がやっていて、よく出くわす例をしてはこのようなものだ。課題は解ける、プログラミングはできるのだが、実はコードが何をやっているのか、深く説明をもとめるとできないみたいなことがよくあるのだ。(例ではLispを上げたが、私がよく見るのはScratchやJSやC#なのだが。)

プログラミング的ゾンビだなと思うとき

プログラミング的ゾンビだと判断するときの基準はなんだろうか?一つ、私が考えるのはそれは、間違えを犯したときだ。コードの間違えがかなり独特な間違えをする。そしてコードが間違っているとき、プログラミング的ゾンビ間違えを自分で直すことができない。たとえば先ほどの例でいえばこのような間違えのときだ。

(do ((i 1 (+ i 1)) (sum 0)) 
        ((> i 100) sum) 
        (setf sum (+ sum i)))
(print)

これは極端な例だが、しかし実際にこれを似たような間違えを犯している人が案外多かった。この例では、ループもしくはprint式を書く場所が違うのだが、彼らは平気な顔をして書いて、「なぜ間違っているかわからない」ということを言いだすのである。

先ほどの回答をみると何が間違っているか一目瞭然だが、考えてみると写経でコードを書いて学習している等だったとして、彼らにとっては、コードの意味よりも、写経して正しいコードと間違え探しをしながらコードを書いてきたので、そもそもコードを構文的にとらえることができていないのでは?と考えるのである。

もう一つある、それは全く新しい問題を出したときである。たとえば、先ほどの問題から、「1 ~ 100の偶数のみを足せ」の場合、解答できないパターンだ。もし、彼らに類似するパターンがあれば、回答することができるが、彼らが経験したことがない問題に出くわした場合、彼らは解答することができないのだ。

一方では、問題を解くのがめちゃくちゃ早いという特質もある。過去のパターンをそのまま思い付くことができればいいため、分かる問題はすぐできるのだ。これはもちろんプログラミング的ゾンビのみの特質ではなくて、一般のプログラマーでも解きなれた問題はすぐ書けるというのに近い。

プログラミング的ゾンビができないこと

と考えたとき、プログラミング的ゾンビができないこと、というのはつまりこういうことだと私は考える

間違えた問題を直すことができない。これはプログラミングの理解が足りていないということだろうと考えている。

自分で思いつくことができない。パターンからコードを生み出すという点ではプログラミング的ゾンビはコードを生成することができる。むしろ、その方が高速に生み出せる。しかしながら、パターンに適合しない問題に当たったときに、プログラミング的ゾンビはその問題を解決することができなくなるのだ。つまり、自分でコードのパターンを思いつくことができないと言ってもいいかもしれない。

この2つのことについて順番に考えていこう。

プログラミングの理解とは

とここまで書いていくと、そもそもプログラミングの理解するということはどういうことか?という疑問が出てくるだろう。たとえば先ほどのコード

(print (do ((i 1 (+ i 1)) (sum 0)) 
        ((> i 100) sum) 
        (setf sum (+ sum i))))

というコードを理解するというのはいったいどのようなものだろうか?ということである。また、理解していくというのはどこまで理解するのだろうか?というコードの理解の深さもあるだろう。

私としての意見を述べていこう。ざっくりを私が考えるのは、

  1. そのコードを構文的に把握し、コードを部分に分解できること
  2. コードの部分が何を意味するのかを部分的に説明できること
  3. それらを統合してコードの挙動が把握できること
  4. コード全体の挙動からどうしてその問題が解けるのか理解できること

この四点である。もう少し具体的に説明していこう。

まず(1)についてだが、構文的に把握するというのは、たとえば、上記において(print ...)とあるが、printというのは関数名であり、(do ...)というのはdoマクロであることを把握する。もっと深く行くと(do (ループ変数について書く) (ループが終わる条件を書く) ループする式)を書くみたいな、形を捉えて、更に言えば、(do ((i 1 (+ i 1) ...と書いたとき、iはdoループ内で使う変数、1が初期値、(+ i 1)が更新する時の式、そして、((i ...) (sum 0)) ...と書いてあるので、sumもループで使う変数で初期値が0であることがわかる...というようなことだ。

ここで注意したいのはあくまで「構文的」に把握する段階である。それが何を意味するのかまではいかない。printという関数名、doマクロの文法的な構造は分かるが、それがどのような挙動をするか?等はまだ把握していなく、それを把握するのは次の段階である。その前にこのようなコードの文法を捉えていく、そのような段階があるのだ。

(2)の段階について、つまりコードの部分が何を意味するんか?といのうことであるが、例えば、(print ...)とあるのは、printの後方に掛かれた式を評価して、その値を標準出力へ書き出すというものであるとか、(do ((i 1 (+ i 1)) ...とかけば、ループ変数iの初期値で初期値は0で、ループが継続するときiに1を足したものをiに代入して、次のループに進むみたいなそのような理解ができるとかそのようなことである。

(3)の段階にて、たとえば、先ほどの式の例でいえば、「このコードはつまり、変数iと変数sumという2つのループ変数を用意して、最初、iが0、sumが0で、ループが一つ進むと iは1で、sumが1、また次はiは2で、sumは3になって...そしてiが101になったらループが止まって、その時のsumの値が返されるのか~」等を把握することである。こうやって文章で書いているが、私の場合は実際に機械が動いているのをイメージしている。しかしここではまだ、機械の動作のイメージだったり、コード全体については把握しているが、じゃあなぜそれが目的のためのコードかという段階に至っていない。ただ、例えばコードの順番が分かるみたいなものである。

(4)の段階だと、どのようなコードがなぜ、正しく答えを出せるのか?という問いに答えていくところである。上記の例でいえば、大雑把だが、簡単に説明すると、
n回ループを回るとき、iはnとなり、また、sumは前回のsumにnを足されたものになる。sumは初期値0なので、つまり

(ループ0回目のsum)=0(ループ0回目のsum) = 0
(ループn回目のsum)=(ループn1回目のsum)+n(ループn回目のsum) = (ループn-1回目のsum) + n

となるので、ループ100回目に、sumが0 ~ 100まで足されたものとなり、iが100になるとループから脱出し、sumの値をこのループは返すため、1 ~ 100を足した問題に解答できる。わざわざ数式を書いてみたが、実際のところこのようなイメージを持っているだけであって、頭の中で数式を入れて検証しているわけではない。ただ、ある程度このようなものに近い、説明ができるみたいなものである。コードの挙動がどうして、その問題を解決するかということである。

上記のことはわたしが、思うコードの理解を大雑把に説明してみたが、たぶん人によって違う理解をしているのかもしれない。もっと雑にとらえていたり、もっと細かく正確に厳密に考えて書いているかもしれない。4つのプロセスを踏んだが、人によって、理解の順番も違うかもしれない。でも私の頭の中ではおそらくこのような段階を得て、理解しているような気がするのである。順番は異なるかもしれないが、ざっくり意識的にも無意識的にもプログラミングを読むとき、把握するときにやっているような気がするのである。

プログラミングの内容を把握していないとは?

つまるところ、上記の段階のどれかが理解できていないということである。つまり

  1. コードの構文を把握できず、部分に分解することができない
  2. コードの部分が理解できていない
  3. コードを統合することができない。
  4. なぜそのようなコードが正しいのか理解できない

ということである。プログラミング的ゾンビというのは考えてみるとこれらの工程をすっとばして、コードを書く人なのかもしれない。

ほとんどの場合、1ができていないことが多い。つまり、シンタックスを捉えきれていないパターンが多いと私は感じている。例えば、先ほどの間違え

(do ((i 1 (+ i 1)) (sum 0)) 
        ((> i 100) sum) 
        (setf sum (+ sum i)))
(print)

というものはそもそも構文の理解ができていないと判断することができるだろう。

2,3,4の場合はどうだろうか?ほとんどの場合構文が理解することができれば、2と3、4への理解が速い。ただたまに1が分かっても、2,3,4がよく分かっていないという例に出くわすことがたまにあるように感じる。それは、コミュニケーションをとっていくとだんだんわかっていくのだ。

たとえば「doループはどこ?」と聞くと、そのコードの部分はどこにあるか?というのは分かっている。しかし、じゃあ、このループは何しているの?と答えられない場合だ。これは2のパターンだなと思う。

3の場合は、全部のことを脳内でやっちゃって、ぐちゃぐちゃになっている場合である。その場合、処理をいったん紙に書いてごらんと言ったら、分かったとなる人が多い。ほとんどの場合、ワーキングメモリが足りなくて、全てのことが考えられないのである。

4の場合は、プログラミングを分解して、各自がどのように動いていくか、それについては納得してもらえるが、でもそう動いたからといって、なんで問題が解決するのかが分からない人である。それは数学的な理解だったり、アルゴリズムの理解だったり、コード以上に、背景だとか理論がわからない人だったりする。例えば、底辺に高さを掛けて、2で割るとどうして、三角形の面積ができるか?わからないみたいなものに近い。

つまり、プログラミングの理解というのは、このように様々なことがあると、私は思っている。

どうして、そのコードが創造できるのか?どのようにして、そのコードが思いつくののか?

一方、プログラミング的ゾンビは、過去のあるパターンを引き出す以外のことで、コードを思いつくいというのができないと述べた。一方で、コードを思い付くって、どのようにしてやっているのだろうか? そのところを述べて行く。

先ほどのコードの別解について

そういえば、先程のコード、これらのコードは違う解決の仕方もあるだろう例えば、

(print (do ((i 1 (+ i 1)) (sum 0 (+ sum i))) 
        ((> i 100) sum)))

と書いてもいいし、loopマクロを使って、

(print (loop for i from i to 100 sum i))

とも書いてよい。またまたループ等を使わず、簡単な数学の知識を使って

(print (let ((n 100)) (* n (+ n 1) (/ 1 2))))

などやって良いのだ。
たまたま、いくつかある正しい回答の選択肢から適当に選んだというだけである。

基本的なプログラミングの理解が前提

さて、これらのコードをどうして私は思いつくことができるのだろうか?組み立てることができたのだろうか?頭のなかで何をきっかけとして、どのように展開して、どのようにこのコードを構築したのだろうか?

まず、私が考えるに、先程のプログラミングの理解が前提となるのは間違いないだろう。(1)文法的な構文的な理解、(2)各構文や式がどのような意味を持つのか、(3)各構文の意味を統合し把握、(4)背景的な理論の理解はまず前提となるだろう。これらの理解がなければ、プログラミングのコードを書くことはできなりだろう。

どうやってアイディアを創造するか?

しかしながら、それだけでは、プログラミングの理解だけでは新しくコードは創造することはできないだろう。例えば、釘とトンカチと木材で、釘を打つことや、鋸で木を切ることを、覚えたとしても、テーブルがいきなりつくれるわけではないだろう。そこに、テーブルというアイディアがあって始めて、テーブルをつくることができる。

つまりここで議論したいのは、そのようなアイディアはどのように作っていくものだろうか?という問いに近い。1 ~ 100の合計を求めるというのはどのように、コードへのアイディアが思いつき、コードに落し込むことができるのだろうか? (このようなアイディアを思いつく作業を、設計と呼んだりする人がいるだろうが、今回は敢えて、このような書き方をした)

これと似たような議論に「数学の解放というのはどうやって思いつくのだろうか?」それととても似ているように思える。私は、数学がある程度できる人間はプログラミングの素質があるように思うのだが、それは、このような思考ととても似ているからだと思っている。数学の解放を思いつくことと、プログラムで問題を解く思考はとても似ているように感じる。

さてどのように思いつくのだろうか?いろいろ説明してもいいのだが、結局のところいろいろ考えてみたらその思いついた というのが私の感覚で、なにか意識的にこうすれば、解法やアイディアを思いつくといことは無いように感じる。もちろん、「このような道筋をたどっていけば、このように思いつける」みたいなものはあるかもしれないが、結局のところそれは後付けであって、実際のところ何かひっかりやら、怪しいところについて考えていたら、そのアイディアが出てきたというのが正直なところ。

たとえば、先ほどに関しては、「1 ~ 100を足すのだから、沢山計算しないといけない。だから、ループ使おう。100回足すのだから100回ループ回す、その時のループカウンタを足していけば1~100の合計が分かるんじゃね?」みたいな思考になっているとする。でも、その思考はどこからやってきたのだろうか?。おそらくこの思考にならないから根本的にプログラミングができないみたいなのがあると思う。

もちろん、すごく大きい問題だったりする場合とかは、問題を分割したりみたいなことなことをやったりするものだ。しかしながら、根本的には、いろいろ考えたら行きつく先みたいなところであるように感じる。ループをこのように回して、if式を書いて、なんかデータ構造はこのようにして、と考えてみたいなものだ。

おそらく解法が思いつ方法がもしあったとすれば、そもそも世の中の問題は大抵解決するのであろう。しかし、残念ながらそのようなものはないと思っている。ただし、考えて、いろいろ試していけばいつか解答できるやろみたいな、そのようなものがプログラミングだって思っている。

アイディアを考えるとはどういうことか?

さて、いろいろ考えてたら、思考してたらできた。と書いたが、じゃあどのようなことを考えていけばいいのだろうか?というのが最後に思うことである。私はどのようなことを思考するのか。おそらく私はこの2点について考えていることが多いとおもう。

  1. 可能性について考える
  2. 問題について調べてみる

可能性について考えるとは

まず、私が思うところは、あるプログラミング言語機能だったりするものを学習したりするだろう。そこで、その言語機能について、いったいどんなことができるだろうか?という可能性を考えていくのでる。たとえば、ループという道具、if文という道具があったとき、どのように使うか?を考えていく。

ここでセンスがある人は、「いろんなことができそうだ!」とワクワクしだす。一方、センスがない人は、「でなにができるの?」とまったくそのことに関心を持たない。その違いがある。いろんなことができると考えた人は、おそらく既に、「あれもできる」「これもできる」といったことを自分で考えだす。センスがない人は、その道具で解ける問題でも、「いやこんなつまらない道具だと解けないでしょ」となって、その可能性を信じていないから解けないのである。

できれば、ある発明や技術などが出てきたとき、どんな応用例があるんですか?と聞く人ではなく、「こういったものに使えそう!どうですかね?」と提案できる、そういう人がプログラミングに関してセンスがあるように思う。

問題について調べてみる

2つ目は、結局のところ、問題に関して、どれだけのことを理解できるか?ということである。その問題について多くのことを知っていることが有利ではあるが、それだけでなくて、その問題からどれだけ多くの情報を引き出せるか?というのが大事になってくるだろうと思うのである。

問題についてひっかかりを調べていくと、類似の問題がみつかりそれで、ああこれを使えばわかるみたいなことがあると思うのだ。問題しらべていくとどうやら、いくつかの部品に分かれるのが分かったりとかそういうことだ。

どのようにしてプログラミングを学ぶべきか?

とまあここまで書いてきた。プログラミング的ゾンビに関心がありいろいろ考えをまとめるために書いてきたが、結局なぜそのような関心があるかといえば、私の経験とプログラミング言語についてやプログラミングの教育や学習についてここでいろいろ整理しようと思ったからである。

最後に、プログラミング学習について書いていこう。プログラミングはどのように学習すればよいのだろうか?プログラミング的ゾンビにならずに済むのだろうか?

私は以下の点が重要だと考えている。特に2つ目の点が重要だ。

  • プログラミングの基礎的な理解に努める
  • 自分の関心事についての課題について考えていく。自分のための問題を解いていく。

まずプログラミングの基本的な理解というのは、日々技術書を通読したり、だれかに教わったりして、「プログラムの書き方」を覚えていくことである。これは、そこまで問題にならない。(この時点で挫折する人はおおいのだけれども)

そして、そのあと通読した後、「プログラムの書き方」が分かった後が非常に大切である。自分にとっての関心事についてプログラミングで創造的に取り組むことである。なぜ大切かと思うのかというのは、まず自分自身の課題というのは大抵の場合、新しい問題の場合が多い。そして、自分自身のことだからこそ、興味をもって考えつづけられるということである。そして、問題を自分で展開ができる。自分自身の問題は再現がないのだ。それは次々に可能性を見つけられるということにつながる。

S.パパートは「構築主義」といったが、その学習についての理論は非常に正しいような気がするとふと思うのである。

最後に

ここまで書いたが割と不完全燃焼であり、私の考えていること、まとめておきたいことはすべてかけた、そして正しく書けたとは到底おもえない。しかしながら、今後ともこの内容に近いことがあれば、このにまとめていこうかなと考えている。