本記事では、GUI部分の実装と計算部分の実装について書いていきます。この記事は連載記事「Haskell初心者が電卓アプリを作る」の2回目の記事です。
- 「Haskell初心者が電卓アプリを作る : 1」
- 「GUIと計算部分の実装 (Haskell初心者が電卓アプリを作る : 2)」← 今ここ
- 「Haskellでstateパターンを実装する (Haskell初心者が電卓アプリを作る : 3)」
- 「hspecでHaskellのテストコードを書く (Haskell初心者が電卓アプリを作る : 4)」
ソースコードは以下にアップしています。
https://github.com/daikon-oroshi/haskell-calculator-sample
目次
GUI の実装
GUI ライブラリ
GUI は GTK のバインディングである gi-gtk を用いました。gi-gtk を使うには依存パッケージのインストールが必要です。インストール方法は gi-gtk の README に書いてあります。mac なら以下を実行してください。
brew install gobject-introspection gtk+ gtk+3
その後、stack の package.yaml の dependencies に haskell-gi-base と gi-gtk を追加すると stack build 時にコンパイルしてくれます。
dependencies:
- base >= 4.7 && < 5
- haskell-gi-base
- gi-gtk
電卓画面
電卓アプリの画面は以下の画像の様になりました。配置の構造について解説します。 (見た目の調整には手を付けていません。)
要素の配置構造は概ね以下の画像のようになっています。
Box という要素があり、その中に要素を配置していく仕組みで、Box の小要素の配置の方向を水平か垂直のどちらか指定できます。以下の様にして Box に個要素を追加していきます。
import qualified GI.Gtk as Gtk
...
root_box <- new Gtk.Box [#orientation := Gtk.OrientationVertical]
text_view <- new Gtk.Entry [ #maxLength := 11, #editable := False]
keys_box <- new Gtk.Box [#orientation := Gtk.OrientationHorizontal]
#add root_box text_view
#add root_box keys_box
root_box は上画像には明示していませんが一番外側の Box で、子要素を垂直方向に配置します。text_view は上の赤枠の要素で、keys_box は下の赤枠の要素です。追加した要素から順に上から配置されていきます。key_box は青枠の2つの要素を小要素として持っており、水平方向に配置します。
表示の変更
以下の様にして変更できます。
buffer <- Gtk.getEntryBuffer text_view
Gtk.setEntryBufferText buffer some_text
イベントハンドラの登録
Button に対して以下の様にしてイベントハンドラを登録できます。callback_func の型は IO() です。
btn <- new Gtk.Button [ #label := "tmp" ]
_ <- on btn #clicked callback_func
ちなみに
_ <- after btn #clicked callback_func
もあるようで、default handler の前に実行されるか後に実行されるかの違いがあるようです。
その他
gi-gtk のサンプルコードは検索しても引っかからないので、使いたい場合は hackage と hoogle をよく読む必要があります。慣れればこの2つあれば十分です。
計算部分の実装
基本的な考え方
電卓では普通の表示だけでなく、”0.” や “0.00” のような入力途中のものを表示する必要があります。なので、普通の数値を扱うだけでは不十分です。文字列のまま保持しつつ、計算の時に read で数値に変換する方法も考えられますが、あまり美しくないので、電卓用の新しい数値の型を作りました。
data ExpNotation = ExpNotation {
_exponent :: Maybe Int,
_significand :: Int
}
これは指数表記の様なもので、
$$ \begin{cases} \textrm{_significand} & (\textrm{_exponent} = \textrm{Nothing}) \\ \textrm{_significand}\ /\ 10^{e} & (\textrm{_exponent} = \textrm{Just} \ e) \end{cases}$$
を表します。_exponent を Maybe Int にしているのは、”0″ と “0.” を区別するためです。
実装
電卓プログラムを動かすためには数値の型が 4 つのメソッドを持てば十分です。
data Operation = Plus | Sub | Prod | Div deriving (Show, Eq)
class (Num a, Fractional a) => CalcValue a where
-- |
-- 数字を末尾に追加する
addDigit :: a -> Int -> a
-- |
-- 小数点をつける
dot:: a -> a
-- |
-- 表示する
display :: a -> String
-- |
-- 計算する
calculate :: a -> a -> Maybe Operation -> a
calculate _ _ Nothing = 0
calculate dv1 dv2 (Just x)
| x == Plus = dv1 + dv2
| x == Sub = dv1 - dv2
| x == Prod = dv1 * dv2
| otherwise = dv1 / dv2
calculate は Num クラスかつ Fractional クラスであれば定義できるので、個別に定義する必要はありません。具体的な実装は以下のとおりです。
instance CalcValue ExpNotation where
dot :: ExpNotation -> ExpNotation
dot ExpNotation {_exponent = Nothing, _significand = x}
= ExpNotation {_exponent = Just 0, _significand = x}
dot dv = dv
addDigit :: ExpNotation -> Int -> ExpNotation
addDigit dv a
| numOfDigits dv > limitOfDigits = dv
| isNothing (_exponent dv)
= dv {
_significand = addDigitToLast (_significand dv) a
}
| isJust (_exponent dv) && a == 0 && _significand dv == 0
= dv {
_exponent = fmap (+1) (_exponent dv),
_significand = 0
}
| otherwise =
ExpNotation {
_exponent = fmap (+1) (_exponent dv),
_significand = addDigitToLast (_significand dv) a
}
display :: ExpNotation -> String
display ExpNotation {
_exponent = Nothing,
_significand = x
} = show x
display ExpNotation {
_exponent = Just e,
_significand = x
}
| x == 0 =
"0." ++ concat (replicate e "0")
| otherwise =
let
sign_str = if x > 0 then "" else "-"
abs_x = abs x
digits = numOfDigits abs_x
in
if digits > e
then
let
(int_part, deci_part) = splitAt (digits - e) $ show abs_x
in sign_str ++ int_part ++ "." ++ deci_part
else
sign_str ++ "0." ++ concat (replicate (e - digits) "0") ++ show abs_x
numOfDigits は ExpNotation の桁数を取る関数です。パターンマッチのおかげで書きやすいです。
0 で割る
0 での割り算は Nothing にする方法が考えられますが、(/) の型の問題があり ExpNotation のままではできません。
ghci> :t (/)
(/) :: Fractional a => a -> a -> a
Haskell には Infinity が定義されており (1 / 0 = Infinity)、それを使う方法も考えましたが、-Infinity と NaN も扱わないといけないのでめんどくさくてやめました。別の割り算メソッドを定義する方法もありますが、今回はやめました。excetion を使う方法は、exception の扱い方がわからなのでやめました。
そこで、Maybe ExpNotation を CalcValue クラスにすることで、zeroDivisionError を回避しました。コードを一部抜粋します。
type MExpNotation = Maybe ExpNotation
instance Fractional MExpNotation where
(/) :: MExpNotation -> MExpNotation -> MExpNotation
Nothing / _ = Nothing
_ / Nothing = Nothing
_ / (Just 0) = Nothing
(Just dv1) / (Just dv2) = Just (dv1 / dv2)
fromRational :: Rational -> MExpNotation
fromRational a = Just (fromRational a)
instance CalcValue MExpNotation where
dot :: MExpNotation -> MExpNotation
dot = fmap dot
addDigit :: MExpNotation -> Int -> MExpNotation
addDigit Nothing _ = Nothing
addDigit (Just dv) a = Just (addDigit dv a)
display :: MExpNotation -> String
display Nothing = "Error"
display (Just dv) = display dv
display Nothing = “Error” としているで、一度 Nothing になるとどのような計算をしても Error と表示されます。Error状態を追加して計算をできなくしても良かったのですが、面倒なのでやめました。
ちなみに上記のような書き方はそのままではコンパイルが通りません。以下のおまじないを追加するとコンパイルが通ります。
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances, InstanceSigs #-}
桁あふれ
計算が12桁を超えた場合は、先頭の12桁を取るようにしました。これをエラー扱いするなら exception を用いた方がいいように思います。
ご支援のお願い
記事を読んで、「支援してもいいよ」と思っていただけましたら、ご支援いただけると幸いです。サーバー維持費などに充てさせていただきます。登録不要で、100円から寄付でき、金額の90%がクリエイターに届きます。