34

GUI ウィジェット クラスの階層を構築することは、オブジェクト指向プログラミングのほぼ標準的な作業です。他のウィジェットを含めることができるウィジェットの抽象サブクラスを持つある種の抽象Widgetクラスがあり、テキスト表示をサポートするウィジェット、入力フォーカスであることをサポートするウィジェット、ブール値を持つウィジェットのさらなる抽象クラスが多数あります。ボタン、スライダー、スクロールバー、チェックボックスなどの実際の具体的なクラスに至るまで。

私の質問は: Haskell でこれを行う最善の方法は何ですか?

Haskell GUI の構築を困難にするものはたくさんありますが、それは私の質問の一部ではありません。Haskell でインタラクティブな I/O を実行するのは、少しトリッキーです。GUI を実装することは、ほとんどの場合、非常に低レベルの C または C++ ライブラリにラッパーを記述することを意味します。また、そのようなラッパーを作成する人は、既存の API をそのままコピーする傾向があります (おそらく、ラップされたライブラリを知っている人なら誰でも安心できるでしょう)。これらの問題は、現時点では興味がありません。Haskell でサブタイプのポリモーフィズムをモデル化する最善の方法に純粋に興味があります。

架空の GUI ライブラリからどのようなプロパティが必要になるでしょうか? そうですね、いつでも新しいウィジェット タイプを追加できるようにしたいと考えています。(つまり、可能なウィジェットの閉じたセットは良くありません。) コードの重複を最小限に抑えたいと考えています。(たくさんのウィジェット タイプがあります!) 理想的には、必要に応じて 1 つの特定のウィジェット タイプを規定できるようにしたいだけでなく、必要に応じて任意のウィジェット タイプのコレクションを処理できるようにしたいと考えています。

上記のすべてはもちろん、自尊心のある OO 言語では些細なことです。しかし、Haskell でこれを行う最善の方法は何でしょうか? いくつかのアプローチを考えることができますが、どれが「最良」になるかはわかりません。

4

3 に答える 3

33

実際のウィジェット オブジェクトを持つことは、非常にオブジェクト指向です。関数型の世界で一般的に使用される手法は、代わりに関数型リアクティブ プログラミング (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を呼び出すからです。createWindowsetTitleregisterKeyCallbackIO

もちろん、これらすべての値をデータ構造にグループ化して、次のようにすることもできます。

window :: WindowProperties -> ReactiveWidget
       -> FRP (ReactiveWindow, ReactiveWidget)

これらWindowPropertiesは、ウィンドウの外観と動作を決定するシグナルとイベントです (たとえば、閉じるボタンが必要かどうか、タイトルはどうあるべきかなど)。

ReactiveWidget、アプリケーション内からマウス クリックをエミュレートする場合に備えて、キーボード イベントとマウス イベントである S&E をEvent DrawCommand表し、 はウィンドウに描画するもののストリームを表します。このデータ構造は、すべてのウィジェットに共通です。

ReactiveWindowはウィンドウの最小化などのイベントを表し、出力はReactiveWidget外部/ユーザーからのマウスとキーボードのイベントを表します。

次に、実際のウィジェット、たとえばプッシュ ボタンを作成します。署名があります:

button :: ButtonProperties -> ReactiveWidget -> (ReactiveButton, ReactiveWidget)

ButtonPropertiesボタンの色/テキストなどを決定し、ボタンの状態を読み取るためにand などをReactiveButton含みます。Event ButtonActionSignal 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 ()

(defData.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.

どこにも「ウィジェット オブジェクト」がある必要はないことに注意してください。レイアウトは、パーティショニング システムに従って入力イベントと出力イベントを変換する単純な機能であるため、ウィジェットにアクセスできるイベント ストリームを使用するか、別のサブシステムにストリーム全体を生成させることができます。ボタンとラベルについても同じことが言えます。これらは、クリック イベントを描画コマンドなどに変換する単純な関数です。これは完全なデカップリングの表現であり、その性質上非常に柔軟です。

于 2012-08-17T10:44:45.327 に答える
10

wxHaskell GUI ライブラリは、ファントム型をうまく利用してウィジェット階層をモデル化します。

アイデアは次のとおりです。すべてのウィジェットは同じ実装を共有します。つまり、それらは C++ オブジェクトへの外部ポインターです。ただし、これは、すべてのウィジェットが同じタイプである必要があるという意味ではありません。代わりに、次のような階層を構築できます。

type Object a = ForeignPtr a

data CWindow a
data CControl a
data CButton a

type Window  a = Object  (CWindow a)
type Control a = Window  (CControl a)
type Button  a = Control (CButton a)

このように、 type の値Control Aも type と一致するWindow bため、コントロールをウィンドウとして使用できますが、その逆はできません。ご覧のとおり、サブタイプはネストされた型パラメーターを介して実装されています。

この手法の詳細については、wxHaskell に関する Dan Leijen の論文のセクション 5 を参照してください。


この手法は、ウィジェットの実際の表現が均一、つまり常に同じである場合に限定されているように見えることに注意してください。ただし、少し考えれば、ウィジェットの表現が異なる場合にも拡張できると確信しています。

特に、オブジェクト指向は、次のようにデータ型にメソッドを含めることでモデル化できることが観察されています。

data CWindow a = CWindow
    { close   :: IO ()
    , ...
    }
data CButton a = CButton
    { onClick :: (Mouse -> IO ()) -> IO ()
    , ...
    }

サブタイピングは定型文を節約するかもしれませんが、必須ではありません。

于 2012-08-23T12:13:35.523 に答える
7

サブタイプのポリモーフィズムなど、Haskell で実行できる OOP を理解するには、OOHaskellを参照してください。これにより、さまざまな強力な OOP 型システムのセマンティクスが再現され、ほとんどの型推論が保持されます。実際のデータ エンコーディングは最適化されていませんでしたが、型ファミリを使用するとより適切な表示が可能になるのではないかと思います。

インターフェイス階層 (ウィジェットなど) のモデル化は、型クラスを使用して行うことができます。新しいインスタンスを追加できるため、具体的なウィジェットのセットが開かれています。可能なウィジェットの特定のリストが必要な場合は、GADT が簡潔なソリューションになる可能性があります。

サブクラスの特別な操作は、アップキャストとダウンキャストです。

これはウィジェットのコレクションを持つために最初に必要であり、通常の結果は存在型を使用することです。HList ライブラリのすべてのビットを読むと、他にも興味深い解決策があります。アップキャストは非常に簡単で、コンパイラはコンパイル時にすべてのキャストが有効であることを確認できます。ダウンキャストは本質的に動的であり、実行時の型情報のサポート (通常は Data.Typeable) が必要です。Typeable のようなものを考えると、ダウンキャストは単なる別の型クラスであり、失敗を示すために結果が Maybe でラップされます。

このほとんどに定型文が関連付けられていますが、QuasiQuoting と Templating を使用するとこれを減らすことができます。型推論は依然として大部分が機能します。

新しい Constraint の種類と型についてはまだ調べていませんが、アップキャストとダウンキャストに対する既存のソリューションを強化する可能性があります。

于 2012-08-17T11:24:39.123 に答える