トリビア目次へ lnpp公式サイトへ

継承も理解できずに変な電卓を作っちゃう話

ちなみに現時点でのソースはこちら
activity_main.xml
MainActivity.kt

HP電卓は好きです。
それも伝統的な4段スタックのものではなく,RPLを使用しているものです。
メモリーのある限り何段でもスタックに積めて,全部足す時などとりあえず数値を入れて行って,最後に+を連打すると答が出る。記録式電卓みたいに入力した数値をチェックしながら足していくことが可能なRPLです。
何台も持っていて,あらゆるところですぐに使えるようにしてあるのですが……。
若干の不満もあります。
それは……HPが電卓の生産から撤退して,もはや新品を購入することはできないことです。

「なけりゃあ作ればいいじゃない」
……作ってみました。

でも……私,オブジェクト指向が苦手です。普段はLispに慣れ親しんでいるし,機械語に落として「こうやって動くんだ」ということは理解できるのですが,オブジェクト指向で書いて,「なぜ,こう書いただけで動くのか?」がいまいちよくわかってないし,特に継承がよくわかっていません。(いや,schemeの継続もよくわかってないだろうと言われればまさにそのとおりなんですが……。)その他にも初めて書くkotlinで,「プロジェクト指向らしさが全くない」とかいろいろプロからのツッコミはあると思うのですが……。
まあ,生暖かい目で見守ってもらえればと思います。なんたって日曜大工!専門は国際法!!

1 ターゲット

BlackBerry Key2です。物理キーボードがついている機種です。物理キーボードからの入力と画面上に表示された独自のテンキーからの入力のどちらも受け付けます。
入力画面になると自然に現れるキーボードでも(現時点では)入力できますが,操作性が悪くなる(独自のテンキーが隠されてしまう)ので,将来的にはあえて表示させないようにするかもしれません……そうすると他の機械では使えなくなるねい。
画面の大きさは4.5インチに1080×1620dot。もっとも画面表示は基本的に(特に横幅は)相対的な指定にしているので,この画面より広ければ動くと思います。狭ければたぶん操作性が悪くなるか,画面移動ができなくてまともに動かないと思います。
ハードウェア的にはBlackBerry KeyONEでも動くと思いますし,androidのバージョンが7なのですが,8以降の特殊機能は使わず,コンパイルも4.1以上なので,たぶん動くんじゃないかなとは思います。

2 xmlで画面を設定する

Android Studioでプロジェクトを起こす時に,基本的にはEmpty Activityを選ぶと思うのですが,その場合,ある程度の設定がしてあるactivity_main.xmlとMainActivity.ktが生成されます。そこに必要な処理を書き加えていくのが基本になります。で,私のようにkotlin使うの初めてなんて場合には,xmlファイルに最低限の設定をして,それを使うコードをktに書いて,「この使い方でいいんだ」って確認したあとで,拡張していく……の繰り返しで作ることになると思います。
ただ,今回の場合,実はJavaのプログラムを改造した時にactivity_main.xmlもいったんは作っていて,ただそのソースが手違いで失われたので,今回,外見としては似ているものを1から書いたため,はなから完成形に近いものができています。そこに,コードを少しずつ書き足しては動作を確認して……の繰り返しで作りました。

3 xmlに仕掛けをしてktから操作する

最初にはまったのが「なんでxmlで書いたもので情報がコードに渡せるんだ?」という話です。今までは基本的にコンソールから動かすアプリケーションしか書いてこなかったので,このあたりの仕組みが全くわからなかったのです。
物の本によると……
「画面に何か感触があるかは常時見張っておこう」……リスナ
「画面に何か触れた!」……イベント
「対応する処理を行おう」……イベントハンドラ
という仕組みらしいのですが……。なぜ,コードを書くだけで実現してしまうのか,今もピンと来ていません。
とはいえ,これを再現すると動くのが間違いないので理由はわからないけど受け入れることにします。

xmlのファイルで EditText android:id="@+id/neko" と書くと
ktのファイルで findViewById(R.id.neko)でその部分の情報が得られるという関係だそうです。
これが基本。
その上で,今回は neko についてsetOnClickListenというメソッド(で,いいのかな),すなわちneko.setOnClickListenで拾って処理を書けばいいことにします。

4 EditTextを入力に,スタックの表示をTextViewに

スタックの内容を示すのにTextViewを4段配置し,下から上へ伸びていくようにしています。HP電卓と一緒です。
また入力内容を示すのにEditTextを1段配置しています。EditTextに入力されるものはString型ではないので,「textView0.text.toString()」というように,String型に変える必要があります。
本当は,全部TextViewにしてRPLなHP電卓と同じ挙動にしたかったのですが,「文字の追加と削除」「その間,スタックを1段ずつ上げる」「文字の追加と削除が終わったらスタックをもとに戻す」の作業が意外に面倒で,EditTextにすれば,HP電卓の挙動とは異なりますがプログラムが簡単になるので,とりあえず動くものを作ろうという方針で……日和りました。

5 MutableListの使用

「var mainstack: MutableList = mutableListOf()」
HP電卓も基本はスタック操作です。というので,スタックをどう実現するかという話になります。
普通は配列を使うのでしょうが,配列はサイズが固定されてしまい,あとで追加することはできません。配列がいっぱいになっているかどうかを管理する必要が出てきます。
というのでkotlinのMutableListを使います。MutableListの個々の要素はStringにします。変な電卓である理由の1つとして「文字列をそのまま受け入れる」というのがあるので個々の要素はString一択なのですが,まともな電卓を作る際にも真の値と表示が一致するのは実はStringだというのもあります。
で,わかりやすさを優先して,mainstack[0]をスタックの最下層ということにして,元からあるデータは1つずつずれることにしています。
また取り出す時はmainstack[0]から取り出して,元からあるデータを1つずつずらします。
なお,mainstackのデータの増減がある場合には「var mainstackp: Int = -1」を同時に操作しています。これは,後で説明するmainstackにデータがない時にmainstack[x]としてデータを取り出そうとするとエラーが出てしまうことへの対策で,mainstackpの値を確認して,データがある時だけ処理するためのものです。
とはいえこれはMutableListの要素数を取り出す関数があるはずなので,それで判断した方がスマートです。(あとで書き換えます。)
なお,mainstack(とmainstackp)は大域変数としていますが,たぶんclassにした方がオブジェクト指向的に正解なんでしょうね。

6 スタック操作

スタック操作については,基本的にはmainstackの中身を書き換えて実現しています。
mainstackに必要な操作をして,その結果を都度表示させるようにしています。
swap,dropはソース見てもらえればわかると思います。
clearについては,MutableListの初期化をするともに,入力エリア(EditTextで実現しtextView0というidを与えています)も(nullではなく)長さ0の文字列(空文字列)を入れています。
cについては,入力エリアをクリアするだけでスタックはいじりません。
BSについては,入力エリアから1文字削除するのですが,一旦読み取って,末尾の文字を削除して元に戻すという操作をしています。
enterについては,入力エリアに文字がある場合には,その入力エリアの内容をスタックに積みます。その際,文字列の前後に空白がある場合にはcutします。入力エリアに文字がない場合には,DUPとして動作します。具体的には最下層のスタックの内容を複写して最下層に置きます。
evalについては,入力エリアの文字をコマンドとして解釈して実行する(=lisp的に言うと「評価」する)予定なのですが,現在は実行する関数を1つも書いていないので,ただ戻るだけです。
通常の電卓の場合,状態遷移図を書いて,そのとおりに実装するという方法があるようなのですが,RPLなHP電卓の場合には,Lisp的に「入力→評価→出力」で回した方が簡単ですし,Androidでイベント駆動だってことは「リスナ→イベント→イベントハンドラ」で「入力→評価→出力」のループを実現できるので,「ボタンのid.setOnClickListen」でその処理内容を書き,evalで入力エリアを評価させると,結局Forth処理系に似た処理ができるようになります。
現時点では,関数(イベントハンドラに相当するもの)をwhenで場合分けして呼び出すことを考えていますが,「textView0.x」と書けて,xに対応する関数が動くようになるとだいぶ楽なんですが……(できるかどうかすらわからない)。

7 計算

四則演算については,まずスタックに2つ以上の入力があり,かつ,その入力が数値であることが必要です。それをチェックしてから四則演算にかかります。
もし入力が足りなかったり,数値ではない場合なのですが,入力が足りているかどうかのチェックはmainstackpの値で判断しています。また数値かどうかについては,null許容型の変数(末尾に?を付ける)に.toDoubleOrNullメソッド(?)で入れてみて,nullでないことを確認しています。
もし入力が足りなかったり,nullだった場合ですが,HP電卓でも警告音と警告メッセージを出していますが,本件プログラムではUNIX流で(笑)何も起きないことにして,楽をしています。
確認した後,+,ー,×については,今度はnull許容型でない通常の変数(末尾に?がつかない)にtoDoubleで改めて入れて,計算し,mainstackからは2つ出して答を入れるという動きになります。
÷については,0除算チェックをするために,除数についてはInt型のnull許容型変数にも入れて0除算チェックをしています。エラー時には何も起きない点は一緒です。
「標準」「軽減」は消費税計算のためのものですが,正確に計算するためにInt型を使っているので,特に小数を与えた場合,計算後の税額と本体価格の和が計算前の金額とは一致しないことになっています。
税額計算に限らず,BigDecimal型を使えば正確に計算できるのですが,kotlinでBigDecimal型をどう書けばいいのか,今いちわかっていないので,「四則演算はdouble型,税額計算はInt型」ということでとりあえず作りました。
また,null許容型とそうでない型とで2回変換していますが,これはnull許容型とそうでない型で型が不一致であるというエラーが出ることでこうしていますけど,何かうまい手があるんじゃないかと思います。(大人しくtry〜catchで拾った方がよかったかもしれない。)
なお,dotを1つの数で2回以上でることはあり得ないので,電卓はエラーを出すのですが,本件プログラムでは,入力としては受け付けて,数値としては扱わないということにしています。

以上,kotlinで書いた初めてのプログラムで,プロから見れば「何無駄なことやってんだ」というものなのですが……本人よ〜くわかっております。
それでも,テクニックに走っていない分,解読しやすいプログラムにはなっていると思いますので,何かの参考になれば……ということで公開します。

なお,将来的には,
1 evalの中でstack操作については,Forthの関数を多く実現する
2 入力エリアで(で始まった場合には,Lispの関数とみなして評価する
3 最終的にはForthの関数もLispの関数として実現して,全体としてはLispが動いていることにしたい
なんてことを考えています。
……実現はいつになることやら……。

(2021.10.22. 初版)

トリビア目次へ lnpp公式サイトへ