Haskell メモ : Threepenny-gui とは その2
24 Days of Hackage: threepenny-gui
この記事は,24 Days of Hackage: threepenny-gui の抄訳です。
FRPの本を出版した Heinrich Apfelmus 氏 が Haskell 用の GUI ライブラリ threepenny-gui を発表した。
- GTK や QT など既存の GUI ライブラリと組み合わせるタイプではなく,ブラウザ用の html ページを作成するタイプ。
- 汎用のユーザーインターフェースを組み上げてページにしていく。もちろん,インターフェース要素同士の相互作用もあり。
- websocket を使い,クライアントーサーバ間の緊密なフィードバックループも作成可能。
To Do リストアプリの作成事例
基本となる型
この事例で利用する型は以下の通り:
type Database = Map UserName ToDoList
type UserName = String
type ToDoList = Set String
Database
(To-Do項目のデータベース)は, ユーザ名から to-do リストへの Map
になっている。内部では,ユーザ名は単なる文字列,to-do リストは文字列のSet
(集合)として格納されている。
threepenny-gui の起動
main :: IO()
main = startGUI defaultConfig setup
setup :: Window -> UI ()
setup rootWindow = undefined
startGUI
は,HTTP サーバを起動している。ここでは初期設定をそのまま使っているので, 受信ポート番号は 10000 である。setup
では,ユーザインタフェース要素によって組み立てられ,ユーザが目にするブラウザページとなる Window
を引数にとる。
すべてのクライアントとの接続それぞれに setup
が呼び出されるので,通信内容や状態はそれぞれ隔絶されている。従い,何らかの相互通信が必要な場合,共有変数などを使って明示的に共有しておく必要がある。そこで,STM を使ったミュータブル変数を使う。
main :: IO ()
main = do
database <- atomically $ newTVar (Map.empty)
startGUI defaultConfig (setup database)
setup :: TVar Database -> Window -> UI ()
setup database rootWindow = do
atomically
やTVar
についてはSTMを学ぶと理解できる。魚野注
これで,UI を組み立てる準備ができた。まず,どの人のto-doリストを表示するかを決めるのに使うため,顧客のユーザ名の入力をしてもらう。
setup :: TVar Database -> Window -> UI ()
setup database rootWindow = void $ do
userNameInput <- UI.input
# set (attr "placeholder") "User name"
loginButton <- UI.button #+ [ string "Login" ]
getBody rootWindow #+
map element [ userNameInput, loginButton ]
まず呼び出しているのが,UI.input
である。これは,新しい<input>
要素の作成に使われる。演算子#
は,関数と引数の順序を逆にする。
魚野注
(#) :: a -> (a->b) -> b a # f = f a
ここでは新たに作成したテキスト欄の placeholder 属性を変更していることになる。
ログインボタンを作成するところでは, UI.button を使用している。また,ボタン要素の子要素として,テキスト内容を示す文字列を追加している。
これで,UI 要素の両方を作ることができた。これをrootWindow
の UI 要素として追加しておこう。getBody
はWindow
をとってそのウィンドウに属する要素を返す。
魚野注 hackage での説明より
(#+) :: UI Element -> [UI Element] -> UI Element
DOM 要素を与えられた要素に子要素として与える
次に,ユーザが"Login"ボタンを押したときに反応することを考える。クリックイベントを観測しなければいけない。
on UI.click loginButton $ \_ -> do
誰かが"Login"ボタンをクリックしたら,該当するユーザ名をデータベースから探し,存在すれば,そのユーザが作成済みのto-doアイテムを表示する。また,新規項目追加用の入力欄も表記する必要があるだろう。
userName <- get value userNameInput
currentItems <- fmap Set.toList $ liftIO $ atomically $ do
db <- readTVar database
case Map.lookup userName db of
Nothing -> do
writeTVar database (Map.insert userName Set.empty db)
return Set.empty
Just items -> return items
userNameInput
欄の値を受け取り,STMトランザクション処理に入って全てのto-do項目を入手する。- 入力されたユーザ名でデータベースを検索し,もし見つかれば格納されているそのユーザのto-do項目を返す。そうでなければ新しいユーザとしてその人を「登録」し,空のto-do項目を返す。
次に,ユーザのto-do項目を表示する部分である。
let showItem item = UI.li #+ [ string item ]
toDoContainer <- UI.ul #+ map showItem currentItems
showItem
をto-doの項目として一つ一つ処理していき,<ul>
コンテナの中に入れていく。新しい項目を追加する際にはこのリストに追加するので,<ul>
の要素には名前をつけておかなければならない。
備えるべき機能の最後の駒は,新しいto-do項目を追加する機能だ。このためには,別の<input>
要素を利用する。ユーザが,項目が入力された状態の時にリターンキーを押したら,リストにその項目を追加する。
newItem <- UI.input
on UI.sendValue newItem $ \input -> do
liftIO $ atomically $ modifyTVar database $
Map.adjust (Set.insert input) userName
set UI.value "" (element newItem)
element toDoContainer #+ [ showItem input ]
新しく入力された項目が作成されたら,sendValue
イベントを待つことになる。このイベントは,クライアントがリターンキーを押した時に作成される。そして,小さなSTMトランザクションを実行し,新しい項目をto-doリストに付け加える。このトランザクションが完了したら,入力欄の記載内容を空にし,to-doリストに新たに付け加えられた項目を追加する。
最後に,全てをUIに組み込む。
header <- UI.h1 #+ [ string $ userName ++ "'s To-Do List" ]
set children
[ header, toDoContainer, newItem ]
(getBody rootWindow)
もう一度アクセス先(http://localhost:10000)に戻ろう。ユーザ名欄が表示されるはずだ。この欄に入力すれば,そのユーザのto-do項目が表示される。値を変えて同じユーザとして再度ログインしても,項目は残っているはずだ。
50行足らずのコードを書いただけで,立派に機能するアプリケーションが作成できた。完成したコードは,GitHubの現著者のリポジトリに置いてある。