実際のウィジェット オブジェクトを持つことは、非常にオブジェクト指向です。関数型の世界で一般的に使用される手法は、代わりに関数型リアクティブ プログラミング (FRP) を使用することです。FRP を使用すると、純粋な Haskell のウィジェット ライブラリがどのようになるかを簡単に説明します。
tl/dr:「ウィジェット オブジェクト」を処理せず、代わりに「イベント ストリーム」のコレクションを処理し、どのウィジェットから、またはそれらのストリームがどこから来るかは気にしません。
FRP にはEvent a
、無限リストと見なすことができるの基本的な概念があります[(Time, a)]
。したがって、カウントアップするカウンターをモデル化する場合は[(00:01, 1), (00:02, 4), (00.03, 7), ...]
、特定のカウンター値を特定の時間に関連付ける のように記述します。押されているボタンをモデル化したい場合は、[(00:01, ButtonPressed), (00:02, ButtonReleased), ...]
モデル化された値が連続的であることを除いて、一般に a と呼ばれるものもありSignal a
、これは に似ています。Event a
特定の時間に個別の値のセットを持っているわけではありませんが、Signal
その値をたとえばで尋ねることができ00:02:231
、値4.754
または何かが得られます。信号は、病院の心拍数計 (心電図装置/ホルター モニター) のようなアナログ信号と考えてください。これは、上下にジャンプするが「ギャップ」を作ることのない連続した線です。たとえば、ウィンドウには常にタイトルがあるため (ただし、おそらく空の文字列です)、いつでもその値を尋ねることができます。
GUI ライブラリでは、低レベルでmouseMovement :: Event (Int, Int)
andmouseAction :: Event (MouseButton, MouseAction)
または something があります。これmouseMovement
は実際の USB/PS2 マウス出力であるため、イベントとして位置の違いのみを取得します (たとえば、ユーザーがマウスを上に移動すると、イベントが取得されます(12:35:235, (0, -5))
。その後、「統合」またはむしろ「蓄積」することができます)。mousePosition :: Signal (Int, Int)
マウスの絶対座標を取得するための移動イベント。mousePosition
タッチ スクリーンなどの絶対ポインティング デバイスや、マウス カーソルの位置を変更する OS イベントも考慮に入れることができます。
同様に、キーボードの場合は がありkeyboardAction :: Event (Key, Action)
、そのイベント ストリームを に「統合」してkeyboardState :: Signal (Key -> KeyState)
、任意の時点でキーの状態を読み取れるようにすることもできます。
画面に何かを描画したり、ウィジェットを操作したりする場合、事態はさらに複雑になります。
ウィンドウを 1 つだけ作成するには、次のような「魔法の関数」を使用します。
window :: Event DrawCommand -> Signal WindowIcon -> Signal WindowTitle -> ...
-> FRP (Event (Int, Int) {- mouse events -},
Event (Key, Action) {- key events -},
...)
OS固有の関数を呼び出してウィンドウを作成する必要があるため、関数は魔法のようになります(OS自体がFRPでない限り、しかし私はそれを疑っています)。これがモナドにある理由でもあります。舞台裏でモナドのandなどFRP
を呼び出すからです。createWindow
setTitle
registerKeyCallback
IO
もちろん、これらすべての値をデータ構造にグループ化して、次のようにすることもできます。
window :: WindowProperties -> ReactiveWidget
-> FRP (ReactiveWindow, ReactiveWidget)
これらWindowProperties
は、ウィンドウの外観と動作を決定するシグナルとイベントです (たとえば、閉じるボタンが必要かどうか、タイトルはどうあるべきかなど)。
はReactiveWidget
、アプリケーション内からマウス クリックをエミュレートする場合に備えて、キーボード イベントとマウス イベントである S&E をEvent DrawCommand
表し、 はウィンドウに描画するもののストリームを表します。このデータ構造は、すべてのウィジェットに共通です。
ReactiveWindow
はウィンドウの最小化などのイベントを表し、出力はReactiveWidget
外部/ユーザーからのマウスとキーボードのイベントを表します。
次に、実際のウィジェット、たとえばプッシュ ボタンを作成します。署名があります:
button :: ButtonProperties -> ReactiveWidget -> (ReactiveButton, ReactiveWidget)
はButtonProperties
ボタンの色/テキストなどを決定し、ボタンの状態を読み取るためにand などをReactiveButton
含みます。Event ButtonAction
Signal ButtonState
このbutton
関数は、イベントやシグナルなどの純粋な FRP 値にのみ依存するため、純粋な関数であることに注意してください。
ウィジェットをグループ化する (たとえば、水平方向に積み重ねる) 場合は、次のように作成する必要があります。
horizontalLayout :: HLayoutProperties -> ReactiveWidget
-> (ReactiveLayout, ReactiveWidget)
HLayoutProperties
には、境界線のサイズに関する情報とReactiveWidget
、含まれるウィジェットの が含まれます。には、子ウィジェットごとに 1 つの要素を持つReactiveLayout
が含まれます。[ReactiveWidget]
レイアウトが行うことは、レイアウト内Signal [Int]
の各ウィジェットの高さを決定する内部を持つことです。次に、入力からすべてのイベントを受け取りReactiveWidget
、パーティション レイアウトに基づいてReactiveWidget
、イベントを送信する出力を選択します。同時に、マウス イベントなどの発生元をパーティション オフセットによって変換します。
この API がどのように機能するかを示すために、次のプログラムを検討してください。
main = runFRP $ do rec -- Recursive do, lets us use winInp lazily before it is defined
-- Create window:
(win, winOut) <- window winProps winInp
-- Create some arbitrary layout with our 2 widgets:
let (lay, layOut) = layout (def { widgets = [butOut, labOut] }) layInp
-- Create a button:
(but, butOut) = button butProps butInp
-- Create a label:
(lab, labOut) = label labProps labInp
-- Connect the layout input to the window output
layInp = winOut
-- Connect the layout output to the window input
winInp = layOut
-- Get the spliced input from the layout
[butInp, layInp] = layoutWidgets lay
-- "pure" is of course from Applicative Functors and indicates a constant Signal
winProps = def { title = pure "Hello, World!", size = pure (800, 600) }
butProps = def { title = pure "Click me!" }
labProps = def { text = reactiveIf
(buttonPressed but)
(pure "Button pressed") (pure "Button not pressed") }
return ()
(def
はData.Default
からdata-default
)
これにより、次のようなイベント グラフが作成されます。
Input events -> Input events ->
win ---------------------- lay ---------------------- but \
<- Draw commands etc. \ <- Draw commands etc. | | Button press ev.
\ Input events -> | V
\---------------------- lab /
<- Draw commands etc.
どこにも「ウィジェット オブジェクト」がある必要はないことに注意してください。レイアウトは、パーティショニング システムに従って入力イベントと出力イベントを変換する単純な機能であるため、ウィジェットにアクセスできるイベント ストリームを使用するか、別のサブシステムにストリーム全体を生成させることができます。ボタンとラベルについても同じことが言えます。これらは、クリック イベントを描画コマンドなどに変換する単純な関数です。これは完全なデカップリングの表現であり、その性質上非常に柔軟です。