免責事項: 以下の方法が良い方法であるとは約束できませんが、実際に作業するのは楽しそうです。スピンしてみましょう。
いくつかの必須のインポート
まず、いくつかのデータ型を放り出しましょう。実際にやり取りできる単純な「ファイル システム」を定義するために、いくつかの詳細を入力し、少し調整します。
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
ます。と を取り、Operation
とFilesystem
を返す必要があります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
の間にのみ存在するため、後から考えると、前もってそれらを結合し、構成された関数をインタープリターに渡すこともできます。inp
check
- を呼び出すときは
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
。