Simple Scheme で電卓をつくってみる(4)

Simple Schemeで電卓を作っています。データ保持の方法が一応決まったので実装しましょう。
整数部分と小数部分を表すリストを暫定的にトップレベル変数にバインドしておきます。
電卓で数値を入力すると、このリストに数値が追加されていくようにしたいわけです。整数部分と小数部分のどっちを入力しているかをwhich-partというフラグ変数で管理します。

(define i-part '(0)) (define f-part '()) (define which-part 'int)

例えば365.2425という数が入力されたときi-part(5 6 3)f-part(5 2 4 2)となります*1

リストへの数の追加は以下のinput-numberが生成する関数によって行われます。

(define max-ndigits 8)
(define (input-number n)
  (lambda ()
    (and (< (+ (length i-part) (length f-part)) max-ndigits)
         (if (symbol=? which-part 'int)
             (if (and (null? (cdr i-part)) (= 0 (car i-part)))
                 (set! i-part (list n))
                 (set! i-part (cons n i-part)))
             (set! f-part (cons n f-part))))))

追加してみましょう。試しに。

(define _2 (input-number 2))
(define _3 (input-number 3))
(define _4 (input-number 4))
(define _5 (input-number 5))
(define _6 (input-number 6))

i-part f-part           ;初期状態
(_3)(_6)(_5)
(set! which-part 'frac) ;小数部分入力に切り替える
(_2)(_4)(_2)(_5)
i-part f-part           ;入力後

下にある12個の式を評価した結果が表示されます。

f:id:brv00:20191102083535j:plain:w250

*2

この関数のリストを作ります。
リストの並び順はbuttonsbutton-labelsと同じにします。こうすれば、ボタンのエフェクトの表示とリストへの数の挿入(いずれは電卓内での計算処理も)を同じ添字で行うことができます。
並び順を同じにするためにラベル(文字列)をキーとする連想リストを作っているますが、Simple Schemeにはassocがありません。代わりにfilterを使っています。

(define func-alist
  (list
   (list "0" (input-number 0)) (list "1" (input-number 1)) (list "2" (input-number 2))
   (list "3" (input-number 3)) (list "4" (input-number 4)) (list "5" (input-number 5))
   (list "6" (input-number 6)) (list "7" (input-number 7)) (list "8" (input-number 8))
   (list "9" (input-number 9)) (list "." (lambda () (set! which-part 'frac)))
   (list "+" ignore) (list "-" ignore) (list "×" ignore) (list "÷" ignore)
   (list "√" ignore) (list "=" ignore) (list "%" ignore) (list "AC" ignore)
   (list "C" ignore) (list "MR" ignore) (list "M+" ignore) (list "M-" ignore)))
(define button-funcs
  (map (lambda (b) (cadar (filter (lambda (p) (string=? (car p) b)) func-alist)))
       button-labels))

ignore(lambda () #f)と定義された関数です。何もしません。

i-partf-partで表された数を画面に表示するために、数字の列のイメージを生成する関数を定義しましょう。
桁数上限いっぱいに表示されたときに左端の数字が画面の左から1/6の辺りに、常に右端の数字が画面の右から1/6の辺りに、表示されるように調節しています*3

(define wdx (* width (/ 2/3 (-- max-ndigits))))
(define (wconv-x x) (round (- (* width 5/6) (* wdx x))))

(define (overlay-window scn)
  (letrec ((place-num
            (lambda (d lis scn part)
              (if (null? lis)
                  (if (symbol=? part 'int)
                      scn
                      (place-num d i-part
                                 (place-image (text "." (round wdx) "black")
                                              (wconv-x (- d 0.5)) (/ width 7) scn)
                                 'int))
                  (place-num (++ d) (cdr lis)
                             (place-image (text (number->string (car lis))
                                                (round wdx) "black")
                                          (wconv-x d) (/ width 7) scn)
                             part)))))
    (place-num 0 f-part scn 'frac)))

このイメージをkeyboardの上に重ねます。それなら最初から最後の行を(place-num 0 f-part keyboard 'frac)とするほうが簡単な気がしますけど、まあこのままでいいや。

ここまでの機能を電卓上で使えるようにbig-bangの呼び出しを書き替えましょう。
ボタンが押されたとき、まずbutton-funcsで内部状態*4を変更し、そのあと、keyboardの上にwindow、その上にボタンエフェクト、という順に重ねて表示します。

(big-bang
 calculator-image
 (on-draw (lambda (b) b))
 (on-mouse
  (lambda (_ x y what)
    (if (string=? what "button-down")
        (let ((x (iconv-x x)) (y (iconv-y y)))
          (let ((i (+ x (* 6 y))))
            (if (and (<= 0 x) (< x 6) (<= 0 i) (< i 23))
                (let ((b (list-ref buttons i)) (x (conv-x x)) (y (conv-y y)))
                  (begin
                    ((list-ref button-funcs i))
                    (set! calculator-image (overlay-window keyboard))
                    (place-image
                     (text (button-label b) (/ (* 4 (button-size b)) 3) "white") x y
                     (place-image (circle 100 "solid" (button-color b)) x y
                                  calculator-image))))
                calculator-image)))
        calculator-image))))

数値の表示部分に枠がないと落ち着きませんね😌

続きます。

(ここまでのコード)

*1:下位桁から並べています。変えるかもしれません。

*2:andは最後に評価した式ではなくboolean型の値を返すみたい

*3:末尾桁をリストの先頭にしたのは、そのほうが、このコードがシンプルになりそうだったからです。Simple Schemeのリストはランダムアクセスが定数時間で行われているようなのでlist-refの使用をためらう必要はなく、なので少しの差でしかありませんが((末尾呼び出しでない)再帰は多値を使うことになってネストが深くなりそう…)。

*4:今のところi-partとf-part(とwhich-part)のみ