さて、前回「プログラミング書法」と言う考え方を紹介したわけだが。
僕のスタイルを紹介したけど、繰り返すが、あれがベストだとは限んないだろう。
僕自身は「ベスト」だと思ってるけど、他にもっと良いアイディアがあるかもしんない(※1)。
いずれにせよ、「ソフトウェアを組み上げる」為に「どうパーツとして分割して書いていくか」が「プログラミング書法」を構築する着眼点で、そしてどうしても「使ってる言語の構文」の影響もある程度受けざるを得ない。
しかしながら、自分に「ルール」を課して、決まったフォーマットで書いていく、と言うのは重要なアイディアだと思う。
ワンパターンでいいんだよ。むしろ「パターン」を見つけられない方が徒手空拳で大変な事になる。
加えると、オリジナリティが必要なのは「ソフトウェアのアイディア」の方であって、「どう書くか」なんつーのにオリジナリティは必要ないんだ。
さて、このブログでは毎回毎回C言語脳っつって「C言語はすべてのプログラミングの基礎である」と言った世迷い言を攻撃してるが。
何度も書くが、「C言語のエキスパート」の事を言ってるわけじゃない。「それ以外のヤツら」で、特にプログラミング教育に下手に関わろう、とかやってるヤツらの事を攻撃してるわけだが。良く知りもしないのにPython入門系の本を書くヤツらとか、だよな。
でもこのブログ周辺だと、実はC言語使い、ってのはあんまいないわけだよな(笑)。だからこそ平和でいられるんだけど(笑)、一方、例えば「C言語脳が書くスタイル」ってのが「どう酷いのか」と言うのにピンと来ない人も多いと思う。
丁度都合よく、教えて!gooのC言語カテゴリにサンプルとなりそうな例があったんで、ちと見ていってみようか。
質問はこんなんだ。
ハッキリ言おう。この質問は宿題の丸投げだ。言っちゃえばこの質問者はもはや自分で考える気が全く無い。
いや、それはいいんだよ。どうしてもプログラミングが「出来ない」タイプがいる。人には向き、不向きがあるんで、努力して勉強すれば何とかなる、なんてモンでもない。さっさとこのコースを「ドロップアウト」して、別の事柄を勉強した方がいい。
そういう「向いてない人」が良く現れる、のが教えて!gooと言うサイトだ。
まぁ、質問自体も投げやりで、ぶっちゃけ、この投稿はマルチポストなんだけど、そもそも関数one-nineteenとかtwenty-ninetyなんつー関数が「どんな関数を指してるのか」サッパリ分からなかった。
忖度を要求する最悪の質問者だろ(笑)?
投げやりだ、って言ったじゃないか(笑)。
んで二回目の投稿でやっと関数「One_Nineteen」の概要が分かる。相変わらずtwenty-ninetyが何を指してるのかサッパリ分からんが、その「例示された関数」を見るとこれがゾッとするんだよ(笑)。
見てみる?
これだ。
うっわ〜〜、ってなんないか(笑)?俺はなった(爆
これしかも宿題だろ?宿題で使う「関数」ってことはテキストかなんかに載ってるんだろうけど・・・あまりに酷い。
そうなの、まるでC言語脳養成コード、って言うような酷さだ(笑)。言った通りだろ?printf塗れだって(笑)。
プログラミング初学者にキレイなコードを提示しよう、って気概もクソもない。
いや、こんな酷い教材で勉強してるのならC言語脳ってゴキブリのように湧いてくるわ、って納得のクオリティだ(笑)。
分かるだろ?「C言語脳が良く知らずに書いたPython入門書」の元ネタみたいなのはこういうカンジなんだ。
仮にこのような(不必要な)出力噛ました関数を書くにせよ、Lisperだったらこんな風に書かない。と言うかこういう「みっともない」スタイルで書かなきゃいけないような脆弱な構文をLispは持っていない。
Lispだったら・・・ここではRacketを用いるが、この部分だとこう書いちゃう。
もう何度も同じ出力関数(C言語ではprintf、Racketではdisplay)なんざ書きたくねぇだろ?でもこのテキストじゃそれを強制してるわけ。んなバカな!って思うんだけど、なるほど、この方策だとC言語だと構文に自由度がないから、バカみたいにprintfを書きまくらないとならない。
RacketのようなLispだと、前回見たとおり、三項演算子的な記述が可能で、displayの引数にcaseと言う条件式をぶち込む事が出来る。
言い換えるとdisplayと言う出力関数の引数に条件式を取れる、と言う事だ。
これは、フツーのプログラミング言語では殆ど無いケースで、前回見た通り、やるなら条件分岐ではなく三項演算子と言う特殊な記法に頼らないとならない。
ところが、この方式で記述出来る言語が現れた。Rustだ。こいつは見た目C言語に寄せてるが、C言語じゃ不可能な、Lisp的な事をやってのける。
つまり、お題の「C言語じゃみっともない」プログラムをLisp的にこう書くことが出来る。
ご覧の通り、println!マクロの第2引数に条件分岐をツッコむ事が出来る。つまり、C言語のようにprintln!塗れにせんでもエエ、っちゅうこっちゃ。
厳密に言うと、RustのmatchはC言語のswitch文のような条件分岐文ではない。これはもっと強力なパターンマッチ式だ。
しかしながら、いずれにせよ、Rustのパターンマッチは「返り値がある」式だ、と言う前提で、お陰さんでC言語脳的な無駄な記述を減らす事が出来る。Rust様々だし、C言語のようにRustを書くべきではない、と言う一例だ。
一方、この辺、Pythonは弱い。そもそも、Pythonにはswitch/caseが無いし、これらは文であって式ではない。パターンマッチもそうで、結果、printの引数としてこれら構文を突っ込めない。
結果、三項演算子を突っ込まなアカンのだが・・・・・・。
うん、いくら何でもこれはアレだろ、って僕でも思う(笑)。意味自体は分かるけどな。
しかしそもそも、switch/caseに節を20近くツッコむ、って方法論自体が間違ってるんだよ。そんなん書きたいか?俺はやだね(笑)。どう見てもアタマの悪いプログラムにしかなりようがない。
しかし、アタマの悪い方法論をやらせよう、ってのがまさしくC言語脳なんだ。
分かる?
これは本来、条件分岐の問題じゃなくってデータ設計の問題なんだよ。
と、こう聞いて、
「ハッシュテーブル?」
って考えた貴方は偉い。違うけどな(笑)。
これはもっと単純な、リストや配列を使うのが正解なの。何故ならリストや配列は要素番号がある。要素番号を利用するんだ。
つまり、Pythonだと、
こう書くのが正解で、Racketだとこう(※2)。
そしてRustだとこうなる。
Rustの、これはまぁ、欠点とは言えない欠点なんだけど、この程度の「スクリプト」を作る際にも「キチンとした」プログラム構造を要求する。
そもそも、無いわけじゃないけど、Rustは設計上、大域変数を排除しようとしてて、使えないわけじゃないが、記述コストがかかるようなカンジになってる。
つまり、Rustの設計者が「大域変数を頼るようなプログラミングをしないで欲しい」と思ってる、って事だ。
よってそれに従おう。
結果、配列はmain関数内に書き、関数one_nineteenは第2引数に配列を取るような「本格的な」スタイルになっている。
まぁ、ロジック自体は根本的にはPython/Racket版と同じだ。そして「気楽に」スクリプトとして書けないようになってる辺りは、これは言語の方向性の違いだろう。
Rustはやっぱりシステムプログラミング向けを想定したプログラミング言語なんだ。
いずれにせよ、ここで挙げたどのプログラミング言語でもそうだけど、まずは「解」を条件分岐に頼らせるような事とせず、データ型(具体的にはリスト/配列)にしてしまって、そこから「解」を検索するようにしてる。
なお、第0要素が""(空文字列)になってるのは、問題の仕様が0に対応する"zero"を要求してないからで、言っちゃえば「要素番号」と「アルファベット表記」が対応するように第0要素に空文字列をセットしてるんだ。
これは何もトリッキーな手法じゃない。ある意味普遍的なプログラミングテクニックなんだ。
敢えてモダンな言語の立場で言うと、条件分岐を排除出来るようなら排除した方が良い、ってこった。それより重要視するのはデータ構造の設計だ。データ構造の設計さえ上手く行えればプログラムの本体はシンプルになる。
この観点から言うと、関数twenty-ninetyなんかの第一バージョンも次のようなカンジででっち上げる事が可能だ。
Python:
Racket:
Rust:
さて、本来のお題から言うと、例えば78、とか受け取った数値をseventy-nineとかに直したいわけだよな。
これには何が必要なのか。単純に言うと78を10で割った商と余りが必要だ。78を10で割った商は7、78を10で割った余りは8だ。このケースだと商である7は配列/リストtwenty2ninetyの要素番号に使え、余りの8は配列/リストone2nineteenの要素番号として使える。
そして、数xが20以上か20未満か、で関数one-nineteenの挙動は変わるわけだ。与えられた数xが20未満だったら配列/リストone2nineteenから要素番号に即した文字列を返せばいいんだけど、20以上だったら数を10で割った剰余が頼るべき相手だ。
以上の条件よりRacketだとこうなる。
繰り返すが、Lispの条件分岐は式なんで、「リストの要素番号を指定する」場所に条件式をツッコんで構わない。
そして、Lispでそれが可能だ、と言う事はLispや他の関数型言語から機能を借りてきてるRustでも同様の事が行える、と言う事だ。
この、配列の要素番号として「条件分岐」をツッコんで構わない、と言う事をC言語脳は教えてくれないだろう。何故ならC言語だと(三項演算子があるにせよ)ご法度の記法だから、だ。
しかし、繰り返すが、構文上の見た目はさておき、実質的にはRustの構文はLisp等の関数型言語に近いんでこういった事が出来る。意味も特に難解じゃない。C言語脳がこういったテクニックを「知らん」だけだ。
そしてRustの設計者は「これをやって構わない」と思ってるから、こういう機能を載せてるわけだよな。
分かる?「C言語脳が解説したようなRustの説明」だとRustの「構文上の旨味」を取り逃した上に洗脳されかねない、と言う事を。
「見た目がC言語に似てるから」と言って「C言語に合わせて書く」必要性なんざ全くねぇんだ。
さて、これら3つの言語の中で、Python「だけ」は条件分岐は保守的だ。あくまで「文」であって「式」ではない。
よって、同じ事をPythonで行いたい場合は、三項演算子的な記述法を持つ条件式に頼らないとならない。
さて、例えば78と言う数値が得られた場合、seventy-eightと間にハイフンを挟んだカタチで表示したい。
一方、例えば80って言う数値が来たら単にeightyと表示させたいわけだよな。
つまり、関数twenty-ninetyに於いて、与えられた数値xの剰余が0だった場合はハイフンを付けたくないが、そうじゃなければハイフンをつければいい。
もう一つの条件は、与えられた数値xが20より小さかったら関数twenty-ninetyの出番はないわけだ。つまりその時にはハイフンもクソも登場しない。
Python:
Racket:
Rust:
Pythonの場合、条件式に含む返り値がどこまでなのか、と言うのを明示する為に、カッコを使ってそこを括らないとならない。カッコが無ければこのケースだと、条件を満たした場合の返り値が Twenty2Ninety[x // 10] + '' 全部、と誤解されるだろう。
RacketとRustはR同士で(違)殆ど同じだ。Lispだと当たり前なんだけど、Rustで文字列を「条件分岐が返した文字列」と直接連結出来る、なんて事を想像出来なかった人は意外と多いんじゃないか。
これも「C言語的には滅茶苦茶に見える」記法なんだけど、「Rustの条件分岐は値を返す」と言う「理屈」をキチンと知ってれば取り立てて驚くような事でもない。
このように、Lispを含めた関数型言語だと「割に当たり前の機能」をRustは持ってるが、C言語脳は果たしてこれを理解してるのか。そして理解してないとしたら、彼らは「そういう機能がC言語に無い」と言う根拠1点だけで「難読だ」と騒ぎ出すんだ。
繰り返すが、もはや現代のモダンな言語では「C言語はプログラミングの基礎」にはなり得ない。むしろ「C言語的発想」は邪魔なだけで、そしてモダンな言語はどうしてもLispを含む関数型言語に近づいて行ってるんだ。
最後に、この3つの言語で書いたプログラムを端末で動かせるスクリプトとして完結させよう。2つのプログラムファイル名をそれぞれint2english.py(Python)、int2english.rkt(Racket)とする。
Rustの場合は、cargo new int2english としておいて、main.rsファイルはそのディレクトリのsrcフォルダ内に自動生成されてるもの、としよう。
Python:
Racket:
Rust:
どれにしても、端末上でコマンドライン引数として入力した0 < x < 100の数値xを英語として表示してくれる。
さて。
「アタマの悪い」C言語脳的プログラムに対し、Lisp/Python/Rustの構文の優位性を説きながらここまで論を書いてきたんだけど。
実際の話、C言語でも、この程度のプログラムだったら似たような形式でプログラミングする事は可能だ。
例えば僕が書いてみたC言語でのコードはこのようになる。printfなんかも使いまくってない。
(僕は違うけど)C言語のエキスパートも似たようなコードを書くと思う。ロジック的にはそっちの方がスッキリしてマシだから、だよ。
でも、このテキスト/宿題ではトンマな・・・うん、だから「アタマの悪い」プログラムを学生に書かせようとしてるわけだよな。
何でだろ。
考えられる理由として1つ目、はだ。プログラミング初心者へのswitch文の練習、って事が考えられはするんだけど・・・。
でもよ、練習で、と言っても20も節を書かせる、とか物凄くバカ臭くねぇ?もっとシンプルな練習でイイと思うんだわ。20も節を書かせるようなプログラムを組むハメになったらフツーならどう考えても「手抜きして」データ型設計した方がマシ、って判断になる筈だ。無闇な苦労をさせる、とか意味が分からん。
2つ目、はだ。何度か言ってるけど、実はC言語には文字列がない。
まぁ、事実上、C言語では char* と言うデータ型は文字列として考えていいんだけど、この"*"ってのがポインタだ。そして多分この宿題のレベルで言うとカリキュラム的に学生たちはポインタまで進んでない。
でもよ。だとするとだ。要はこの「数値から英語への変換」なんつーお題はこのレベルでは難し過ぎるんだよ。キレイにならないプログラムが前提のお題なんつーのは百害あって一利無し、だ。
きったねーアタマの悪いプログラムを書く事を学生に強制すべきじゃねぇんだわ。まだ材料が揃ってないのなら「難しい」課題を学生に出すべきじゃねーだろ、ってな話だ。カリキュラム進度とお題の難易度が合ってない。そしてプログラミング初心者向けの講座でC言語が導入され出したのが1990年代前半だったと思うんだけど、30年近くも経っていて「教育法」が確立されてない、ってのが驚くべき現実なんだわ。
前回も書いたけど、「プログラミング書法」とか「スタイル」ってのは思ったより重要なんだ。void型の関数でprintf塗れの汚ぇ「悪いスタイル」を最初にやっちゃうと、これを矯正するのは並大抵じゃない。言っちゃえば、変態プレイで調教されまくった女みてぇになっちまうんだっての(笑)。
いや、個人的にはどスケベな女は大好物だけどな(笑)。しかし俺の趣味はここではどーでもいい(笑)。
いずれにせよ、最初のプログラミング言語とそれで書くスタイルってのは後々まで引きずるモノ、となる(※3)。
いや、進度が進めば自ずとから「キレイなプログラムを意識して書けるようになる」って?
んな事ねぇだろ。C言語脳が書いた「Python入門」的な本がprint塗れのクソ本になってるトコを見ても、「最初にやってたコーディングスタイル」からなかなか抜け出せてない、って証明じゃねぇか?
こんな状態なんで、C言語ってのは「初心者に教えるプログラミング言語」としては引退すべきだと思う。そして「C言語脳」的なコーディングから皆が解放されるのも、今から何十年もかかるんじゃねぇの?とか思ってるんだ。
それくらい、C言語脳が害毒を垂れ流してて「汚染状態」になってるんだよ。
今回の話はその証明の一つだ。
※1: 計算機科学者のドナルド・クヌースは「文芸的プログラミング」と呼ばれるスタイルを提唱したようだが良く知らん。
※2: 厳密に言うと、こういう固定長(つまり、突っ込む要素数が決まってる)なコンテナを設計する場合、Lispではリストよりベクタを使った方が速度的には有利となる。
ただし、このようなモックアップ程度を作る場合は、速度的に不利ではあってもリストを使った方が良いだろう。
あとで「最適化」はいくらでも出来るから、だ。
※3: 例えばC言語だと、最初に「とにかく何でもmain関数にツッコむ」書法でプログラムを書かされる為、「main関数以外の」関数の作り方を教わっても、「main関数に何でも書きまくり」なスタイルから脱却するのに思ったより苦労する。
「悪しきスタイル」を矯正するのは無理、あるいは物凄い時間と労力がかかるものなんだ。