Simple Scheme で電卓をつくってみる(11) ―エラー処理―

Simple Schemeで電卓を作っています。割り算を組み込んで加減乗除が揃いましたが、0で割ろうとした場合の対応をまったく考えていませんでした。
0で割ろうとすると0除算エラーなので、エラー処理機構を作ります。
桁溢れエラーの処理もここでやってしまいましょう。

まず、エラー状態を定義します。

(define nothing 'nothing) (define overflow 'overflow)
(define divide-by-0 'divide-by-0) (define error-type nothing)

エラー状態はerror-typeという変数にバインドします。エラー状態は3種類あり、エラーでないときはnothing、桁溢れエラーはoverflow、0除算エラーはdivide-by-0です。
error-typeの初期値はnothingです。error-typeには、計算結果が10^max-ndigitsを越えたときと0除算を行おうとしたとき、それぞれのエラー状態が代入され、解除されたとき、初期値であるnothingが代入されます。

(define (check-overflow!)
  (let* ((i (num-i result)) (li (- (length i) max-ndigits)))
    (and (> li 0)
         (begin (set! result
                  (make-num (num-sign result)
                            (take i li) (drop (take i max-ndigits) li)))
                (set! error-type overflow))))) ; 桁溢れエラー

(define (make-op i)
  (lambda ()
    (and (symbol=? error-type nothing)
         (begin (and (symbol=? mode inputting)
                     (begin (set! result ((list-ref ops op-no) result input))
                            (check-overflow!) (set! window result)))
                ; 0除算エラーでは、エラー直前に押された演算ボタンは無視され、…
                (set! op-no (if (symbol=? error-type divide-by-0) 0 i))
                (set! mode waiting)))))

(define (div x y)
  (let ((sign (mul-signs-of x y)) (lfy (length (num-f y))) (y (num->int y)))
    (if (= 0 y) (begin (set! error-type divide-by-0) zero) ; 0除算エラー
        ...)))

エラーが起こる直前に演算ボタンが押された場合、桁溢れエラーでは、(桁溢れした値が10^max-ndigitsで割られた上で)電卓を例えば次のように操作することで計算を続けることができます。

  1. エラーを解除する(Cを押す)。
  2. 数値を入力する。
  3. 演算ボタンか=を押す。

例えばmax-ndigitsが8のときに333333.33×333333.33÷と入力すると、計算結果の正確な値は111111108888.8889であり、整数部分が8桁を越えているので、エラーになります。このとき表示窓に表示される数値は、正確な値に対して小数点が左に8移動した、1111.1110となります。このあと例えばC33.333333=と入力すると、1111.111÷33.333333が実行され、33.33333という結果が得られます。
つまり桁溢れエラー直前に押された演算ボタンは有効です。
一方、0除算エラーでは、エラー直前に押された演算ボタンは無視され、その後の計算に関与することはありません。Cを押すと、(ほぼ*1)初期状態に戻ります。
つまり0除算エラー直前に押された演算ボタンは無効です。
これは参考にしている電卓(SL-720L)の挙動をそのまま真似しました*2。エラーの種類で挙動を変えるのは面倒だったのですが、0除算の結果を8桁ずらすというのはやりようがない*3ので、桁溢れと異なる挙動にしました。
挙動を変えるためにエラー状態を3種類定義したわけです。

さて、error-typeoverflowdivide-by-0のとき、つまりnothingでないとき、CとAC以外のボタンは無効です。
なので、CとAC以外の各ボタン関数の先頭に次のようなコードを挿入しました。

    (and (symbol=? error-type nothing)

上のmake-opの定義にも挿入されていますが、つまり、error-typenothingのときのみ続きのコードが実行されるわけです。

また、overlay-window以下のバインドを挿入して、エラー時にEを表示させるようにしました

             (scn (if (symbol=? error-type nothing) scn
                      (place-image (text "E" 40 "#888") (sconv-x 13) sy scn))))

エラー時に押せるボタンはCとACのみですが、Cが押されたときの挙動はエラー状態の解除のみです。桁をずらして計算可能な範囲に収める等のややこしい処理はすべてエラー発生時にすんでいます。

(define (inputC)
  (if (symbol=? error-type nothing) 
      ; (帰結部)
      (set! error-type nothing)))

初期状態がひとつ増えたのでACでやることもひとつ増えました。

(define (inputAC)
  (begin (set! op-no 0) (set! result zero) (set! window result)
         (set! mode waiting) (set! error-type nothing)))

桁溢れのときと0除算のときの挙動の例をそれぞれ貼っておきます。

続きます。

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

*1:メモリなどはそのまま残ります。まだメモリないけど。

*2:違う挙動にしたところは今のところありませんが。

*3:divは0を返していますが、割り算の結果が0というわけではないのでこれを8桁ずらして続きの計算に使うわけにはいきません。