Simple Scheme で電卓をつくってみる(13) ― メモリ機能 ―

モリーキーを実装します。
右端にあるM+、M-、MRの3つのボタンですね。

f:id:brv00:20191027111144j:plain:w250

モリーキーは計算結果を保存し、あとで取り出せるようにするボタンです*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-funcaddsubを渡すとボタンに反応する関数が生成されます。addを渡して生成される関数がM+ボタンを押したときに起動する関数であり、subを渡して生成される関数がM-ボタンを押したときに起動する関数です。

計算結果をmemoryに足すときやmemoryから引くときに桁溢れすることがあります*4。この桁溢れは他の桁溢れと扱いを変えて、エラーを通知しつつディスプレイに0を表示して、メモリの内容は変更しないことにしました*5
そうするとerror-typeoverflowではなくdivide-by-0ということになりますが、0割りは起こってないので紛らわしいですね。で、エラーの名前を変えました。リカバーされないエラーなのでnon-recovとします。また、overflowrecovに変えます。*6

そして保存された値を取り出すのはMRボタンです。

(define (inputMR)
  (and (symbol=? error-type nothing)
       (begin (set! input memory) (set! mode inputted) (set! window input))))

MRを押すとmemoryinput(とwindow)に代入されます。つまり電卓はMRが押されるとmemoryの値が入力されたような挙動を取るわけです。
だからといってモードはinputtingにするわけにはいきません。inputtingだと続けて数字を入力したとききに、inputmemoryの右に入力した数字をくっつけた数になってしまいます*7
かといってwaitingにもできません。inputにコピー*8されたmemoryの値が、以降の計算で無視されてしまいます。
というわけで新しいモードを作ります。inputtedです*9
各モードにおける主なボタンの挙動は次のようになります。

モード数字ボタン演算ボタン=ボタン
waitinginputを0にリセットしてから入力された数字を追加する次に行う演算を指定しなおすresultをinputに代入した上で、それぞれを左右のオペランドとして指定された演算を実行する
inputtedresultと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からメモリの消去以外の処理を分離しました。分離した処理をinputACinputCのそれぞれから呼び出し、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桁目が切り捨てられるので後者のほうが大きくなることがあります。

続きます。

(ここまでのソースコード)

*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ページ)。(数の語呂合わせは覚えなくてよいことでも簡単に覚えてしまう強力な方法であり、いきなり聞かされると私自身迷惑に感じることがあるので具体的には書きません)