bitterharvest’s diary

A Bitter Harvestは小説の題名。作者は豪州のPeter Yeldham。苦闘の末に勝ちえた偏見からの解放は命との引換になったという悲しい物語

Reactive-bananaの紹介(3)

4.Currency Converter

2回目の話題は外国為替変換である。ドルとユーロでの変換を自動的に行ってくれるもので、エントリーは、ドルとユーロの金額入力(表示も)の二つである。マウスでどちらかのエントリーをクリックすると、クリックしたエントリーがフォーカスされ、入力できるようになる。キーから数字が入力されると、瞬時に、相手側の通貨にも反映される。即ち、50ドルを入力しようとして、最初に5を入力すると、5ドルに対するユーロの金額が表示される。続いて0を入力すると、50ドルに対応したユーロの金額が表示される。また、数字以外の文字記号が入力された時は、相手側には、"-"が表示される。

4.1 フレーム、パネル、テキストフィールド

前回は、フレームとテキストフィールドであったが、今回は、フレームの上にパネルを張り、その上に、二つのテキストフィールドを作成する。これは、次のようにする。なお、フレームのラベルは"Currency Converter"とする。

    f        <- frame   [ text := "Currency Converter", tabTraversal := True ]
    p        <- panel f []  -- Use panel for tab traversal
    dollar   <- entry p []
    euro     <- entry p []

パネルの表示画面上での大きさを次のように与える。なお、パネルにもラベルを付けて、"Amounts update while typing."とする。

    set p [layout := margin 10 $
            column 10 [
                grid 10 10 [[label "Dollar:", widget dollar],
                            [label "Euro:"  , widget euro  ]]
            , label "Amounts update while typing."
            ]]

フレームも同じように配置する。ついでに、ドルのテキストフィールドをフォーカスする。

    set f [layout := widget p]
    focusOn dollar

初期画面は次のようになる。
f:id:bitterharvest:20151129092254p:plain

4.2 金額の入力と相手側通貨での金額

それでは通貨を入力し、相手側の通貨での金額を求める関数withStringを説明しよう。これは次のようになっている。

            withString f s
                = maybe "-" (printf "%.2f") . fmap f 
                $ listToMaybe [x | (x,"") <- reads s] 

前回はreadsの説明をしなかったが、これは便利な関数である。

これと似た関数で、readを知っていると思うが、これは文字列を受け取り、readのインスタンスの型の値を返していた。
例えば、

read "True"

は、インスタンスの型がBoolなので、その値のTrueを返す。

read "3" + 7

は、インスタンスの型がNumなので、その値の10を返す。

しかし、

read "3"

とすると、エラーとなる。これは、インスタンスの型が定まらないためである。次のような場合は大丈夫である。

read "\"3\"" ++ "567"

readだと、インスタンスの型が要求しているものと異なるときはエラーを起こす。これを避けたい場合に利用するのが、readsである。例で見ていくことにする。今、50ドルをユーロに替えたときの金額を知りたかったとする。この時、50と入力するので、次のようになる。

Prelude> reads "50" :: [(Float, String)]
[(50.0,"")]

また、誤って、50zと入力したとすると、次のようになる。

Prelude> reads "50z" :: [(Float, String)]
[(50.0,"z")]

あるいは、$50と入力したとすると、

Prelude> reads "$50" :: [(Float, String)]
[]

このように入力に誤りがあったとしても、プログラムは停止することなく、どのようなことが起きているかを伝えてくれる。

入力された値は、readsでいったんリストに替えられ、内包表記で正しく入力されたものだけが、リストとして返される。次の例を見て欲しい。

Prelude> [x| (x,"") <- (reads "50" :: [(Float, String)])]
[50.0]
Prelude> [x| (x,"") <- (reads "50z" :: [(Float, String)])]
[]
Prelude> [x| (x,"") <- (reads "$50" :: [(Float, String)])]
[]
Prelude> 

ここで得たリストを、Maybe Float型に変更すると次のようになる。

Prelude> import Data.Maybe
Prelude Data.Maybe> listToMaybe [x| (x,"") <- (reads "50" :: [(Float, String)])]
Just 50.0
Prelude Data.Maybe> listToMaybe [x| (x,"") <- (reads "50z" :: [(Float, String)])]
Nothing
Prelude Data.Maybe> listToMaybe [x| (x,"") <- (reads "$50" :: [(Float, String)])]
Nothing 

入力間違いはNothingで、数値はJustで返ってくる。

関数withStringでの変数fは、レートを計算するための式である。このfを用いて、相手側通貨での金額が得られる。入力された金額は、Maybe Float型なので、fmap fで相手通貨への変更を行う。この時、出力は、入力に間違いがあるときは"-"が、そうでなければ金額が出力される。

この関数を利用して、ドルからユーロ、ユーロからドルへの変更は次のようになる。

            dollarOut, euroOut :: Behavior String
            dollarOut = withString (/ rate) <$> euroIn
            euroOut   = withString (* rate) <$> dollarIn

ここで、euroInとdollarInは、dollarとeuroのテキストフィールドの振舞いを表したもので、次のように定義されている。

        euroIn   <- behaviorText euro   "0"
        dollarIn <- behaviorText dollar "0"

4.3 金額の変更をアニメーションに

それぞれの振舞いをアニメーションの用に示せばよいので、次のようになる。これは、金額に変更があった時にイベントが発生し、表示が変わることに注意してほしい。

        sink euro   [text :== euroOut  ]
        sink dollar [text :== dollarOut] 

4.4 プログラム全体

プログラムは、このホームページに掲載されいるが、転載する。

{-----------------------------------------------------------------------------
    reactive-banana-wx
    
    Example: Currency Converter
------------------------------------------------------------------------------}

import Data.Maybe
import Text.Printf

import Graphics.UI.WX hiding (Event)
import Reactive.Banana
import Reactive.Banana.WX

{-----------------------------------------------------------------------------
    Main
------------------------------------------------------------------------------}
main :: IO ()
main = start $ do
    f        <- frame   [ text := "Currency Converter", tabTraversal := True ]
    p        <- panel f []  -- Use panel for tab traversal
    dollar   <- entry p []
    euro     <- entry p []
    
    set p [layout := margin 10 $
            column 10 [
                grid 10 10 [[label "Dollar:", widget dollar],
                            [label "Euro:"  , widget euro  ]]
            , label "Amounts update while typing."
            ]]
    set f [layout := widget p]
    focusOn dollar

    let networkDescription :: MomentIO ()
        networkDescription = do
        
        euroIn   <- behaviorText euro   "0"
        dollarIn <- behaviorText dollar "0"
        
        let rate = 0.7 :: Double
            withString f s
                = maybe "-" (printf "%.2f") . fmap f 
                $ listToMaybe [x | (x,"") <- reads s] 
        
            -- define output values in terms of input values
            dollarOut, euroOut :: Behavior String
            dollarOut = withString (/ rate) <$> euroIn
            euroOut   = withString (* rate) <$> dollarIn
    
        sink euro   [text :== euroOut  ]
        sink dollar [text :== dollarOut] 

    network <- compile networkDescription    
    actuate network