4

これに対する全体的な答えが私を関数型リアクティブプログラミングに押しやるだろうと想像しますが、...ちょっと我慢してください。

この質問のサンプルコードもありません。私は自分のコードのいくつかでトピックの近くをさまよいましたが、それで IO モナドにしっかりととどまりました。

やや複雑な状態をモデル化し、それを全体的な Application State モナドに入れるアプリケーションがあると想像してください。コア アプリケーションと特定のユーザー インターフェイスとの間に一定レベルの分離が必要なため、このようにしています。

data S = S DataStore EventStream Sockets
type AppState m = StateT S m

(DataStore、EventStream、および Sockets はすべて、基本的にそのように聞こえるデータ型であると仮定します:))

ここで、EventStream のみを表示する GTK (TreeView、子ノードなし) でテーブルを作成したいとします。私はすでにそれを行うことを学びましたlistStoreNew event_stream >>= treeViewNewWithModel( http://markus.alyra.org/?p=1023を参照してください。ここでは、これを設定する仕組みについてかなり詳しく説明しています)。

しかし、今では AppState モナドにあるデータの変更可能なコピーがあります。アプリケーションがオフになり、EventStream に新しいデータを追加する何かを実行すると、それはビューに表示されません。私が考えることができる唯一の方法はlistStoreInsert my_new_event、モナドに加えられた変更に加えて、のようなメッセージでそれを送信することです。それは可能ですが、不器用に感じ始めています。

さらに悪いことに、この神話上のツリー ビューは管理ビューです。編集可能です!管理者は、「ああ、そのイベントには無効なデータが含まれているので、変更したい!」と言います。これで、上で作成した ListStore 内のデータを問題なく変更できます。問題なく更新を行うコールバックを作成できます。しかし、アップデートを Global AppState Monad に入れる方法はまったく考えられません。

そして、これらの最後のいくつかの言葉は、問題の核心を示しています。グローバルな AppState モナドがある場合、そのモナドを更新するものはすべて、モナドを表示したいすべてのものと一緒に 1 行で実行する必要があります。TreeView はそれを破ります。TreeView モナドでセルが編集されると、編集ハンドラーは完全に IO モナドで実行され、何も返さないことが期待されます。終了データ型はIO (). AppState からデータをアンラップし、編集ハンドラーを実行して、AppState でデータを再ラップする気の利いた方法があったとしても、アプリケーションの他のブランチはそれを見ることができませんでした。

AppState への読み取り専用ビューを提供する独自の完全にカスタムな ModelView インスタンスを作成する方法を理解できたとしても、アプリケーションの残りの部分で状態の更新を利用できるようにする方法が思い浮かびません。

そう...

この方法で GTK/Haskell アプリケーションをモデル化することさえ可能ですか? それとも、私は狂気への道を進んでしまったのでしょうか?

4

1 に答える 1

1

通常の状態モナドを使用して状態を確実に共有する方法はありません。(不自然な例) ユーザーが GUI を介してモデルを編集し、同時に別の場所から新しいエントリを取得した場合はどうなるでしょうか? そのような状況では、純粋なモナドスタックを使用して状態モナドへの変更をシリアル化することはできません。

あなたができることは、可変参照を使用してある種の同期システムを使用することです(MVarたとえば、 s を使用)。実際のアプリケーションの状態を に保存し、状態をMVar読み取ったり変更したりする可能性のある何かが発生するたびに、その にアクセスしMVarます。私が何を意味するかを示す擬似コードを次に示します。

-- This is the MVar that stores your application state
appStateMVar :: MVar S
appStateMVar = unsafePerformIO $ newMVar initialAppState
{-# NOINLINE appStateMVar #-}
-- It could also be passed as a parameter to the functions below, so that when
-- you define the callbacks, you create a closure over the MVar that you use.
-- (i.e.:
-- > appStateMVar <- newMVar initialAppState
-- > createListViewWithCallback $ whenUserAddedSomethingViaTheGUI appStateMVar
-- )
-- That way, you don't have to have the MVar in global scope and can avoid the
-- use of `unsafePerformIO` to initialize it, etc.

main :: IO ()
main = do
  createListViewWithCallback whenUserAddedSomethingViaTheGUI
  createSocketsAndListenUsingCallback whenChangesArriveOverTheNetwork
  runSomeKindOfMainLoop

-- This would be called on any thread by the GUI when the user added something in
-- the view (For example)
whenUserAddedSomethingViaTheGUI :: AddedThing -> IO ()
whenUserAddedSomethingViaTheGUI theThingThatWasAdded =
  takeMVar appStateMVar >>=
  execStateT (addToTheState theThingThatWasAdded) >>=
  putMVar appStateMVar

-- This would be called by the network when something changed there
whenChangesArriveOverTheNetwork :: ArrivedChanges -> IO ()
whenChangesArriveOverTheNetwork theChangesThatArrived =
  takeMVar appStateMVar >>=
  execStateT (handleChanges theChangesThatArrived) >>=
  putMVar appStateMVar

次に、以前と同じように、純粋なモナドを記述addToTheStateして使用できます。handleChangesAppState

もちろん、FRP を使用する場合は、アプリケーションの状態を時間の経過とともに変化する純粋な信号にすることで、この非常に命令的なスタイルの状態配線を回避できます。reactive-banana双方向の GUI エディター/ビューを FRP イベント ネットワークと統合できるようにするいくつかの作業が行われたことを理解しています。

于 2012-07-11T14:57:19.033 に答える