Simple Scheme で電卓をつくってみる(7.5)
単に第2引数を返すだけだと最初の数値入力で小数部分に余分な0を入れた場合、=を押しても0が残ってしまいます。別に残ってもいいのですが取り除くのもそれほど難しくありません。
(define (get2nd x y) (make-num (num-sign y) (num-i y) (rdrop0s (num-f y))))
続きます。
Simple Scheme で電卓をつくってみる(7)
=を実装しましょう。
=が押されると、それまでの計算結果と最後に入力された数に対して最後の数の入力直前に押された演算ボタンの演算が適用されます*1。
演算ボタンとほとんど同じ機能ですが、=に割り当てられた演算は特にありません。というわけで基本は他の演算ボタンと同じ内容で、op
に関しては初期化するようにすればいいわけですね。ところで、今、op
の初期値はadd
です。ということは=ボタンは+ボタンと同じということですね。
(define (input=) (begin (and (symbol=? mode inputting) (begin (set! result (op result input)) (set! window result))) (set! op add) (set! mode waiting)))
それじゃ困るので、初期値を別の関数に変更します。第2引数を返す射影関数でよいですね。
(define (get2nd x y) y) (define op get2nd)
(define (input=) (begin (and (symbol=? mode inputting) (begin (set! result (op result input)) (set! window result))) (set! op get2nd) (set! mode waiting)))
— brv00 (@brv00) 2019年11月11日
=が押せると心理的に落ち着きますね(*´ω`*)
続いてクリアボタンを作りましょう。
ACとCの2つがありますね。
ACは完全に初期化するボタンです。
(define (inputAC) (begin (set! op get2nd) (set! result zero) (set! window result) (set! mode waiting)))
よく見るとinput
の初期化を忘れてますが、input
は次に書き替わるまで多分参照されないので問題ないです( ・∀・)
Cボタンは、数の入力中に押されると入力中の数だけを取り消します。
それ以外のときに押された場合はACと同じです*2。
(define (inputC) (if (symbol=? mode inputting) (begin (set! which-part 'int) (set! input zero) (set! window input)) (inputAC)))
— brv00 (@brv00) 2019年11月11日
続きます。
Simple Scheme で電卓をつくってみる(6)
Simple Schemeで電卓を作っています。
足し算と引き算を実行するコードができたので電卓に組み込みましょう。
演算ボタンが押されると何らかの関数が呼び出されるわけですが、その関数は下記のmake-op
で生成することにします。
(define op add) (define (make-op func) (lambda () (begin (and (symbol=? mode inputting) (begin (set! result (op result input)) (set! window result))) (set! op func) (set! mode waiting))))
電卓で演算ボタンが押されると、③直前に入力された数と、②その前に押された演算ボタンと、①その前までの計算結果の3つに対して①②③という演算*1が実行され、結果が画面に表示されます。
演算が実行されるのは(当然ですが)当該の演算ボタンが押されたときではありません。次に数字が入力されてそのあと演算ボタン*2が押されたときです。
というわけで演算ボタンが押されたとき、op
にその演算を記憶させておいてあとから取り出せるようにしておきましょう。
演算結果はresult
に保存されます。
op
とresult
の初期値はそれぞれadd
と0((make-num " " '(0) '())
)です*3。
この電卓は内部で2つのモードのいずれかを持っています。それぞれwaitingモードとinputtingモードと呼ぶことにしました。演算ボタンが押されてから数字ボタンが押されるまでがwaitingモードで数字ボタンが押されてから演算ボタンが押されるまでがinputtingモードです。
waitingモードとinputtingモードでの各ボタンが押されたときの挙動は次の表のようになります。
モード | 数字ボタン | 小数点ボタン | 演算ボタン |
---|---|---|---|
waiting | inputを0にリセットしてから入力された数字を追加する | inputを0にリセットしてから小数部分の入力に切り替える*4 | opを最新のものに変更する |
inputting | inputに入力された数字を追加する | 小数部分の入力に切り替える | 演算を行ってからopを最新のものに変更する |
つまり演算ボタンが押されると数の入力は一旦終わりで、次に数字ボタンや小数点ボタンが押されたときは新たな数の入力が始まったと見なされるわけです。
(waitingモード中の演算ボタンの入力は直前の演算ボタンの入力の取り消しです((その前に行われた)演算の取り消しではないことに注意))
数字や小数点の入力関数に、waitingモードのときの処理を追加しました。
(define waiting 'waiting) (define inputting 'inputting) (define mode waiting) (define (rcons xs x) (append xs (list x))) (define which-part 'int) (define (input-number n) (lambda () (begin (and (symbol=? mode waiting) (begin (set! mode inputting) (set! which-part 'int) (set! input zero))) (and (< (+ (length (num-i input)) (length (num-f input))) max-ndigits) (begin (if (symbol=? which-part 'int) (if (and (null? (cdr (num-i input))) (= 0 (car (num-i input)))) (set! input (make-num " " (list n) (num-f input))) (set! input (make-num " " (rcons (num-i input) n) (num-f input)))) (set! input (make-num " " (num-i input) (rcons (num-f input) n)))) (set! window input)))))) (define (input-point) (begin (and (symbol=? mode waiting) (begin (set! mode inputting) (set! input zero) (set! window input))) (set! which-part 'frac)))
動かしてみるとこんな感じです。
— brv00 (@brv00) 2019年11月8日
続きます。
Simple Scheme で電卓をつくってみる(5.7)
前回は電卓内で実行される計算の内、加減算の具体的なやり方を決めました。
電卓内で実際に保持される数はリスト型ではなくnum
型です。なので、前回のコードを利用してnum
の加減算を定義しましょう。
num
は整数部分と小数部分をリストで保持するstructureです。
前回決めたやり方で加減算をするには、加減算を行う2つの数の整数部分同士、小数部分同士でそれぞれ桁数を揃える必要があります。
それには、桁数が少ないほうに0を付加すればよいですね。整数部分は上位桁側に、小数部分は末尾桁側に付加することになります。付加すべき個数分の長さを持った0のみのリストを生成できると便利なのでそういう関数を定義します。
(define (get-padding xs n) (build-list (max 0 (- n (length xs))) (lambda (_) 0)))
8桁電卓では、桁をそろえたときに最上位から8桁目よりも後ろにある数は計算に使われないようです。
例えば、12.345678 - 9.3456778を計算すると、正しい値は3.0000002ですが、8桁電卓では3.000001になります。
9.3456778の整数部分の桁数を12.345678にそろえたとき、9.3456778の最後の8が9桁めになるため計算に使われなかったと考えると辻褄が合いますね。
というわけで数を8桁め*1まででそろえるために次のような関数を定義しましょう。
(define (trim xs n) (take (append xs (get-padding xs n)) n))
これを用いて桁を揃える―num
から前回の方法で加減算ができるようなリストを生成する―関数を定義します。
(define (align x y) (let* ((ix (num-i x)) (iy (num-i y)) (li (max (length ix) (length iy))) (ix (append (get-padding ix li) ix)) (iy (append (get-padding iy li) iy))) (values li (trim (append ix (num-f x)) max-ndigits) (trim (append iy (num-f y)) max-ndigits))))
12.345678と9.3456778の桁をそろえてみましょう。
(align (make-num " " '(1 2) '(3 4 5 6 7 8)) (make-num " " '(9) '(3 4 5 6 7 7 8)))
最初の2は整数部分の長さです。
これで、num
型の数を(小数点の位置と桁数がそろった)リストに変換できるようになりました。
足し算や引き算で得られた値を表すリストの先頭や末尾に余分な0がくっついている場合があります*2。
なので0を取り除く関数を定義しましょう。
(define (drop0s xs) (if (and (cons? xs) (= (car xs) 0)) (drop0s (cdr xs)) xs)) (define (rdrop0s xs) (reverse (drop0s (reverse xs))))
これでnum
に対する加減算を定義する準備が大体できましたが、リストで表された数に対する加減算はやり方しか書いていないので関数をちゃんと定義しておきます*3。
(define (lop op xs ys) (foldr (lambda (x ds) (append (i/% (+ x (car ds)) 10) (cdr ds))) '(0) (map2 op xs ys)))
では加減算のうち、足し算関数、のほうを先に定義しましょう。名前はadd
とします。
(define (add x y) (if (string=? (num-sign x) (num-sign y)) (let*-values (((li xs ys) (align x y)) ((zs) (lop + xs ys)) ((zs li) (if (= (car zs) 0) (values (cdr zs) li) (values zs (++ li))))) (make-num (num-sign x) (take zs li) (rdrop0s (drop (trim zs max-ndigits) (min li max-ndigits))))) (sub x (make-num (num-sign x) (num-i y) (num-f y)))))
(異符号同士の足し算は絶対値同士の引き算が必要なのであとで定義する引き算関数のsub
を呼び出して処理します)
(lop + xs ys)
が返すリストは、長さが9になっています。最上位桁での繰り上がりがなければ先頭が0になります。
この場合は先頭を削ればよいです。
最上位桁での繰り上がりがある場合は先頭は0ではないので残さないといけません。なので末尾を削ることになります。
どちらを削ればよいかを判断するには先頭を見ればよいということですね。
先頭を削る場合は先頭を見た直後に削ります(cdrします)が、末尾を削る場合は先頭の値を見た直後でなく、計算結果を整数部分と小数部分に分けてからにしました。そして小数部分だけから削っています。小数部分の長さが0のとき、言い替えると整数部分の長さが9のときはなにも削られないことになります。これは9桁すべてが整数部分のとき、つまり桁溢れを起こしたときに電卓の表示窓にエラー表示*4をしなければならないからです。
こうしておくと足し算の結果を受け取(って表示す)る側は、桁溢れがあったかどうかを整数部分の長さで知ることができるわけです。
(define rate-common (make-num " " '(2 7 6) '(4 8 7 5))) (define rate-leap (make-num " " '(8 8) '(7 5 5))) "276.4875 + 88.755" (add rate-common rate-leap) (define today (make-num " " '(2 0 1 9) '(1 1 0 8))) (define thelastday (make-num " " '(9 9 9 9 9) '(9 9 9))) "2019.1108 + 99999.999" (add today thelastday) (define today (make-num " " '(2 0 1 9 1 1 0) '(8))) (define thelastday (make-num " " '(9 9 9 9 9 9 9 9) '())) "2019110.8 + 99999999" (add today thelastday)
桁溢れを起こさない計算の結果は8桁に切り詰められ(末尾が0ならさらに切り詰められ)*5、桁溢れを起こす計算の結果は9桁になっています。
続いて引き算関数です。絶対値の大きい数から小さい数を引くのでまず比較関数を定義します。
(define (l<? xs ys) (and (cons? xs) (or (< (car xs) (car ys)) (and (= (car xs) (car ys)) (l<? (cdr xs) (cdr ys))))))
減数と被減数を入れ替えると答えの符号が変わるので符号を変える関数もあるとよいです*6。
(define (neg sign) (if (string=? sign " ") "-" " "))
(define (sub x y) (if (string=? (num-sign x) (num-sign y)) (let*-values (((li xs ys) (align x y)) ((sign zs) (if (l<? xs ys) (values (neg (num-sign x)) (cdr (lop - ys xs))) (values (num-sign x) (cdr (lop - xs ys))))) ((i) (drop0s (take zs li)))) (make-num sign (if (null? i) '(0) i) (rdrop0s (drop zs li))))) (add x (make-num (num-sign x) (num-i y) (num-f y))))
これを使って引き算をしてみましょう。
"1 - 0.2857142" (sub (make-num " " '(1) '()) (make-num " " '(0) '(2 8 5 7 1 4 2))) "12.345678 - 9.3456778" (sub (make-num " " '(1 2) '(3 4 5 6 7 8)) (make-num " " '(9) '(3 4 5 6 7 7 8)))
ちゃんと(?)9.3456778の最後の8が無視されていますね。
続きます。
*1:何桁電卓かはmax-ndigitsの値で決まるのですがここでは8ということにしておきます。
*2:もともと8桁より桁数が小さいときには余分な0をくっつけているのですが、それがなくても2数の最後の桁が例えば5と5だったら足しても引いても計算結果の最後の桁は0になります。
*3:加算か減算かも引数で指定できるやつ。
*4:端のほうに小さくEって出るやつ。
*5:2つめの結果は実際には整数部分5桁、小数部分3桁の合計8桁に揃えてから足し算をして得ていますが、端数切り捨てでも同じ結果が得られます。この2つの方法の間で結果が変わるのは引き算のときです。
*6:1回しか使ってないので関数はなくてもよいかもです。
Simple Scheme で電卓をつくってみる(5.3)
多倍長計算で加減算を行う関数を定義しましょう。
桁ごとに足してから繰り上がり、という順序で処理します。
桁ごとに足すためにmap2を定義します*1。
(define (map2 f l1 l2) (build-list (length l1) (lambda (i) (f (list-ref l1 i) (list-ref l2 i))))) "945+257" (map2 + '(9 4 5) '(2 5 7)) "945-257" (map2 - '(9 4 5) '(2 5 7))
この結果に対して繰り上がり処理をするわけです。
末尾の要素を10で割って商を1つ上位の桁に足し、新たな末尾とします。これを繰り返しながら10で割ったときに出る余りを右から順に並べると足し算の結果を10進法で表したリストが得られます。リストの末尾からの処理は、Simple Schemeの場合は、foldrが便利です。
(foldr (lambda (x ds) (cons (/ (+ x (car ds)) 10) (cons (% (+ x (car ds)) 10) (cdr ds)))) '(0) '(11 9 12))
945+257は1202ですね。正しい結果(正しい10進表現)が得られました。引き算のほうも同じようにしてみましょう。
(foldr (lambda (x ds) (cons (/ (+ x (car ds)) 10) (cons (% (+ x (car ds)) 10) (cdr ds)))) '(0) '(7 -1 -2))
945-257は10進法で表すと688ですがそうなっていません。
%
は、絶対値が除数より小さく、符号が被除数と同じであるような値を返す剰余関数のようです。
ここで欲しいのは、絶対値が除数より小さく、負でない値を返す剰余関数、つまり次のような関数です。
(define (i% x y) (let ((r (% x y))) (if (< r 0) (+ r y) r)))
商を得る関数も新たに定義しなければなりません。
/
は、除数をかけてから%
で得られる余りを足すともとの数が得られるような値を返す関数です。
ここで必要なのは、除数をかけてから上で定義したi%
で得られるような余りを足すともとの数が得られるような値を返す関数です。
剰余と商はリストにこの順でconsするので、この2つをまとめたリストを返す関数を定義しましょう。
これをappendするようにすれば(foldr ...)
も少しすっきりします。
(define (i/% x y) (let ((q (/ x y)) (r (% x y))) (if (< r 0) (list (-- q) (+ r y)) (list q r)))) (foldr (lambda (x ds) (append (i/% (+ x (car ds)) 10) (cdr ds))) '(0) '(7 -1 -2))
今度は正しい表現になっていますね*2。
続きます。
*1:Simple Schemeのmapは1つのリストにしか対応していないのです。
*2:いや先頭の0はいらんけど。
Simple Scheme で電卓をつくってみる(5)
数を入力できるようになりました。
入力した数は、整数部分と小数部分をそれぞれトップレベル変数で保持するようになっています。画面に表示するときはこれらの変数から直接数値を取り出してイメージに変換して表示しています。
しかし、この方法では入力した数値以外の数を表示できません。
そこで、表示するデータを保持する、window
という変数を別に作ることにしました。
数字を表示するときは、まず表示したい数をwindow
へ代入することになります。
代入を簡単にするために、structureを使って、整数部分と小数部分を1つにまとめることにしました。
(define-struct num (sign i f)) (define input (make-num " " '(0) '())) (define window input) (define result #f) (define memory input)
数を保持するstructureの型名はnum
です。num
には整数部分と小数部分だけでなく符号も加えました。入力内容が負数になることはありませんが、計算結果やメモリの中身が負数になることがあるからです*1。
数字ボタンをタップするとinput-number
内で生成された関数が起動します。この関数の最後にwindow
への代入処理を追加しました。
*2
(define (rcons xs x) (append xs (list x))) (define which-part 'int) (define (input-number n) (lambda () (and (< (+ (length (num-i input)) (length (num-f input))) max-ndigits) (begin (if (symbol=? which-part 'int) (if (and (null? (cdr (num-i input))) (= 0 (car (num-i input)))) (set! input (make-num " " (list n) (num-f input))) (set! input (make-num " " (rcons (num-i input) n) (num-f input)))) (set! input (make-num " " (num-i input) (rcons (num-f input) n)))) (set! window input)))))
(define (overlay-window scn) (let* ((lis (map number->string (append (num-i window) (num-f window)))) (wdx (* width (/ 3/4 (+ max-ndigits 1/2)))) (iwdx (round wdx)) (wconv-x (lambda (x) (round (+ (* width 1/8) (* wdx (+ x (- max-ndigits (length lis)) 1)))))) (y (/ width 8))) (foldl (lambda (x scn) (place-image (text (car x) iwdx "black") (cadr x) y scn)) (place-image (text "." iwdx "black") (wconv-x (- (length (num-i window)) 0.5)) y (place-image (text (num-sign window) iwdx "black") (wconv-x -0.8) y scn)) (build-list (length lis) (lambda (i) (list (list-ref lis i) (wconv-x i)))))))
入力した数字の表示部分を少し広げ、数字のサイズを少し小さくしました。
また、数字の表示位置を少し右にずらしました。
これらの変更の目的は、符号を表示するための場所を空けることです。
*3
→古いバージョンの数字のサイズ(wdx
)と数字位置決定関数(wconv-x
)
枠をつけました。この場合keyboard
がバインドしているのは電源オフ状態の電卓のイメージですね。じゃあkeyboardという名前をやめろってなりますけど、どうしましょう。また今度考えます。
(define keyboard (place-image (round-rectangle (round (* 27/34 width)) 150 5 "outline" "gray") (/ width 2) (/ width 7) (place-buttons 0 0 buttons (empty-scene))))
続きます。
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 ;入力後
この関数のリストを作ります。
リストの並び順はbuttons
やbutton-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-part
とf-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))))
— brv00 (@brv00) 2019年11月2日
数値の表示部分に枠がないと落ち着きませんね😌
続きます。