見出し画像

Retro-gaming and so on

フラッシュ暗算ソフト

教えて!gooに以下のような質問があがっていた。


実の事を言うと、「フラッシュ暗算」ってのが何を指すのかサッパリ知らんかった(笑)。
調べてみたら、あーなるほど、数字を何秒間か表示して、暗算をしてもらって、答えを入力、それが正解かどうか調べるわけか。なるへそ。

さて、質問者は「競技プログラミング」経験者ではあるけれども「ソフトウェアの作り方を知らん」と言う話だ。
このブログを読んでる人は、例によって「REPL使えば簡単に作れるんじゃねーの?」って思うだろう。
その通りだ。
そして、これは批判じゃないんだけど、「競技プログラミング」が出来たから、っつったって、「ソフトウェアを書けるとは限らない」と言う良い例でもあるんだよね。
いや、競技プログラミングと言うのは「アルゴリズムを学ぶ」には有用でしょう。
ただし、書くのはあくまでソフトウェアと言うよりはスクリプトだ。REPL構造を持たないプログラムしか書かないわけだ。んで言っちゃえばこれが出来るってのは「別種の才能」なんだよな。
世の中には詩作がある。小説がある。エッセイがある。全部「文章を書く」って事は共通してはいるんだけど、かと言って「詩作の才能があるヤツが」面白い小説が書けるのか、と言うとそりゃまた別の話であって。基本的には同じ「文章を書く」作業だろうと、才能は別種なんだよな。
そういう事が「プログラミング」と言うジャンルでも生じてるし、言っちゃえば今からでも細分化されていくだろう。少なくとも、過去の例とか見てても「ワードプロセッサを書ける」ヤツが「面白いゲームを書ける」とは限らん、ってのを僕らは知ってる筈だ。

さて。今回のREPLには特有の問題があるんだけど、取り敢えずこの質問者の質問を見てみる。この人は大変エラい、って思った。
何故なら「最低限の労力で」ソフトウェアをでっち上げるにはどんなツールが必要なのか問うてるから、だ。
こういう考え方はいい考え方だよな。元々C++やってた、って事もあってズブの素人ではないわけなんだけど、それにしても、最近のPython界隈に入ってくる初心者みたいに「自分の実力がどの程度が把握してないのに」夢見るような事を投稿してくる輩よか遥かにいい。っつーかそういう「夢想する奴ら」を生み出してるのはあくまで教育側の問題だとは思ってるんだけど。あとは本を書く奴らのせいか。ライブラリばっか紹介してて中身の無い事を初心者に吹き込んでる奴らのせいだ。
でもこの人は、「経験者であるが故に」おかしなスタイルの質問じゃない。大変好感が持てる。
ただし、答えは「Pythonだけあればいい」だ。他に特にツールは必要ない。IDEなんかも紹介する必要もない。余計なモノは一切要らないんだ。
(大体、C++の経験者な以上、既に愛用のIDEがある可能性さえある)
このプログラムを書く際に必要なのは、あえて言うと「端末」だ。DOS窓/PowerShell(Windows)やShell(Linux/Mac)を入出力の媒体として使う、って事さえ念頭に入れておけば良い。ブラウザ上で動作させる、とかGUIにしよう、とか余計な事は考えない。一番簡単で手慣れたツールになるのは圧倒的にDOS窓/PowerShell/Shellであって、それで動くプログラムを書くのが一番簡単なんだ。そしてそれこそがプログラミングの基礎なんだよ。

端末は非プログラマには何の面白みもない古臭いインターフェースだが、プログラムを書く側に取っては最も頼りになる「そこにある」ツールだ。

さて、このプログラムは一つだけ厄介な点があるんだけど、それは後回しにして、最初に、環境用の変数を作成する構造体・・・と言うか、Pythonだからクラスから作っていく。
まず、材料として何が必要になるのか考えよう。
ザーッと問題を見る限り、

  • 表示する数値の桁数
  • 暗算の材料になる数値の個数
  • 数値が表示される秒数
がまずは必要になる、ってのが分かる。
もう一つ必要なのは、表示された数値が格納されるリストだ。
次々と乱数で生成した数値を、Lisp用語で言うと、リストへとconsしていく・・・そうすれば、表示される数値は常にリストのcar(head、あるいは第0要素)になるし、答えをコンピュータ側で検算する際に、Pythonでは格納リストのsumを取れば良くなる。これと入力された数値が等しいかどうかで正誤判定が出来るわけだ。
もう一つは一種フェーズを用意する。
と言うのも、このREPLに於いて、「数値を表示する」だけの時は入力が要らない。
従って、要求された個数の数値が表示された後にのみ入力を求めるように現在の「状態」を示す一種のフラグを用意しておく。

これら5つをまとめて、環境用クラスEnvを次のように定義してみよう。



一つTips。
PythonのクラスはRacketで言う#:transparent無しの構造体のようなモノで、生成されたブツ(インスタンス)の中身を簡単に見る事が出来ない。
つまり、printによるデバッグが難しいわけだ。なんせどういう状態なのか確認出来ないからな(これは大方のOOP(オブジェクト指向)言語の欠点だと思ってる)。
そこで、Racketの構造体の#:transparentのように「内容物がすぐ確認が取れる」ようにするには、__repr__と言う特殊メソッドを作って文字列としてデータが返るようにしておけばいい。
そうすれば、Envのインスタンスをprintに渡せば、現在のインスタンスのメンバ変数(インスタンス変数)がどうなってるのかが見え、プログラムのどの辺で間違った計算をしたのか分かるようになる。

次に入力のreadを作ろう。
先の議論で分かる通り、このプログラムでは「大方のケースでは」入力が全く要らない。
じゃあ、いつ入力が必要になるのか、と言うと、乱数で生成した数値の個数が、設定した「数値の個数」、つまりEnvのインスタンス変数のnumに一致した時、だ。
例えば、numが15だった時、乱数で生成した数値の個数が15だったら、「暗算した答えを入力してくれ」とプロンプトに表示すればいいわけだ。
この検査は、要するにnumと、数値格納用リストの長さが一致するかどうか、って事だ。
これを鑑みると、関数readは次のようになればいい。


基本的には、上に書いた通り、環境Envのインスタンス変数であるnumlstの長さが一致した時だけ、入力を求めるが、それ以外は空文字列""を返す。
なお、条件分岐の枝が一つ多いけど、それは「フラッシュ暗算ソフト」を終了する為のモノだ。
現時点、環境Envのインスタンス変数であるflagNoneTrueFalseの3つの状態になる事を想定してる。数値をただ表示してる時はNoneだけど、入力された答えが正しいか誤ってるか、をTrue/Falseで表現する。
つまり、一回フラッシュ暗算ソフトを走らせると最後は必ずこのflagはブール型になってるわけだ。そのブール型の時に「遊ぶのを止めますか?」と訊く為にこういうカタチになっている。

次にevalを作ろう。
先に完成形を見てみようか。


まず、最初に終了条件を調べてるが、先程見た通り、一回ソフトを走らせて最後まで行くと、「必ず」Envのインスタンス変数であるflagはブール型になっている。
その時、入力から'Y'及び'y'がやってきたらsys.exit()を呼び出してプログラムを終了する。
まぁ、そこは簡単でしょ。
問題はフラッシュ暗算ソフトが「数値を表示し続ける」タームだ。
まず、Envのインスタンス変数であるdigitsnumsecondsは変わらない。こいつらは一旦ソフトが走り出せば不変なんで特に弄る必要がないわけだ。
結果、残り2つを弄るだけでいい、って事になる。
最初にEnvのインスタンス変数であるlstから見てみようか。
単純には、先にも書いたけど、Lisp用語で言うとEnvのインスタンス変数であるlstに生成した乱数をconsしていけばいい。Pythonだと[乱数] + lstでLispで言うconsが出来る。
その乱数生成だけど、randintを使う。Pythonのマニュアルにはこんな風に書かれている。


つまり、例えば3桁の数を表示したい場合は、randint(100, 999)だったら良いわけだ。4桁だったらrandint(1000, 9999)だよな。
そうすると、乗数を使えばPython表記で、10 ** (digits - 1)10 ** digits - 1を与えれば済む、と言う話になる。そうすれば帳尻が合う。
あとは、Envのインスタンス変数であるlstの長さとnumが一致してる時は、既にフラッシュ暗算ソフトが1周し終わった、って意味なので、(あれば)次に備えて、lstを空リストに戻しておけばいい、って事になる。

残るはEnvのインスタンス変数であるflagの処理だ。ちとややこしく見えるが説明しよう。
何度も言うが、Envのインスタンス変数であるlstの長さとnumが一致してる時はreadが入力フェーズになってて、実際入力結果(つまり暗算した場合の答え)が渡ってくる。そうすると、入力(x)とEnvのインスタンス変数であるlstの総和(sum)が一致してるのかどうか、ってのが焦点だ。ここで等価比較でTrueFalseが返ってくるわけで、真偽はすぐ分かる事となり、どっちにせよ、それを格納する。
仮にEnvのインスタンス変数であるflagがbool型で、なおかつlstが空リストだったら、次の問題作成の用意をしなきゃならないんだけど、この場合はflagを初期値だったNoneに戻す事、となる。
あとは、数値表示中なんで、Envのインスタンス変数であるflagはいじらず、そのままの状態を保持しておけばいい。

なお、三項演算子的な書き方をしてるんで、人によっては「アレ?」とか思うかもしれない。
でも慣れよう。
そもそもLispなんかだと「全部式」なんで、Lispの条件分岐はそれこそ全部三項演算子みてぇなモンだ。
また、C言語みたいな言語だと「三項演算子は読みにくいから止めましょう」と言うようなサジェスチョンが良くある。ネット検索してみればそういう話が山ほど引っかかる筈だ。
ただし、このテの話にはちと誤解がある。「三項演算子が悪」って事じゃないんだ。そうじゃなくって「C言語系の三項演算子が読みづらい」って話なだけで、それは「全言語に適用されるべき性質の話ではない」と言う事。悪いのはC言語系の構文であって、三項演算子の存在自体が悪いわけじゃないんだよ(※1)。
C言語の三項演算子は「視覚ノイズ的な」ショートカットな記述法なんだけど、Pythonは読もうと思えば読める範囲の記述だ。
くれぐれも件の「C言語脳」が言う「三項演算子は悪」っつー話をアタマから信じないように。あくまでそれは「C言語系」の内輪の話であって、他の言語じゃ必ずしもそうじゃねぇんだ。
むしろ便利だし、記述が短くなるんでどんどん使え。C言語系以外の言語、ならな。

さて、最後にprint部を作る。
が、ここでちと問題があるんだ。っつーかこのトピック最大の難関だ。
「フラッシュ暗算」って言うのは「数値が表示されて」そしてそれが消えて「別の数値が表示」されないとならないわけ。
ところが、インタプリタとか通常の端末の状態だと、数値が「残ってしまう」んだよな。ガンガンスクロールして表示していってくれるんだけど、一旦印字したモノはどっちにせよ画面に残ってしまう。
これを一体どうすんだ、と言う話になるわけだ。

果たして、これを解決するには?
まずはコードをお見せしよう。


Envのインスタンス変数であるflagNone以外の時、ってのは暗算で出した答えが実際合ってるか合ってないかを表示すればいい。まぁそこはいいだろ。例によって「メッセージ分離方式」で、True/Falseをキーとしたハッシュテーブルを作っておいて、そこから文字列を引っ張ってきてる。それはいい。
問題は、だ。Pythonを初めとしてフツー、プログラミング言語にはプログラミング言語の機能として「端末画面に今まで表示されてたブツを全消去」する機能なんて持たされていない。
こういう時どーすんのか、と言うと、解決策は実は単純だ。OSの機能を呼び出して端末に映ってるモノを全消去するんだ。そうすれば要求仕様を満たせる。
MS-DOSならclsと言うコマンドが端末に映ってる文字だ何だを一旦全消去するモノ(Clear Screenだろう)、そしてUNIX系のコマンドでそれに当たるのがclearだ。
一回自分の使ってるPCのOSで、適応するコマンドを走らせてみればいい。これらのコマンドが「画面に映ってるモノを」掃除してくれるのが分かると思う。
つまり、今回のdisplayの本体部分は、

  1. OSに頼んで端末に映ってるブツを全消去してもらう。
  2. Envのインスタンス変数であるlstの最初の要素を印字する。
  3. Envのインスタンス変数であるseconds分だけsleepする(ウェイト処理)。
と言う3つのプロセスで、要求仕様を実現してるわけだ。
最後に環境、envをそのまま返すのを忘れないようにしよう。

あとはいつもどおり、REPLを形成するだけ、なんでそれはいいだろう。ソースコード全体はここに置いておく。

今回これをここで改めて取りあげたのは、

  1. 端末で動くプログラムを作るのが一番簡単だ。
  2. 端末で動かす以上、何か上手く行きそうにない場合はOSに頼れる。
と言う2点を強調したかったからだ。
昨今、すぐ「ブラウザでプログラミングすれば・・・」とかバカなばっか言う輩や書籍ばっか増えてきてるみてぇなんでそーじゃねーよ、って言いたかった事。端末前提の方がむしろラクなケースに落とし込める、と言う証明をしたかったんだ。
もう一つは、何か困った事があったらOSに頼れ、と言う習慣が大事だ、ってのも書きたかったのね。プログラミング言語は色々出来るんだけど、ただ、「何かを実現する為には」メチャクチャ手間がかかる場合があるわけ。あるいは「出来ねぇ」とかさ(笑)。
でも意外と、「OSの機能を使えば」解決する事、とか山ほどあるわけよ。むしろプログラムを書く必要がなかった的な事さえあるわけだ(※2)。
今はGUIの世の中なんで、例えばWindowsを使ってればMS-DOSコマンドを知らんでいい、とかなってるんだけど。でもパンピーはそれでいいけど、プログラム書く側だとちとマズい事が多い。と言うか、昔、例えばPC-9801でプログラミング学んだ層とかは圧倒的に、まずはDOSを使いこなしてたんだよな。足りない部分を自作のプログラミングで補うとか。それは「プログラムを作る際に手間をかけない」って意味では正しいんだよ。昔の人は偉かった(謎
まぁ、僕もUNIXコマンド全部知ってるわけじゃないんだけど(クッソ多いし・笑)、CLIはCLIで使い勝手があって、特にプログラミングの際に、とてつもない威力を発揮する事があるよ、って書きたかったわけだ。今回はテーマがドンピシャだったんで、それを書いた、と言うお話だ。

ただ、一つ難点を言うと、MS-DOSとUNIXじゃ、今回見た通り「コマンド名が違う」って事がままあるのね。そうすると、あるOSの機能に頼ったブツを書くと、それを別のプラットフォームに持っていけない・・・と言う事がしばしば生じる。いや、「しばしば」じゃねぇか(笑)。頻繁に起こる。
それだけが悩みドコって言えば悩みドコかな。
なかなかポータビリティって大変だよ(苦笑)。

※1: Lispは全部式なんで、条件式自体に返り値がある。
一方、C系言語やPythonなんかの「いわゆるフツーの言語」の場合、条件分岐は文で三項演算子は式を形成する。従って、前者は返り値が存在しないが、三項演算子には返り値がある。
そうすると、例えば配列の何番目かの数値を条件で分けて返したい場合、配列の参照の部分に三項演算子を突っ込んで、Lisp的な記述をする事が可能で、しかもそれが便利でコードが短くなったりするんだけど、特にC系言語の場合は、「三項演算子の見た目の悪さ」のせいで、勿体なくも単に忌避される事が多い。

// C言語系だと次のような書式になってるのが多い。
pred ? var0 : var1

結果、三項演算子が「値を返す」が故に便利だ、と言う事が、C系言語の学習者には特にピンと来ない話になってたりする。
ちなみに、Pythonの var0 if pred else var1 と言う書き方は、通称「三項演算子」と言われるが、実際問題、elseifはガンガン組み合わせられるので、例えば var 0 if pred0 else var1 if pred1 else var2  ... みたいに長く書く事が可能で、結果、五項演算子だったり七項演算子だったり、と自在になる(から「三項演算子」って通称はおかしくねぇか?って思ってる・笑)。

※2: そういう意味で言うとLispはサイテーかもしんない(笑)。なんせ、スティーヴ・イエギが言ってるように「オペレーティングシステムが存在しない振りをしようとして、リストがすべてだとか」言ってる言語だからだ(笑)。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

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

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