17

内部で状態モナドを使用するモデル ファイルシステムに取り組んでいます。次のような操作を持つ型クラスがあります。

class Monad m => FS m where
  isDirectory  :: Path -> m Bool
  children     :: Path -> m [Path]
  ...

cdls、などのコマンドを提供する小さな対話型インタープリターに取り組んでいますcat。インタプリタでの操作は、次のように記述できます。

fsop :: FS m => Operation -> m Response

との定義は重要ではOperationありResponseません。必要に応じて、それらを文字列にしてください。

私が解決しようとしている問題Operationは、ファイルシステムを解釈して応答を出力する I/O モナドにトップレベルのループを書くことです。IO が FS のインスタンスである場合 (つまり、IO モナドを直接操作している場合)、人生は単純です:

loop :: Path -> IO ()
loop currentDir = do
        op <- getLine
        case read op of
          ChangeDir d -> loop d -- should test 'isDirectory d', but let's not
          Ls -> do { files <- children currentDir
                   ; mapM_ putStrLn files
                   ; loop currentDir }
          Exit -> return ()

しかし、それは私が望むものではありません。使いたいControl.Monad.State

newtype Filesystem a = Filesystem (State (Data.Map.Map Path Contents) a)

そして宣言する

instance Monad Filesystem ...
instance FS Filesystem ...

抽象化を使用して、FS任意のインスタンスで動作するシングルステップ関数を作成できます。実際、次のコードがコンパイルされます。

step :: FS fs => Path -> Operation -> fs (Path, Response)
step currentDir op = 
        case op of
          ChangeDir d -> return (d, "")
          Ls -> do { files <- children currentDir
                   ; return (currentDir, unlines files) }

この時点で、私は完全に立ち往生しています。 私がやりたいことは、 IO モナドにインタラクティブなループを書くことです。これは s を読み込んでOperationsを出力できますが、必ずしも IOではないResponse状態モナドで動作します。(IO にないモデルを持つ理由の 1 つは、QuickCheck プロパティをテストできるようにするためです。)

これは標準的な問題である必要があるように感じます — そうでない ステートフルな抽象化の上にあるインタラクティブな read-eval-print ループ —IOしかし、私はそれを理解できないように見えるので、息を呑むほど明白な何かを見落としているに違いありません. 私はオンラインで見ましたが、啓発されていません。

呼び出すstepことができる対話型の IO 実行計算を作成するための助けをいただければ幸いです。

4

4 に答える 4

8

モナド変換子の使用についてはどうですか? それらは、モナドを結合する多かれ少なかれ標準的な方法です。簡単な例を次に示します。

type Foo a = StateT String IO a

replT :: Foo ()
replT = do
  str   <- liftIO getLine
  state <- get
  liftIO $ putStrLn ("current state: " ++ state)
  liftIO $ putStrLn ("setting state: " ++ str)
  put str
  replT

以下は、ghci 内から replT を実行した結果です。

*Main> runStateT replT "Initial state"
asd
current state: Initial state
setting state: asd
zxc
current state: asd
setting state: zxc
asdasd

3 つのモナド変換ライブラリがあります。mtl、トランスフォーマー、monadLib。あまり使わないのでおすすめはできません。

于 2010-07-08T20:37:20.217 に答える
6

免責事項: 以下の方法が良い方法であるとは約束できませんが、実際に作業するのは楽しそうです。スピンしてみましょう。


いくつかの必須のインポート

まず、いくつかのデータ型を放り出しましょう。実際にやり取りできる単純な「ファイル システム」を定義するために、いくつかの詳細を入力し、少し調整します。

type Path = String
type Response = Maybe String
type Contents = [String]

data Operation = Cd Path 
               | Ls 
               | MkDir Path
               | Quit
    deriving (Read, Show)

次に、少しエッジの効いたことを行います...すべてのモナドを取り除きます。何?これは狂気です!おそらく、しかし時には、>>=提供するすべての隠された配管が物事を隠しすぎていることがあります.

ファイル システム自体については、現在の作業ディレクトリと、パスからその子へのマップを格納するだけです。また、それとやり取りするためのいくつかの関数も必要です。

data Filesystem = Filesystem { wd :: Path, files :: M.Map Path Contents }
    deriving Show

newFS = Filesystem "/" (M.singleton "/" [])

isDirectory p fs = M.member p $ files fs
children p fs = fromMaybe [] . M.lookup p $ files fs
cd p fs = fs { wd = p }
create p fs = let newPath = wd fs ++ p ++ "/"
                  addPath = M.insert newPath [] . M.adjust (p:) (wd fs)
              in (newPath, fs { files = addPath $ files fs })

次に、関数のモナドのないバージョンについて説明しstepます。と を取り、OperationFilesystemを返す必要がありますResponse(変更されている可能性があります) Filesystem:

step :: Operation -> Filesystem -> (Response, Filesystem)
step (Cd d) fs = (Just "Ok\n", cd d fs)
step (MkDir d) fs = first (\d -> Just $ "Created " ++ d ++ "\n") $ create d fs
step Ls fs = let files = children (wd fs) fs
             in (Just $ unlines files, fs)
step Quit fs = (Nothing, fs)

State...うーん、その型シグネチャはすでにモナドの内臓によく似ています。まぁまぁ、とりあえず無視して突進しましょう。

ここで必要なのは、インタプリタへの汎用インターフェイスを提供する関数です。Filesystem特に、インターフェースを少なくともある程度自己完結型にして、インターフェースを使用するものはすべて手動でステップスルーする必要がないようにする必要がありますが、インターフェースを使用するコードに十分に気付かないようにして、接続できるようにする必要があります。モナド、IOその他のMonad、またはモナドがまったくないことさえあります。

これは主に、どちらかの部分を制御するのではなく、何らかの方法で外部コードをインタプリタとインターリーブする必要があることを示しています。さて、Haskellは関数型言語なので、高階関数をたくさん使った方がいいということですよね?私にはもっともらしいと思われるので、使用する戦略は次のとおりです。関数が次に何をすべきかわからない場合は、それを想定している別の関数に渡します。全員が何が起こっているかを理解するまで繰り返します。完璧な計画ですね。

すべての核心はstep関数なので、それを呼び出すことから始めます。

interp1 :: Operation -> Filesystem -> (Response, Filesystem)
interp1 op fs = step op fs

……さて、スタートです。私は推測する。でも待って、どこOperationから来ているの?それを提供するには外部コードが必要ですが、IO. そこで、汚い仕事をする別の関数を取得します。

interp2 :: ((Operation -> (Response, Filesystem)) -> t) -> Filesystem -> t
interp2 inp fs = inp (\op -> step op fs)

もちろん、今私たちが持っているのは、tそれが何であるかさえ知らない愚か者だけです。Responseどこかに aと aが含まれている必要があることはわかっていますがFilesystem、それを処理することはできませ。そのため、続行方法に関するいくつかの指示とともに、別の関数に渡します...もちろん、さらに多くの関数を渡します。それは機能です。

interp3 :: ((Operation -> (Response, Filesystem)) -> a)
           -> (a -> ((Response, Filesystem) -> b) -> c)
           -> (Filesystem -> b)
           -> (String -> Filesystem -> b) 
           -> Filesystem 
           -> c
interp3 inp check done out fs = check (inp (\op -> step op fs)) test
    where test (Nothing, fs) = done fs
          test (Just s, fs)  = out s fs

…いや、それはかなり醜いです。しかし心配はいりません。すべては計画通りに進んでいます。次に、いくつかの観察を行うことができます。

  • 型はとaの間にのみ存在するため、後から考えると、前もってそれらを結合し、構成された関数をインタープリターに渡すこともできます。inpcheck
  • を呼び出すときはdone、缶に書かれていることを正確に意味する必要があります。したがって、の戻り値の型はdone、インタープリター全体と同じである必要があります。つまりb、同じ型である必要がありcます。

さて、これでdoneすべてが終わるとしたら、何outですか?その名前がほのめかしているように、外部コードに出力を提供していますが、その後はどこに行くのでしょうか? 何らかの方法でインタープリターにループバックする必要があり、インタープリターがまだ再帰的ではないことに気付くかもしれません。進むべき道は明らかです。通訳は、ヨルムンガンドのように、自分のしっぽをつかみます。解釈が終了するまで(またはラグナロクまで、どちらか早い方まで)無期限にループバックします。

interp4 :: ((Operation -> (Response, Filesystem)) 
               -> ((Response, Filesystem) -> r) -> r)
           -> (Filesystem -> r)
           -> (String -> Filesystem -> (Filesystem -> r) -> r)
           -> Filesystem
           -> r
interp4 checkInp done out fs = checkInp (\op -> step op fs) test
    where loop = interp4 checkInp done out
          test (Nothing, fs) = done fs
          test (Just s, fs)  = out s fs loop

...ああ、今は動くって言いましたか?いいえ、真剣に!

インターフェイスを使用するIOコードを次に示します。

ioIn f k = putStr "> " >> (k . f =<< readLn)
ioDone fs = putStrLn "Done" >> return fs 
ioOut x fs k = putStr x >> k fs

ioInterp :: IO Filesystem
ioInterp = interp4 ioIn ioDone ioOut newFS

コマンドのリストを実行し、出力文字列のリストを生成するコードは次のとおりです。

scriptIn f k (x:xs) = k (f x) xs
scriptDone fs xs = ["Done\n"]
scriptOut r fs k xs = r : k fs xs

scriptInterp :: [Operation] -> [String]
scriptInterp = interp4 scriptIn scriptDone scriptOut newFS

コードだけでは想像力を十分に刺激しない場合は、両方を GHCiで実行する例をここに示します。


まあ、それはそれです。またはそれは?率直に言って、そのインタープリターは母親だけが愛することができるコードです。すべてをエレガントに結び付けるものはありますか?コードの根底にある構造を明らかにする何か?

...わかりました。これがどこにつながるかは明らかです。円を描いて相互に末尾呼び出しを行う関数の全体的な設計は、継続渡しスタイルに非常によく似ており、インタプリタの型シグネチャに 1(foo -> r) -> rではなく 2 回、継続モナドとしてよく知られている特徴的な pattern が見られます。

残念ながら、それでもなお、継続は頭を痛め、インタープリターの非常にアドホックな構造を で実行される計算にどのように解きほぐすのが最善なのかわかりませんMonadCont

于 2010-07-09T03:14:37.503 に答える
2

ここで 2 つの解決策を考えることができます。

1) モナド変換ライブラリを使用します。ライブラリに関するいくつかの詳細を除いて、Shimuuarの返信を改善することはできません。Transformers 自体は、必要なインスタンスを提供しません。トランスフォーマーと monads-tf または monads-fd のいずれかを使用する必要があります。これらは、それぞれ型ファミリーと Fundeps に基づく実装を提供します。あなたがこの道を行くなら、私はmonads-tfを好みます。APIはmtlのものとほぼ同じです。私は MonadLib の経験はありませんが、これもかなり良さそうです。

2) メイン ループを IO に記述し、ループの反復ごとに runState を呼び出して状態モナドを評価します。次のようなもの:

loop path state = do
  op <- readOp
  let ((newpath, resp), newstate) = runState (step path op) state
  print resp
  loop newpath newstate

これは機能するはずですが、モナド変換子を使用するよりもはるかに慣用的ではありません。

于 2010-07-08T23:43:20.923 に答える
0

のインスタンスが、だけでなく、のインスタンスであるFSことを要求します。MonadIOMonad

class MonadIO m => FS m where ...

次に、次の場所に移動するliftIO方法を利用FSできるようになりIOます。

liftIO :: MonadIO m => m a -> IO a

IOしたがって、モナドに書き込むことができます。

files <- liftIO $ children currentDir

もちろん、 FSインスタンスを作成する前にliftIO 、それぞれに実装する必要があることを意味しますFSが、このアプリケーションの場合(実際の詳細は確認していませんが)、単純なように思えます。

于 2010-07-08T20:31:21.503 に答える