19

デーモンとして動作するプログラムを書いています。デーモンを作成するために、ユーザーは必要なクラスごとに一連の実装を提供します (そのうちの 1 つはデータベースです)。これらのクラスにはすべて関数があり、フォームの型シグネチャがありますが、StateT s IO aクラスsごとに異なります。

各クラスが次のパターンに従うとします。

import Control.Monad (liftM)
import Control.Monad.State (StateT(..), get)

class Hammer h where
  driveNail :: StateT h IO ()

data ClawHammer = MkClawHammer Int -- the real implementation is more complex

instance Hammer ClawHammer where
  driveNail = return () -- the real implementation is more complex

-- Plus additional classes for wrenches, screwdrivers, etc.

これで、各「スロット」に対してユーザーが選択した実装を表すレコードを定義できます。

data MultiTool h = MultiTool {
    hammer :: h
    -- Plus additional fields for wrenches, screwdrivers, etc.
  }

そして、デーモンはほとんどの作業をStateT (MultiTool h ...) IO () モナドで行います。

マルチツールにはハンマーが入っているので、ハンマーが必要なあらゆる場面で使用できます。言い換えれば、次のMultiToolようなコードを記述した場合、型はそれに含まれる任意のクラスを実装できます。

stateMap :: Monad m => (s -> t) -> (t -> s) -> StateT s m a -> StateT t m a
stateMap f g (StateT h) = StateT $ liftM (fmap f) . h . g

withHammer :: StateT h IO () -> StateT (MultiTool h) IO ()
withHammer runProgram = do
  t <- get
  stateMap (\h -> t {hammer=h}) hammer runProgram

instance Hammer h => Hammer (MultiTool h) where
  driveNail = withHammer driveNail

しかし、withHammerwithWrenchwithScrewdriverなどの実装は基本的に同じです。こんな感じで書けると良いのですが…

--withMember accessor runProgram = do
--  u <- get
--  stateMap (\h -> u {accessor=h}) accessor runProgram

-- instance Hammer h => Hammer (MultiTool h) where
--   driveNail = withMember hammer driveNail

しかし、もちろんそれはコンパイルされません。

私のソリューションはオブジェクト指向すぎると思います。より良い方法はありますか?モナドトランスフォーマーかな?ご提案いただきありがとうございます。

4

4 に答える 4

30

あなたの場合のように大規模なグローバル状態に行きたい場合は、ベンが提案したように、使用したいのはレンズです。私も Edward Kmett のレンズライブラリをお勧めします。ただし、別の、おそらくより良い方法があります。

サーバーには、プログラムが継続的に実行され、状態空間に対して同じ操作を実行するというプロパティがあります。問題は、サーバーをモジュール化するときに始まります。この場合、グローバルな状態以上のものが必要になります。モジュールに独自の状態を持たせたい。

モジュールをRequestResponseに変換するものと考えてみましょう:

Module :: (Request -> m Response) -> Module m

何らかの状態がある場合、この状態は、モジュールが次回別の応答を返す可能性があるという点で注目に値します。これを行うにはいくつかの方法があります。たとえば、次のようになります。

Module :: s -> ((Request, s) -> m (Response s)) -> Module m

しかし、これを表現するためのはるかに優れた同等の方法は、次のコンストラクターです (すぐにその周りに型を構築します)。

Module :: (Request -> m (Response, Module m)) -> Module m

このモジュールはリクエストをレスポンスにマッピングしますが、途中でそれ自体の新しいバージョンも返します。さらに進んで、リクエストとレスポンスをポリモーフィックにしましょう。

Module :: (a -> m (b, Module m a b)) -> Module m a b

モジュールの出力タイプが別のモジュールの入力タイプと一致する場合、通常の関数のようにそれらを構成できます。この構成は連想的であり、ポリモーフィックなアイデンティティを持っています。これはカテゴリのように聞こえますが、実際はそうです! それは圏であり、アプリカティブ・ファンクターであり、矢です。

newtype Module m a b =
    Module (a -> m (b, Module m a b))

instance (Monad m) => Applicative (Module m a)
instance (Monad m) => Arrow (Module m)
instance (Monad m) => Category (Module m)
instance (Monad m) => Functor (Module m a)

これで、それを知らなくても、独自のローカル状態を持つ 2 つのモジュールを構成できるようになりました。しかし、それだけでは十分ではありません。もっと欲しい。切り替え可能なモジュールはどうですか?モジュールが実際に答えを出さないことを選択できるように、小さなモジュール システムを拡張しましょう。

newtype Module m a b =
    Module (a -> m (Maybe b, Module m a b))

これにより、 と直交する別の形式の合成が可能になります: これで、型はファンクター(.)のファミリーでもあります:Alternative

instance (Monad m) => Alternative (Module m a)

モジュールはリクエストに応答するかどうかを選択できるようになりました。応答しない場合は、次のモジュールが試行されます。単純。ワイヤ カテゴリを再発明したところです。=)

もちろん、これを再発明する必要はありません。Netwireライブラリは、この設計パターンを実装し、定義済みの「モジュール」(ワイヤと呼ばれる) の大規模なライブラリを備えています。チュートリアルについては、 Control.Wireモジュールを参照してください。

于 2012-12-17T18:12:48.900 に答える
17

lensこれは、他の人が話しているように使用する方法の具体的な例です。次のコード例でType1は、 はローカル状態 (つまりハンマー) で、Type2はグローバル状態 (つまりマルチツール) です。 レンズによって定義された任意のフィールドにズームインするローカライズされた状態計算を実行できる関数をlens提供します。zoom

import Control.Lens
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State

data Type1 = Type1 {
    _field1 :: Int   ,
    _field2 :: Double}

field1 :: SimpleLens Type1 Int
field1 = lens _field1 (\x a -> x { _field1 = a})

field2 :: SimpleLens Type1 Double
field2 = lens _field2 (\x a -> x { _field2 = a})

data Type2 = Type2 {
    _type1  :: Type1 ,
    _field3 :: String}

type1 :: SimpleLens Type2 Type1
type1 = lens _type1 (\x a -> x { _type1 = a})

field3 :: SimpleLens Type2 String
field3 = lens _field3 (\x a -> x { _field3 = a})

localCode :: StateT Type1 IO ()
localCode = do
    field1 += 3
    field2 .= 5.0
    lift $ putStrLn "Done!"

globalCode :: StateT Type2 IO ()
globalCode = do
    f1 <- zoom type1 $ do
        localCode
        use field1
    field3 %= (++ show f1)
    f3 <- use field3
    lift $ putStrLn f3

main = runStateT globalCode (Type2 (Type1 9 4.0) "Hello: ")

zoomタイプの直接のサブフィールドに限定されません。レンズは構成可能であるため、次のような操作を行うだけで、1 回の操作で必要なだけズームできます。

zoom (field1a . field2c . field3b . field4j) $ do ...
于 2012-12-17T22:38:03.117 に答える
6

これは、レンズのアプリケーションのように聞こえます。

レンズは、一部のデータのサブフィールドの仕様です。アイデアは、いくつかの値とtoolLens機能があるため、ツールをフェッチして新しい値に置き換えるというものです。次に、レンズを受け入れるだけの関数としてyour を簡単に定義できます。viewsetview toolLens :: MultiTool h -> hset toolLens :: MultiTool h -> h -> MultiTool hwithMember

レンズ技術は最近大きく進歩し、今では信じられないほどの能力を備えています。これを書いている時点で最も強力なライブラリは、Edward Kmett のlensライブラリです。これは少し飲み込みにくいですが、必要な機能が見つかれば非常にシンプルです。SO でレンズに関するその他の質問を検索することもできます。たとえば、レンズにリンクする機能的なレンズ、fclabels、data-accessor - which library for structure access and Mutation is better、またはlensタグ。

于 2012-12-17T14:50:30.103 に答える