Simple Scheme で電卓をつくってみる(13) ― メモリ機能 ―
メモリーキーを実装します。
右端にあるM+、M-、MRの3つのボタンですね。
メモリーキーは計算結果を保存し、あとで取り出せるようにするボタンです*1。
というわけでまず保存内容を保持する変数を用意します(というかすでにmemory
という名前で大分前から用意してあります)。
(define-struct num (sign i f)) (define plus "") (define minus "-") (define zero (make-num plus '(0) '())) (define input zero) (define window input) (define result zero) (define memory zero)
memory
の初期値は0ですね。M+を押すと、memory
に計算結果が加算されます(memory
がバインドしている値と計算結果とを足し合わせた値がmemory
に代入されます)。
例えば 3 M+ × 4 = M+ と打つと、最初のM+で3が加算されmemory
の値は3になり、次のM+で3×4が加算されmemory
の値は15になります。
計算が途中のときは計算を終わらせてから加算されます。つまり電卓は、M+が押されたとき、まず=が押されたときと同じ挙動をし、それからmemory
への加算を実行します。だから上の例で最後のM+の前の=がなくても電卓の最終的な状態は完全に同じです。最初に3を押したときも計算の途中(数値の入力の途中)であるとみなせますが、これもちゃんと保存されます*2。
M-を押すと、memory
から計算結果が減算されます*3。
例えば 3 M+ × 4 = M- と打つと、最初のM+で3が加算されmemory
の値が3になり、次のM-で3×4が引かれmemory
の値が-9になります。
M+やM-が押されたときに起動する関数を、上記のような挙動をするように定義しましょう。関数は2つになりますが、メモリに結果を足すか引くか以外に違いがないので、生成関数を作って違う部分だけparameterizeすることにします。
(define (make-m-func op) (lambda () (begin (input=) (and (symbol=? error-type nothing) (let ((x (op memory result))) (if (> (length (num-i x)) max-ndigits) (begin (set! error-type non-recov) (set! result zero)) (set! memory x)))))))
make-m-func
にadd
かsub
を渡すとボタンに反応する関数が生成されます。add
を渡して生成される関数がM+ボタンを押したときに起動する関数であり、sub
を渡して生成される関数がM-ボタンを押したときに起動する関数です。
計算結果をmemory
に足すときやmemory
から引くときに桁溢れすることがあります*4。この桁溢れは他の桁溢れと扱いを変えて、エラーを通知しつつディスプレイに0を表示して、メモリの内容は変更しないことにしました*5。
そうするとerror-type
はoverflow
ではなくdivide-by-0
ということになりますが、0割りは起こってないので紛らわしいですね。で、エラーの名前を変えました。リカバーされないエラーなのでnon-recov
とします。また、overflow
はrecov
に変えます。*6
(define (inputMR) (and (symbol=? error-type nothing) (begin (set! input memory) (set! mode inputted) (set! window input))))
MRを押すとmemory
がinput
(とwindow
)に代入されます。つまり電卓はMRが押されるとmemory
の値が入力されたような挙動を取るわけです。
だからといってモードはinputting
にするわけにはいきません。inputting
だと続けて数字を入力したとききに、input
がmemory
の右に入力した数字をくっつけた数になってしまいます*7。
かといってwaiting
にもできません。input
にコピー*8されたmemory
の値が、以降の計算で無視されてしまいます。
というわけで新しいモードを作ります。inputted
です*9。
各モードにおける主なボタンの挙動は次のようになります。
モード | 数字ボタン | 演算ボタン | =ボタン |
waiting | inputを0にリセットしてから入力された数字を追加する | 次に行う演算を指定しなおす | resultをinputに代入した上で、それぞれを左右のオペランドとして指定された演算を実行する |
inputted | resultとinputを左右のオペランドとして指定された演算を実行してから次に行う演算を指定する | resultとinputを左右のオペランドとして指定された演算を実行する | |
inputting | 入力された数字をinputに追加する |
この変更に伴って例えば「waitingモードなら」という条件を「inputtingモードでないなら」とする様な変更をコード内のあちこちでしています。
memory
が0でないときはMが表示されます。表示位置はエラー表示の位置((sconv-x 13), sy)の左隣((sconv-x 14), sy)にしておきましょう。
(define overlay-window (let* (;... (sy (* width 1/12)) (sconv-x (lambda (x) (round (* width (- 9/10 (* 1/20 x))))))) (lambda (scn) (let* (;... (scn (if (and (= 0 (car (num-i memory))) (null? (num-f memory))) scn (place-image (text "M" 40 "#888") (sconv-x 14) sy scn)))) ;... ))))
メモリーキーがないときはinputC
の中でinputAC
を呼び出していましたが、ACがメモリを消去する(memory
を0にする)のに対して、Cはそんなことしないので、inputAC
からメモリの消去以外の処理を分離しました。分離した処理をinputAC
とinputC
のそれぞれから呼び出し、ACでのみさらにメモリも消去します。
(define (clear) (begin (set! op-no 0) (set! result zero) (set! window result) (set! mode waiting) (set! error-type nothing))) (define (inputAC) (begin (clear) (set! memory zero))) (define (inputC) (if (symbol=? error-type nothing) (if (symbol=? mode waiting) (clear) (begin (set! which-part int) (set! input zero) (set! window input))) (set! error-type nothing)))
メモリ機能を利用してNewton法で√5を計算してみました。初期値は有名な語呂合わせを素直に解釈した数字列*10です。
初期値の誤差が1/104程度なので、5 ÷ MR + MR ÷ 2 だけで収束しますが、この方法で求めたあと、同じ入力を繰り返してメモリ内で精度をあげていく方法でも求めています。前者は近似値xを(5/x+x)/2に置き換え、後者はxを(5/x+x)-(5/x+x)/2に置き換えます。無限精度なら同じですが電卓だと2で割ったときに max-ndigits
+ 1桁目が切り捨てられるので後者のほうが大きくなることがあります。
— brv00 (@brv00) 2019年11月24日
続きます。
*1:他のキーは〇〇ボタンって言ってたのにいきなりキーとか言い出す。
*2:もう少し複雑な例として、3 + 4 × M+ と押すと、3 + 4 × =が押されたときと同じ挙動をしたあと結果である49がmemoryに加算されます。
*3:サ変動詞の減算ってあんまり聞かないですね。引き算されます。(スルって書いてある辞書もありますね(-.-))
*4:例えば(max-ndigitsが8のときに) 9 9 9 9 9 9 9 9 M+ 2 M+ と入力した場合。
*5:そういう仕様の電卓しか知らない。
*6:名前を変えずに、メモリに足すときの桁溢れエラーをoverflowにしてもこの仕様通りに動かせそうなことにあとから気づいた。でも平方根ボタンを実装するときにこの変更は役に立つと思います(๑•̀ㅂ•́)و✧ 知らんけど(多分変更せずに平方根つくったらエラーの種類が増える)。
*7:それはそれで面白いかもしれませんが。
*8:多分ポインタを共有しただけだけど、structureの中身を変更できないのでどっちでも同じです。
*9:モード名は全部「数値」を意味する言葉が省かれてる感じ。数値(の入力)を待っている(waiting)、数値を入力中(inputting)、数値を入力し終えた(inputted)。
*10:昔これを聞いて計算してみて合わなくて最終的に「『に』いらんやん(´д`|||)」ってなった記憶。「に」をいれないバージョンが書いてある本もありますけど(19ページ)。(数の語呂合わせは覚えなくてよいことでも簡単に覚えてしまう強力な方法であり、いきなり聞かされると私自身迷惑に感じることがあるので具体的には書きません)