見出し画像

Retro-gaming and so on

Pythonで学ぶプログラミング書法

教えて!gooで興味深い質問があった。



プログラミング初心者の質問とは言え興味深い。
何故なら「変数への代入の必然性」を訊いてきてるから、だ。
別に僕がベストアンサー貰ったからって自慢の為に取り上げてるわけじゃない(笑)。
前にも言ったけど、僕自身は別にベストアンサーはどうでもいい、って思ってるタイプなんだ。

それはさておき、そこの回答で書いた通り、そもそも「変数への代入」ってのは「再計算を防ぐためにする」ってのが第一義だ。
従って、

my_hand = get_my_hand()
you_hand = get_you_hand()
hand_diff = my_hand - you_hand

なんつーのは僕の目には冗長に見える。結局、my_handもyou_handもhand_diffも一回しか使われてない。っつーことは使いまわし目的ではないと言う事だ。
確かにこの形式のプログラミング指南は良くあるが、個人的な意見では「関数を直接書きたくないが為の目的でわざわざ変数を作ってそれに代入する」なんつーのは愚の骨頂に思える。
そしてもっと言っちゃえば、この流儀は「構造化プログラミング」でさえない。ハッキリ言っちゃうと命令型プログラミングなんだよ。機械語やアセンブリ、古のFortranとか構造化BASIC以前のBASICでのプログラミングの考え方に近い。
実は意外と、命令型プログラミングで考えてる人が多い、と言う事を暗示してるわけだ。

まぁ、そこでは書かなかったけど、ぶっちゃけた話、こう、なんでもかんでも変数に代入すると言うスタイルは、例によってC言語脳が良くやるやり方だ。
そして、僕のスタイルってのは当然、僕が決めたスタイルではない。
一般に、(僕は違うけど)Lisperは必要のない代入はしない。そう、僕のスタイルはLisp由来のスタイルなんだ。

そもそも「代入」は破壊的変更なんで、Lisperはそれを好んで行わない。
そして、Lispのプログラミングスタイルは、「代入」を行う際、その書式がどうもリスキーに見えるんだわ。

(setq my-hand (get-my-hand))

とかな。
setq/setf/set!じゃなくって=だったらひょっとしたら代入するのを好む人が増えたかもしんない。でも、一文字(記号)じゃなくって四文字、ってだけで「気軽に代入しよう」って気がなくなる。意味も分からんし(笑)。
そして繰り返すが「破壊的変更」はLispでは好まれるスタイルじゃない。よって代入は「熟考の末」避けるのがデフォって事になる(※1)。

Lispの場合、「破壊的変更」の代表格である「代入」の代わりに「束縛」と言う概念がある。letで変数と何らかの計算結果を結びつける、と。
しかし、こっちはこっちで書式がペナルティ込みに見える、って事がある。

(let ((my-hand (get-my-hand))
  (you-hand (get-you-hand)))
 (let ((hand-diff (- my-hand you-hand)))
  (view-result hand-diff)))

いつぞや書いたが、Lispの場合、原則、束縛を重ねる度に、エディタ上でのコードのインデント、つまりネストが深くなっていく。
ネストが深くなるから可読性が落ちる、とは一概には言えないんだけど、一般的には「嫌だな」って感覚になる。「嫌だな」って感覚になる以上、「不必要な束縛をしない」と言うのがLisp流儀になるわけだ。
「なんでもかんでも代入する」C言語脳的な判断とは言えない判断と違って、Lisp経験者はLispを扱う事により、コーディングスタイルは自然と「抑制が効いた」モノとなる。
「そこで変数を束縛する必要があるのか?」と毎回問いかける事により、自然とLisp経験者は「不必要な代入を避ける」スタイルを身につけるわけだ。と言うより大掛かりなネストを嫌ってるが故、なんだけどな(笑)。
このLisp経験の故、Pythonコードを書く際にも自然と「不必要な代入を避ける」ようになるわけ。
僕だけじゃなく、Lisp経験者でLisperにならなかったヤツらは大体同傾向にあると思う。もちろん「Lispを触った事がある」って程度の浅いレベルではまた違うんだけど、「Lispでプログラミングした事がある」人はそうなるだろう、って話だ。

もう一回確認するが、そもそも「何らかの変数に計算結果を代入する」のは「再計算を防ぐ」為だ。「計算結果に名前を付ける為」じゃない。
いや、「可読性を上げる」為に「計算結果に名前を付ける」のもいいだろう。ただ、そこに必然性はないんだよ。
単なる「プログラムする側の自由」だ。「毎回毎回計算結果に名前をつけろ」と強制させるような謂れはないんだ。
特に、Lispをやった事があるヤツらは「名前を付ける自由」があるのなら「名前を付けない自由」もある、と思ってる。ここが決定的に違うんだよ。
何故ならLispは「関数でさえ名前を付けない自由がある」言語だからだ。そうだな、無名関数、あるいはラムダ式、と言うのはLisp出自だ。
Lisperは場合によっては「名前を付けない関数」まで多用する。

Java 8でラムダ式が登場したのは2014年で、登場当時、多くのJavaユーザーはその登場に混乱した。
もう10年も経つわけだが、いまだに「無名関数」に慣れない人が多い。
要はC言語脳スタイルの「なんでもかんでも名前を付けないと」と言うのは事実上破綻してるし、C言語脳にJavaScriptを習った層の中では「Javaよりも遥かに早く無名関数があった」にも関わらず混乱してる人は依然多い。
こういう「ニュービーにプログラムを教える」と言うテーゼで、既に「C言語脳が教える事」は役に立たなくなってるんだ。「C言語はプログラミングの基礎にはならない」と言うまたもや証明だ。C言語が機能が少ない為、「その知識だけで」プログラミングを教える事には既に弊害しかない。
一方、Lisperにとってはラムダ式/無名関数を使うのはあまりにも当たり前だし、「使いまわししない前提の関数なんざ名前が無くてもエエやん」って思ってる。
名前が無い関数は別に恐ろしいモノでも何でもない。必要がないから名前を付けてないだけで、C言語脳に侵された人たちが恐怖を感じるような「崇高な」ブツでも何でもないんだ。逆に「何故にそんなに使うのを躊躇するのか」分からない。マジで分からんのだ。

彼らは真剣にこう訊く。「無名関数なんていつ使うんだ」と。マジレスすると「いつ使う」もクソも「使いたい時使えばエエやん」ってのが答えだ。
ここでLisperは、例えば「変数に代入しない数値を一体いつ使うんだ」と問われるような違和感を感じるわけ。
例えば、

x = 1
y = 2
z = x + y
print(z)

と書くべきじゃないか?って訊かれたら困るだろ?これは実は構造的には上の質問に挙がってたコードと全く同じだ。1、2は「必ず」変数に代入して答えの変数zを用意してそこで改めてzをprintすべきだ、って言われたら「う〜ん、そうか?」とならないか。
つまり、そんな用語はないが、「無名数値」があって、「名前を付けない数値を一体いつ使うんだ」とかマジメに問われてるようなおかしさが、Lisperが「無名関数」に付いて問われてる時に感じるモノだ。
単にLisperは上のコードをこう書く方を好む。

print(1+2)

とは言っても、「これってフツーじゃん?」って思わないか?そう、Lisperは関数に対しても同じ事をしてる、ってだけなんだ。フツーの判断、だ。
Lisperは結果、変数だろうと関数だろうと「自分が名前を付ける必要性がある」って思った時には名前を付けるし、「付ける必要がない」って思った時は名前を付けない。
単にそれだけの話、なんだ。
僕が回答に「冗長だ」って書いた理由が何となく分かるだろうか。

そもそも「可読性」ってのはある種主観だ。「おっぱい大きな女と小さい女、どっちが好き?」って言うくらい主観と言えば主観だ。
原則として、自分一人でプログラミングするなら好きなようにコードを書けばいい。
一方、会社でコードを書く場合は、その会社での「コーディング規約」に従うべきだろう。そして会社Aと会社Bが同じコーディング規約を採用してるとは限らない。どっちにせよ「郷に入っては郷に従え」って事だな。集団でプログラミングするなら周りに合わせる必要がどうしても出てくる。
ただし、会社でコーディングする際に従う「コーディング規約」自体も主観だ、って言えば主観だ。上司の趣味に従わないといけない、なんて事もあるだろう。
いつぞやの余談で書いたが、上司が再帰が分からないから再帰で簡単に書けるコードを人間コンパイラになって繰り返し構文で書かなアカン、って事もあり得るだろう・・・そういう上司は中卒なのかもしれんが。
どっちにせよ「読みやすさ」なんつーのは人によって変わるのが基本だ。
だから自分でコーディングする際には「自分の好みがどの辺になるのか」と言うのはある種手探りじゃないといけない。

ただし、好み好み、っつってもやっぱ「理があった」方がいいだろう。
お題の「じゃんけん」だと、例えば命令的/ジャンクスクリプト的なコードとしてはまず



って書く方法が考えられる。全く関数を定義してない13行程度のコードで、お題のスタイルよか短い。しかし構造化されてないし、このスタイルで書いていくとメインテナンス性も悪くなる。
件の回答で例として挙げたのはこれだ。8行程度で済んでいる。ただ、そこでも書いたけど、プログラムは短く済めば済むに越したこたぁねぇんだけど、短すぎて意味の汲み取りが難しくなったらそれはそれで可読性が落ちてメインテナンスが難しくなる。
そして僕でも基本的にはここまで圧縮はしない。
可読性と冗長の間のバランスをどう取るのか。なかなか難しい問題だけど、僕の趣味丸出しだと、最適解はこうなる。
ただし、「趣味丸出し」と言いつつ、背景には一応「こうする理由」がある。



行数は16行程度。取り立てて短くもないけど長くもないだろう。
提示した中では最短の8行の約2倍の行数だ。ただ、「行数が増えた」最大の理由は「メッセージを分離してるから」だ。これは毎回書いてるが、メッセージを分離しておけば「メッセージを修正する際」にソースコードで修正箇所を探し回らなくて済む。そのために「敢えて行数が増えるのを覚悟して」メッセージを分離してるんだ。
プログラム本体はこのケースの場合はシンプルに仕上げてる。1行で済んでるのはたまたま、だ。それはここではじゃんけん勝敗判定アルゴリズムを採用してるから、に尽きる。アルゴリズムが分かれば全く知らないでコードを書くよかシンプルで分かりやすい可能性がある。それがアルゴリズムの威力、なんだ。
グーを0、チョキを1、パーを2、とした時、オリジナルのコードの通り、自分と相手の出した手の差で勝敗が決まる。
「あいこ」は両者共同じ手を出すんで差は0なんだけど、自分が勝ちの時は−1か2、自分が負けの時は-2か1になる。
つまり、実際の勝敗は5パターンあって、それら(勝ちの条件を纏めたり負けの条件を纏めるにせよ)に従って条件分岐せなアカンのはオリジナルのコードで見た通りだ。
ただし、その差に3を足して3で割った剰余だけを見ると、0の時は「あいこ」、1の時は「負け」、2の時は「勝ち」と3パターンにまで圧縮出来る。これがアルゴリズムだ。
そしてここで条件分岐を使ってもいいんだが、アルゴリズムの「結果」を直接、辞書型/ハッシュテーブルのキーにしてしまう(それを込みでmsgのキーを書いている)。どうせ「勝ち」「負け」「あいこ」と言う文字列を返さなアカン事が分かってるんで、「じゃんけん勝敗判定アルゴリズム」の計算結果をそのままキーとしてメッセージの辞書型/ハッシュテーブルから対応した値を持ってくればいい。簡単だろ?
ここでも書いたし、何度も書いてるけど、辞書型/ハッシュテーブル/連想配列を備えたモダンなプログラミング言語環境ではそれらを使いこなす事が重要だ。むしろ、条件分岐を書く前に辞書型/ハッシュテーブル/連想配列の類を使ったギミックに落とし込めないか毎回自問した方がいい。難読?慣れだ。C言語脳「だけ」がその形式に馴染めないだけだ。C言語脳の思考誘導には乗るな。

resultは関数だが、入力や出力はそれ自体には含めない。と言うか、関数は入力や出力を切り離すように書け。そしてとにかくreturnしろ。
例外は、例えばPythonだとinputprintを含んだ入力関数や出力関数を作る時だけ、だ(※2)。あるいはprintを用いたデバッグを行う時だけにしとく。
だから僕の趣味で言うと、オリジナルのコードのように、

def start_message():
 print('じゃんけんスタート')

def get_my_hand():
 print('自分の手を入力してください')
 return int(input('0:グー, 1:チョキ, 2:パー'))

とか、

def view_result(hand_diff):
 if hand_diff == 0:
  print('あいこ')
 elif hand_diff == -1 or hand_diff == 2:
  print('勝ち')
 else:
  print('負け')

みたいに無自覚に入力/出力関数を関数に含めるスタイルは失格だ。そしてこういうのもC言語脳が良くやるスタイルだ。
このお題のプログラム程度だと入力関数を作る程でもないし、出力もそうだ。
従って、最後にPython組み込みの素の関数を使って全部を「合わせる」程度で済む。
なお、

print(result(int(input(msg['prompt']))))

なんつー書き方はC言語脳だと眉間に皺を寄せる書き方だろう。
でもLisperにはヘーキな書き方なんだよな(笑)。引数に他の関数の処理をツッコんで行って連鎖させる、ってのはLispでは常套手段だ。特に「読みづらい」とも思わないし、それこそ慣れの問題だろう。
ただ、この辺は「Lispだけで良く行われる」けど他の言語で良く見られる、とは思えないんで、気に入らなきゃお題のように「変数に結果を代入していって」で書き換えてもいいだろう。俺はやらんが(笑)。その辺は任せるし、ハッキリ言っちゃえばこの程度は些事だと思ってる。

ちと纏めてみようか。

  1. 出力する文字列をprintには埋め込まない。出力用文字列は辞書型/ハッシュテーブルを用いて一纏めにしておく。
  2. 関数を書く場合は、出力や入力とは完全に切り離しておき、入力は引数を通じ、出力は返り値を通じて行うように設計する。
  3. 入力/出力用の関数を作る事は構わないが、目標とする計算ロジックを負わせないようにする。
主にこの3点を僕の趣味の「プログラミング書法」としてる。
そして、趣味とは言っても理由はキチンとある。メッセージを分離して一箇所に纏めておくのはメインテナンス性向上の為、そして出力や入力を切り離しておくのは関数型言語の手法で、一般に、入力や出力等の副作用を「計算ロジックに含めると」ゴチャゴチャしたコードになるから、だ。そして、入力/出力自体はコンピュータサイエンス上、計算ロジックじゃないんだ。
もちろん、これがベストとは思ってない。もっと良いアイディアがあれば「自分なりの」書法を組み上げればいいだろう。
先にも書いたけど、所詮「読みやすい」「読みづらい」は主観だ。一方、少なくともLisp由来の僕のスタイルは「そうしてる理由がある」。そしてC言語脳が書くコードみたいに、printfが散見しまくって、トータルのコードの流れがサッパリ分からん、とか言うのとは違うと思ってる。
もちろん、C言語のエキスパート達が書いたC言語のコードは美しいが、「そうじゃない大勢の人たち」が書いた、しかも「自分が大して詳しくもないPythonを使って初心者向けと謳って書いた本」で、何も「方針」を明示せず、ダラダラと思いつくままに書いたコードが「良いコーディングスタイル」になるわけがねぇだろ、ってこった。

余談だけど、コンピュータ・サイエンスは「プログラミング言語の文法さえ覚えればどんなヤツでもソフトウェアを不格好ながらでも何とか作れる」と仮定してるらしい。従って、ソフトウェアのパーツになる部分の書き方は教えたりするんだけど、一本のソフトウェアを作る一般的な方法論そのものに付いては基本語らないんだ(※3)。
このあり得ない前提のせいで、「一本のソフトウェアを作る一般的な方法論を知らない」学生が卒業する。せいぜい、単位を完了する為の卒業制作で一本ソフトウェアを作るくらいじゃなかろうか。つまり事実上、徒手空拳でソフトを作ってる。
面白いモンで、徒手空拳でもソフトウェアを書けちゃうヤツがいるんだよな。まぁ、天才の類だろう。そういう人たちはスーパーハッカーになってくわけだが、そうじゃない人たちは、まぁ、飯のタネを得た、ってくらいの話になる。
いずれにせよ、絵の描き方を学びに学校に来たけど、実際そこで行われてるのは「絵の描き方の方法論」を学ぶ事ではなく、絵の具の混ぜ方、とか筆の使い方、だけを学んで、一つ絵を描く事もほぼないわけだ(笑)。「選ばれし者」にとってはいいのかもしれんが、それ以外の人にとってはどうなんだ、って状態になる(笑)。
そして、コンピュータサイエンスの仕事にプログラミング言語設計の方策を練る、って事があるわけだが。コンピュータサイエンスでは先程書いたように「ソフトウェアの作り方を教えなくてもソフトウェアを書く事が出来る」と言うとんでもない前提があり、そしてその(存在しないけど完成した)ブツを「どう分割するか」と言うのを研究してる。
言わば言語仕様で「どう分割するか」を自然と内包しよう、とするわけだよ。
ところがこれが市場に出ると全然上手く行かないわけだ(笑)。
例えば歴史的には「完成した(と言う想定の)プログラム」は「関数」で分割可能、とした。小さいパーツである「関数」が使えれば、ユーザーは全員「キレイに」あらゆる部品を関数で書き、キレイなソースコードになり、メインテナンス性が向上するだろう、と。
ところが起こった事は「指針がないガラクタのような関数群の塊」だった。「(想定した)プログラムは関数で」確かに分割されてたけど、想定通りにメインテナンス性が向上したわけでもない。起きた事の殆どは、「指針が無く」分割された(想定した)プログラムの断片化したブツだったんだ。
これはマズい、とコンピュータサイエンスの人達は思ったわけだ(笑)。「思てたんとちゃう」と。
「関数を分割の単位としたのが間違ってたんとちゃうんか」となったコンピュータサイエンスの人々は、「じゃあデータを基準にして分割する方法を提示すれば?」となった。これで出て来たのがオブジェクト指向だ(※4)。
ところが、「データ基準で」プログラムを分割するのは「関数基準で」分割するより難しかった。直感的に記述が出来ないし、何より初心者に対して敷居が高い。一体何が起きてるのか把握するのは「関数で分割する」より難しかったんだ。
「とても流行るとは思えなかった」アイディア、オブジェクト指向に神風が吹いた。Javaの登場だ。Sun Microsystemsの営業努力より、「これからのプログラミング言語としてJavaを採用すべき」と言う雑誌記事等が氾濫した。結果、Javaを通じて「オブジェクト指向」は広まった。
でも出て来たのは「ジャンクのようなデータの残骸だらけ」だったんだよ(笑)。
こりゃアカン、ってぇんでデザインパターンの紹介、なんつーのも出て来たんだけど、もう大体分かるだろ。そこは本質的な問題じゃないんだ。
包丁で肉の解体は出来るだろう。でも包丁があるから、っつって「人間が全員」肉を解体出来るたぁ限らない。包丁の存在と「肉の解体法」は別問題で、「肉の解体法を知ってるから」包丁が活用出来るわけだ。
言っちゃえば、コンピュータサイエンス系の人間が間違ってるのは「肉の解体法を伝えずに」、「包丁があればどんな人間でも肉が解体出来る」と言う前提で「持てば勝手に肉を解体出来る」包丁を設計しよう、としてる辺りなんだ。
「ソフトウェア作成の一般論」を伝えずに「ソフトウェアの分割法を研究してる」ってのは何ともヘンな話だ。

関数を使ってソフトウェアを「分割」構築する、ってので思い出すのが星田さんが最初の頃書いてたこれだ。

はっきり言って関数を使うのも初めてなので、どういう時に関数を使うべきなのかがはっきりしないんですよね。

本人も恐らく今なら「何で悩んでたんだろ?」ってなってると思うが(笑)、実際プログラミング初心者はほぼ全員「同様の不安」を抱えてると思う。「ソフトウェアは関数で分けて書けばいい」って言われても、コツと言うか指針と言うか、ノウハウがなければそこに踏み出せない。当たり前だ。
幸いな事に僕は悩んだ事はなかったんだけど、それは僕がデキが良かったから、でも何でも無く、単純にプログラミングをやりだした総初期には数学的スクリプトしか書いてなかったから、だ(※5)。印象と違って、いつぞや書いた通り、数学的な某は実は書くのが簡単だ。「ソフトウェアを作る」なんて事も想定してなかったんで、「言われた通り」関数をシコシコ打ち込んでたわけだ。
そして数学である以上、数学上の関数と書くコード(関数)は1対1対応なんで悩む要素なんて丸っきり無かったわけだ。
言い換えると、星田さんは最初っから「難しい議題に取り組んでいた」わけで、反面、僕は、数学的スクリプト、と言うぬるま湯の中にいて「関数を書きまくってた」わけだよな。それが違いだったわけだ。
しかも、星田さんが特に優秀だったのは「ロードマップを作る」才覚に恵まれてた辺りだろう。多分、プログラミング初心者で「目的とするブツを作るまでに至る道」を想定出来る人って殆どいないと思う。
言っちゃえば、星田さんがかなり特殊な人で、「プログラミング初心者がプログラムを完成させるまでに至る道を想定出来る」と思ってるコンピュータ・サイエンスの仮定ってのはそもそも間違ってるんだわ。


星田さんがプログラムを完成させるまでの必要事項を手書きしたロードマップ。
こんな事が出来る「プログラミング初心者」はほぼいないんだけど、コンピュータサイエンスは「人類全員が星田さんだ」と言うとんでもない仮定をしてる(笑)。

コンピュータサイエンスは「一般論としてのソフトウェアの書き方」を語らない。従って、プログラミング言語の「使い方」を学んだあと、ユーザーは例えば掲示板の書き方を学びたかったら「掲示板の書き方入門」なんかの書籍を買って読み、ゲームの書き方を知りたかったら「ゲームの書き方入門」なんかの書籍を買って読まなきゃならない。言っちゃえば場当たり的なわけだよ(笑)。
「一般論としてのソフトウェアの書き方」を教わってないから、例えば初学者向け「Python入門」なんかの本でもそのテの本の著者は中で「文法解説」に終始して、実際のソフトウェアの書き方を教えないわけだ(笑)。だって「一般論」を知らないわけだから。「具体的なソフトウェアの作り方に付いては別途専門書籍を読んでくれ」としか言いようがないわけ。
その辺、Web上での「プログラミング学習サイト」でも同じだろ(笑)?ぶっちゃけ、この辺は「プログラミング学習」なんじゃなくって「プログラミング言語学習」なんだよ(笑)。プログラミング言語を学べても「具体的なソフトウェアの書き方」は一切分からんわけだ。何故ならそもそもバックグラウンドの「コンピュータサイエンス」がそうだからさ。
小説の書き方を学びたい、って思って学校へ行ったら日本語入門だった、的なアレになってる(※6)。

さて、何度か書いてるが、コンピュータサイエンス上で唯一の例外として「ソフトウェアの書き方を指南」してる本は、ぶっちゃけ、ANSI Common Lispでしか出ていない。
エリック・レイモンドと言うハッカーは、

 LISP を勉強しておきたいのは、別の理由からです――それをモノにしたときにすばらしい悟り体験が得られるのです。この体験は、その後の人生でよりよいプログラマーとなる手助けとなるはずです。たとえ、実際には LISP そのものをあまり使わなくても。

と書いている。
悟りとは一体何だろうか。
ぶっちゃけ、現代ではLispを使うメリットはそんなに無いと思う。かつて「Lispが唯一持ってた機能」なんつーのは、PythonやRuby、あるいはC++、JavaやRustにパクられたりしてる。Lispでなんかマジメに書くより、人気のあるこれらの言語のライブラリを使った方が物事を早く成し遂げられるだろう。
しかし、だ。
Lispを学ぶのはLispを学ぶ為じゃないんだ。ヘンな言い回しに聞こえるがこれが事実だ。
以前、「どのプログラミング言語を学ぶか」と言うのは大して重要な問題じゃない、と書いた。そうじゃなくって「良書があるか否か」が重要なんだ、と。
そして知ってる限りで言うと、「ソフトウェアの書き方の一般論」に付いて書かれてるのは、ANSI Common Lispで書かれた本しかない、んだ。

 
 
上で紹介した二冊は一貫したパースペクティヴ、つまりRead-Eval-Print Loopこそが「ソフトウェアを書くキーだ」とハッキリと述べている。
こんな本は他の言語にはない。
んで、言っちゃえば、これらが「悟り」の正体だ。他の言語だと、文法を学んでも、ソフトウェア自体は「徒手空拳」で作らないとならない。
一方、これらの本で学べば「一貫したパースペクティヴ」で「ソフトウェア作成」と対峙出来る。つまり、これらの本には開発方針が詰まってるんだよ。
そしてそれが「徒手空拳組」との差を生む。そう、それが「悟り」なんだ。
Lispでは60年にも渡る「ソフトウェア開発ノウハウ」がある。それを学ぶのを「悟り」と言わずして何と言うのか。
結果、Lispを学んだ人で、ハッカーが多いのは当然、となる。
特に前者の本は古典的な人工知能に付いての本だ。これも前に書いたが、古典的な人工知能、と言うのは後のテキスト処理のプログラミングでの礎となっている。現代でのあらゆるプログラミングでの礎と言っていい。
PAIP(実用Common Lisp)に付いての評は以前ザックリと書いたんでそれを参照して欲しい。
ホント、マジメに「ソフトウェアを書いてみたい」って人はこの本を読むべきだ。そしてANSI Common Lispを使い続けてもいいが、現代のモダンな言語(Python、Ruby、Rust等)を使う際にも役立ってくれる筈だ。
ANSI Common Lispを使ったこれらの本は、C言語で学ぶには時間もかかるし苦労する地点へとアッサリと貴方を誘ってくれる筈だ。
マジで。

閑話休題。
長い寄り道だったが本題へ戻ろう。
と言うより、もうREPL(Read-Eval-Print Loop)の話をしたから分かるだろう。
上で挙げた「僕の趣味丸出し」の書法で書いた「じゃんけん」プログラムのソースコード。
これはLoopが無いだけで、事実上、ReadとEvalとPrintの3つを組み合わせたモノだ。
言い換えると、この時点では単なる「一回起動して実行したら終了する」じゃんけんスクリプト、なんだけど、Loopを組み合わせれば「キチンとしたじゃんけんゲームのソフトウェア」になる、って事だ。



あるいは、端末でプレイしたいなら次のようにしてもいい。




実際は、このままでは無限ループするだけ、なんでまだ全然ダメダメなんだけど(※7)、意図は分かるだろうか。Lispが教えてくれた僕の「プログラミング書法」は、プログラムを拡張しやすいようなスタイルになってるんだ。
いや、逆に言うとREPLと言う、言わば最終ゴールから逆算したカタチになってる、っつって良い。
いずれにせよ、欲しいのは一貫したパースペクティヴだ。
「何故こういう形式で書くのか」がハッキリしてんだろ?「理」があるんだよ、ここには。
拡張も同様の路線でやっていく。例えばどっちかが五敗したらゲームを終了する、とする。プレイヤー毎の「敗数」をカウントする構造体が欲しい。
そうすれば例えば次のような実装に改造する。



ここで初めて「代入操作」が出てくるが、理由は「使いまわしする」のが明らかだから、だ。
ちょこちょこ改造が加わってるが、それでも「プログラミング書法」と言う形式自体は最初の段階から変わってない、って事が分かるだろう。
なお、条件分岐を三項演算子的に書いてるがそこは趣味だ。
ただし、少々解説しておく。
フツーのプログラミング書法だと、例えば上の例でのreadはこう記述される。



ところが、この構造を良く見てみると、条件節があっても、条件に従って返すブツの構成要素が殆ど同じなのが分かるだろうか。違うのは辞書型msgに与えるキーが違うだけで、殆ど同じ物を返してる。つまり、タイピングで殆ど同じモノを二回タイプせなアカンって事だよな。
タイピング量を節約したいの法則だと、この部分は実はこう書けばスッキリする筈だ。



違うのは辞書型に与えるキーである'continue?''prompt'だけなんで理論的にはこれでイイ筈なんだよ。
ところが、Pythonの構文上これは許されないし、Pythonみたいに許してないプログラミング言語も多い(※8)。
反面、Lispならこういう書き方を許容する。何故ならLispでは条件分岐は値を返すからだ(※9)。

Pythonで似たような事をしたい場合使うのが三項演算子、ないしは条件式だ。「式」と言うからには値を返すので、辞書型/ハッシュテーブルのキーを「演算して」与える事が可能となる。
個人的には、この方法には明るくなるべきだと思う。実際、「難読だ」と言うのはC言語脳だけだし、「理論的に考えると」むしろ意味はより明解だろう。
あとは「慣れの問題」だ。
C言語脳が「三項演算子」を難読だ、使うべきではない、と言うのは、単にC言語的な構文上難解に見えるだけ、って話であり、別にPythonではそこまでじゃないだろ。そして「C言語では非推奨」は「別言語で非推奨」にはならない、んだよ。当然だろ。
なお、星田さんがLispをやりだした時、この「三項演算子的な」書き方を自然と行っていた。星田さんがアタマがいい、ってのもあるんだけど、むしろこういう「やり方」はプログラミング初心者の方がやっちゃう確率が高いんだ。それで、例えばC言語コンパイラとかに怒られて「何で?」とかなっちゃう。
言い換えると、この三項演算子的な書き方は、「プログラミングに慣れてない人」の方が思いつきやすく、プログラミングの学習で「不自由な言語」を用いると、「豊かな発想」が矯正ばっかされてく、と言う良い例になってると思う。
いずれにせよ、ここでもC言語脳的なサジェスチョンには従うべきではない。彼らは矯正されまくってるんだ。去勢、っつっても良い。

これで一応ロジックとしてはプログラムは完成、なわけだが、自分で「不正な入力」をしてみて、どんなエラーが飛んでくるのかメモろう。あとはそれら「例外」に対して「例外処理」でトラップを仕掛ける。
これが例外処理を含んだ「完成した」CLI版のプログラムとなる。変数に「代入」する際も、必然性がある時にやってる、って事が分かるだろう。

そして、この「プログラミング書法」のままで、GUI版にする事も出来る。
詳細はここでは説明しないが、例によってMVC(Model-View-Controller)で構築する事が出来る。



GUI版一式はここに置いておこう。
いずれにせよ、「プログラミング書法」をキチンとしておけば、ワンパターンでプログラムが「修正/拡張出来る」と言う例だ。
プログラミングと「プログラミング書法」は全く違う概念だけど、自分で「特定のフォーマット」で書く、と言う「ルール」っつーか「決まり」を決めておけば、プログラムを「育てていく」と言うのも比較的簡単になる。
そういう「考え方」を是非とも参考にして欲しい。

※1: Lispでは代入により変数を「破壊的変更」出来る。「破壊的変更をしない」と決定するのは、Lisperの言わば良心に支えられてる。
一方、言語仕様レベルで「変数の破壊的変更を禁止してる」プログラミング言語も存在する。代表的なトコがRustだ。
Rustの場合、mut(mutable=可変、の意)キーワードを使えば「破壊的変更可能な変数」を宣言出来るが、これも記述コストの例で、Rustの設計者は

「プログラマが変数を破壊的変更しないプログラムを書く」

事を望んでる表れだ。破壊的変更さえ目論まなければ、mutと言う字数が増える事を書かんで良い。
従って、C言語的に「変数を破壊的変更しまくる」mut使いまくりのプログラムを書くとしたら、それはRustの設計者が望んだスタイルではない、と言う事になるだろう。
C言語脳はRustではmut使いまくりのプログラムを量産する可能性が高いので気をつけよう。
また、どうしたら良く分からんかったらletを使いまくるのがテかもしんない。
なお、そこの例で言うシャドウイングは、RacketなんかでのLispじゃ

(define (main)
 (let ((x 5))
  (let ((x (+ x 1)))
   (let ((x (* x 2)))
    (display (format "The value of x in the inner scope is: ~a~%" x)))
   (display (format "The value of x is: ~a~%" x)))))

となり、letは必ずスコープを引き連れるので意味は明快になる。
ただし、そのせいでエディタ的にはインデントが重なり「ネストが深くなる」んで、「あまり気持ちの良いプログラムではない」と言う思いも強くなる(まぁ、所詮「例」だが)。

※2: 入力や出力は「計算」ではない。コンピュータサイエンス上、これらの機能は「計算」のようなコンピュータの本来の使用法ではなく、あくまで副次的なモノ、つまり「副作用」の一種と捉える。
関数型言語の考え方をベースにしたセオリーだと、副作用目的の関数と言うのは「アリ」なんだが、それはそれとして本体から切り離しておいた方が無難だと言うこと。つまり、入力なら入力を目的とした関数、出力なら出力を目的とした関数を作るに留めておくべきで、それと「計算ロジックを一緒にはしない」事だ。

※3: 大学で教える「完成品としてのソフトウェアの作り方」はインタプリタとコンパイラくらいだろう。
しかし、何度も言うが、「すべてのソフトウェアの基本はインタプリタだ」とは強調しないようだ。どっちかっつーと構文解析の方法論とか、枝葉末節な話が主題だ、と「勘違い」してるらしい(それはそれで大事ではあるが)。

※4: もちろん、他にも要因があるが、ここではこれで行く。

※5: 個人的には、元々、「数学的処理をやりたい」ってのがまずあって、数学的スクリプト専用の特殊な処理系、MATLABクローンのGNU Octaveを使ってた、って事があった。汎用プログラミング言語を使ってたわけじゃなかったんだ。
こういう「数学専用」のプログラミング言語処理系は、エクセル関数組にはかなり分かりやすい。

GNU Octave。行列演算等に特化した数学的スクリプト専用のプログラミング言語処理系。

※6: そういう意味だと、特殊なプログラミングとは言え、「就職斡旋」込みのゲームプログラミングを教えてる専門学校の方がマシかもしんない。「作るものがハッキリしてる以上」それに関するテクニック込みで「ソフトウェアの作り方」が学べるだろう、からだ。
また、日本のゲームプログラマが凄いのは、ゲームデザイナの「無茶振り」でも、力技込みで実現する技術力だ(笑)。コンピュータサイエンス的には「不正解」だろうと、彼らはとにかく「デザイナの要求」に応えようとする。
すげぇ。
ぶっちゃけ、個人的には彼らが一番超人だと思ってる。

※7: 端末等で「無限ループを強制終了」させるにはCtrl-C(つまりCtrlキーを押しながらCを叩く)する。
この辺はLinuxだろうとWindowsのDOS窓だろうと共通してる。

※8: 「許す」か「許さないか」と言う以前に、「それが間違ったアイディアなのか否か」と言う事が問われるが、このケースではそれは「理論的には間違ってない」と言う事になる。
と言う事は、何らかが「許されない」場合、それは単に、あるプログラミング言語の「実装上の判断だ」ってだけの話だ。ユニバーサルな判断ではない。
そして、あるプログラミング言語Aで禁止されてるから、と言って別のプログラミング言語Bで「行えない」わけじゃないんだ。
いつぞや書いた話だが、プログラミングを学ぶと言うのは「これが出来る」と言う事を学ぶと言うより「これが出来ない」事を学ぶ比重が実は高い。
結果、「殆ど自由度がない」(C言語のような)プログラミング言語でプログラミングを学び始めると「あれもこれも禁止」に思えてしまって、最初から視野が狭まる、と言う事になる。
それがC言語脳を形作るんだ。

※9: RustもLispのように「条件分岐は値を返す」ようになってて、極めてモダンだ。
構文の見た目はC言語に寄せてるが、Rustはかなり関数型言語の「機能」を、ヘタすればJavaScriptより多く含んでる。
従って、Rustも「C言語のように教える」のは間違った言語、となる。
Javaとは違うのだよ。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事