46

そこで、モナド (Haskell で使用) に頭を悩ませ始めました。純粋な関数型言語で IO や状態を (理論的にも現実的にも) 処理できる他の方法に興味があります。たとえば、「効果型付け」を使った「mercury」という論理言語があります。haskell などのプログラムでは、エフェクトタイピングはどのように機能しますか? 他のシステムはどのように機能しますか?

4

6 に答える 6

79

ここにはいくつかの異なる質問が含まれています。

まず、IOStateは非常に異なるものです。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に、この型はただの自由なモナドです。そのインスタンスを簡単に書くことができます。)


では、モナドはどこに入るのでしょうか? MonadI/O も状態も必要ないことが判明したのにMonad、なぜそれが必要なのでしょうか? 答えは、そうではないということです。type class について魔法のようなものは何もありませんMonad

しかし、IOand State(およびリストと関数と Maybeパーサーと継続渡しスタイルと ...) を十分に長く使用すると、いくつかの点で非常によく似た動作をすることが最終的にわかります。リスト内のすべての文字列を出力する関数と、リスト内のすべてのステートフルな計算を実行して状態をスレッド化する関数を作成すると、それらは互いに非常によく似たものになります。

似たようなコードをたくさん書くのは好きではないので、それを抽象化する方法が必要です。Monad非常に異なるように見える多くの型を抽象化できるため、非常に優れた抽象化であることがわかりますが、それでも多くの有用な機能 ( のすべてを含むControl.Monad) を提供します。

と が与えられればbindIO :: IO a -> (a -> IO b) -> IO b、モナドについて考えなくても Haskell でreturnIO :: a -> IO aどんなプログラムでも書くことができます。しかし、 、 、、、などIOの多くの関数を複製することになるでしょう。Control.MonadmapMforeverwhen(>=>)

共通 API を実装するMonadことで、パーサーやリストとまったく同じコードを IO アクションで使用できるようになります。これが、クラスを使用する唯一の理由Monadです。異なるタイプ間の類似性を捉えるためです。

于 2012-11-24T04:44:06.733 に答える
21

もう 1 つの主要なアプローチは、Cleanのような一意性型付けです。手短に言えば、(実世界を含む) 状態へのハンドルは 1 回しか使用できず、変更可能な状態にアクセスする関数は新しいハンドルを返します。これは、最初の呼び出しの出力が 2 番目の呼び出しの入力であることを意味し、順次評価が強制されます。

効果型付けは Haskell のDisciple Compilerで使用されていますが、私の知る限りでは、たとえば GHC で有効にするにはかなりのコンパイラ作業が必要です。詳細については、私より詳しい方にお任せします。

于 2012-11-23T23:30:06.947 に答える
9

さて、まず状態とは何ですか?これは、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))
于 2012-11-24T04:58:27.407 に答える
8

A History of Haskell: Being Lazy With Class をご覧ください。モナドが発明される前に、Haskell で I/O を行うための 2 つの異なるアプローチ、継続とストリームについて説明しています。

于 2012-11-29T08:36:00.387 に答える
4

関数型リアクティブプログラミングと呼ばれるアプローチがあり、時間とともに変化する値やイベントストリームをファーストクラスの抽象化として表現します。私の頭に浮かぶ最近の例はElmです(Haskellで書かれていて、Haskellに似た構文を持っています)。

于 2012-12-17T10:54:11.703 に答える
1

私は興味があります - 純粋な関数型言語で I/O または状態を処理できる他の方法はありますか (理論的または現実的に)?

ここで既に述べたことに追加するだけです(注:これらのアプローチのいくつかにはそれがないように見えるため、いくつかの「即席の名前」があります)。

自由に利用できる説明または実装によるアプローチ:

その他のアプローチ - 参照のみ:

  • システムトークン:

    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 巻。シュプリンガー、ベルリン、ハイデルベルク。

于 2020-06-16T03:41:29.340 に答える