そこで、モナド (Haskell で使用) に頭を悩ませ始めました。純粋な関数型言語で IO や状態を (理論的にも現実的にも) 処理できる他の方法に興味があります。たとえば、「効果型付け」を使った「mercury」という論理言語があります。haskell などのプログラムでは、エフェクトタイピングはどのように機能しますか? 他のシステムはどのように機能しますか?
6 に答える
ここにはいくつかの異なる質問が含まれています。
まず、IO
とState
は非常に異なるものです。State
自分で簡単に行うことができます。すべての関数に追加の引数を渡し、追加の結果を返すだけで、「ステートフル関数」が得られます。たとえば、 に変わりa -> b
ます
a -> s -> (b,s)
。
ここには魔法はありません。Control.Monad.State
フォームの「状態アクション」をs -> (a,s)
便利に操作できるようにするラッパーと、一連のヘルパー関数を提供しますが、それだけです。
I/O は、その性質上、その実装にいくつかの魔法が必要です。しかし、「モナド」という言葉を使わずに Haskell で I/O を表現する方法はたくさんあります。そのままの Haskell の IO フリーのサブセットがあり、モナドについて何も知らずにゼロから IO を発明したい場合、私たちができることはたくさんあります。
たとえば、標準出力に出力するだけなら、次のように言えます。
type PrintOnlyIO = String
main :: PrintOnlyIO
main = "Hello world!"
次に、文字列を評価して出力するRTS(ランタイムシステム)を用意します。これにより、I/O が標準出力への出力のみで構成される任意の Haskell プログラムを作成できます。
ただし、インタラクティブ性が必要なため、これはあまり役に立ちません。そこで、それを可能にする新しいタイプの IO を発明しましょう。頭に浮かぶ最も単純なことは、
type InteractIO = String -> String
main :: InteractIO
main = map toUpper
この IO へのアプローチにより、stdin から読み取り、stdout に書き込む任意のコードを作成できます (interact :: InteractIO -> IO ()
ちなみに、Prelude にはこれを行う関数が付属しています)。
これは、インタラクティブなプログラムを作成できるため、はるかに優れています。しかし、実行したいすべての IO に比べてまだ非常に制限されており、エラーが発生しやすくなっています (誤って stdin を読み込もうとすると、ユーザーがさらに入力するまでプログラムはブロックされます)。
stdin の読み取りと stdout の書き込み以上のことができるようにしたいと考えています。Haskell の初期のバージョンで I/O がどのように行われていたかを以下に示します。
data Request = PutStrLn String | GetLine | Exit | ...
data Response = Success | Str String | ...
type DialogueIO = [Response] -> [Request]
main :: DialogueIO
main resps1 =
PutStrLn "what's your name?"
: GetLine
: case resps1 of
Success : Str name : resps2 ->
PutStrLn ("hi " ++ name ++ "!")
: Exit
と書くmain
と、遅延リスト引数を取得し、結果として遅延リストを返します。返される遅延リストには、 と のような値がPutStrLn s
ありGetLine
ます。(リクエスト) 値を生成した後、(レスポンス) リストの次の要素を調べることができ、RTS はそれがリクエストに対するレスポンスになるように手配します。
このメカニズムを使いやすくする方法はいくつかありますが、ご想像のとおり、このアプローチはすぐに厄介なものになります。また、前のものと同じようにエラーが発生しやすいです。
エラーが発生しにくく、Haskell IO の実際の動作に概念的に非常に近い別のアプローチを次に示します。
data ContIO = Exit | PutStrLn String ContIO | GetLine (String -> ContIO) | ...
main :: ContIO
main =
PutStrLn "what's your name?" $
GetLine $ \name ->
PutStrLn ("hi " ++ name ++ "!") $
Exit
重要なのは、メインの開始時に応答の「遅延リスト」を 1 つの大きな引数として受け取る代わりに、一度に 1 つの引数を受け入れる個別の要求を作成することです。
私たちのプログラムは今やただの通常のデータ型です -- リンクされたリストによく似ていますが、普通に走査することはできません: RTS が を解釈するとき、which hold a function; のmain
ような値に遭遇することがあります。GetLine
次に、RTS マジックを使用して stdin から文字列を取得し、その文字列を関数に渡してから続行する必要があります。演習: 書きinterpret :: ContIO -> IO ()
ます。
これらの実装には「ワールドパス」が含まれていないことに注意してください。Haskell での I/O の仕組みは、「ワールドパス」ではありません。GHCでの型の実際の実装にはIO
と呼ばれる内部型が含まれます
RealWorld
が、それは実装の詳細にすぎません。
実際の HaskellIO
は型パラメータを追加するので、任意の値を「生成」するアクションを記述できますdata IO a = Done a |
PutStr String (IO a) | GetLine (String -> IO a) | ...
。IO
これにより、任意の値を生成する「アクション」を作成できるため、柔軟性が高まります。
(Russell O'Connorが指摘しているようMonad
に、この型はただの自由なモナドです。そのインスタンスを簡単に書くことができます。)
では、モナドはどこに入るのでしょうか? Monad
I/O も状態も必要ないことが判明したのにMonad
、なぜそれが必要なのでしょうか? 答えは、そうではないということです。type class について魔法のようなものは何もありませんMonad
。
しかし、IO
and State
(およびリストと関数と
Maybe
パーサーと継続渡しスタイルと ...) を十分に長く使用すると、いくつかの点で非常によく似た動作をすることが最終的にわかります。リスト内のすべての文字列を出力する関数と、リスト内のすべてのステートフルな計算を実行して状態をスレッド化する関数を作成すると、それらは互いに非常によく似たものになります。
似たようなコードをたくさん書くのは好きではないので、それを抽象化する方法が必要です。Monad
非常に異なるように見える多くの型を抽象化できるため、非常に優れた抽象化であることがわかりますが、それでも多くの有用な機能 ( のすべてを含むControl.Monad
) を提供します。
と が与えられればbindIO :: IO a -> (a -> IO b) -> IO b
、モナドについて考えなくても Haskell でreturnIO :: a -> IO a
どんなプログラムでも書くことができます。しかし、 、 、、、などIO
の多くの関数を複製することになるでしょう。Control.Monad
mapM
forever
when
(>=>)
共通 API を実装するMonad
ことで、パーサーやリストとまったく同じコードを IO アクションで使用できるようになります。これが、クラスを使用する唯一の理由Monad
です。異なるタイプ間の類似性を捉えるためです。
もう 1 つの主要なアプローチは、Cleanのような一意性型付けです。手短に言えば、(実世界を含む) 状態へのハンドルは 1 回しか使用できず、変更可能な状態にアクセスする関数は新しいハンドルを返します。これは、最初の呼び出しの出力が 2 番目の呼び出しの入力であることを意味し、順次評価が強制されます。
効果型付けは Haskell のDisciple Compilerで使用されていますが、私の知る限りでは、たとえば GHC で有効にするにはかなりのコンパイラ作業が必要です。詳細については、私より詳しい方にお任せします。
さて、まず状態とは何ですか?これは、Haskellにはない可変変数として現れる可能性があります。メモリ参照(IORef、MVar、Ptrなど)とそれらに作用するIO/STアクションのみがあります。
ただし、状態自体も純粋にすることができます。'Stream'タイプを確認するには、次のようにします。
data Stream a = Stream a (Stream a)
これは価値観の流れです。ただし、このタイプを解釈する別の方法は、値を変更することです。
stepStream :: Stream a -> (a, Stream a)
stepStream (Stream x xs) = (x, xs)
これは、2つのストリームが通信できるようにすると興味深いものになります。次に、オートマトンカテゴリAutoを取得します。
newtype Auto a b = Auto (a -> (b, Auto a b))
これは実際にはと似Stream
ていますが、ストリームが常にタイプaの入力値を取得する点が異なります。これはカテゴリを形成するため、ストリームのある瞬間が別のストリームの同じ瞬間からその値を取得できます。
これもまた別の解釈です。時間の経過とともに変化する2つの計算があり、それらが通信できるようにします。したがって、すべての計算にはローカル状態があります。これは、次のように同型であるタイプですAuto
。
data LS a b =
forall s.
LS s ((a, s) -> (b, s))
A History of Haskell: Being Lazy With Class をご覧ください。モナドが発明される前に、Haskell で I/O を行うための 2 つの異なるアプローチ、継続とストリームについて説明しています。
関数型リアクティブプログラミングと呼ばれるアプローチがあり、時間とともに変化する値やイベントストリームをファーストクラスの抽象化として表現します。私の頭に浮かぶ最近の例はElmです(Haskellで書かれていて、Haskellに似た構文を持っています)。
私は興味があります - 純粋な関数型言語で I/O または状態を処理できる他の方法はありますか (理論的または現実的に)?
ここで既に述べたことに追加するだけです(注:これらのアプローチのいくつかにはそれがないように見えるため、いくつかの「即席の名前」があります)。
自由に利用できる説明または実装によるアプローチ:
「直交ディレクティブ」 - Maarten Fokkinga と Jan Kuper によるI/O への代替アプローチを参照してください。
疑似データ- F. Warren Burton による関数型プログラミング言語における参照透過性による非決定性を参照してください。このアプローチは、Dave Harrison が彼の論文 Functional Real-Time Programming: The Language Ruth And Its Semanticsでクロックを実装するために使用されており、Lennart Augustsson、Mikael Rittri、Dan Synek による一意の名前の生成についての機能的なパールの名前の供給。Hackageにはいくつかのライブラリ実装もあります。
Witnesses - Tachio Terauchiと Alex Aiken によるWitnessing Side Effectsを参照してください。
オブザーバー- Vipin Swarup、Uday S. Reddy、および Evan Ireland によるApplicative Languages の割り当てを参照してください。
その他のアプローチ - 参照のみ:
システムトークン:
L.アウグストソン。システム トークンを使用した機能 I/O。PMG メモ 72、Dept Computer Science、Chalmers University of Technology、S-412 96 Göteborg、1989 年。
「エフェクトツリー」:
Rebelsky SA (1992) I/O ツリーとインタラクティブな遅延関数型プログラミング。: Bruynooghe M.、Wirsing M. (eds) プログラミング言語の実装とロジック プログラミング。PLILP 1992. コンピュータ サイエンスの講義ノート、第 631 巻。シュプリンガー、ベルリン、ハイデルベルク。