web-dev-qa-db-ja.com

なぜモナド?副作用をどのように解決しますか?

私はHaskellを学び、モナドを理解しようとしています。 2つの質問があります:

  1. 私の理解では、Monadは、MaybeListIOなどの「コンテナ」内のデータを操作する方法を宣言する別の型クラスです。これら3つのことを1つの概念で実装するのは賢明でクリーンなように見えますが、実際には、一連の関数、コンテナー、および副作用でクリーンなエラー処理を実行できるようにすることが重要です。これは正しい解釈ですか?

  2. 副作用の問題はどの程度正確に解決されますか?このコンテナの概念では、言語は基本的に、コンテナ内のすべてが非決定論的(I/Oなど)であると言います。リストとIOはどちらもコンテナーであるため、リスト内の値は私にはかなり決定論的であるように見えますが、リストはIOと同等に分類されます。では、決定論的とは何であり、副作用は何ですか?基本値が決定論的であるという考えに頭を悩ませることはできません。それをコンテナに貼り付けるまで(これは、同じ値とその隣にある他の値、たとえばNothing)とランダムにすることができます。

Haskellが入力と出力で状態変化をどのように回避するかを直感的に説明できますか?私はここで魔法を見ていません。

47
Oliver Zheng

重要なのは、一連の関数、コンテナー、および副作用でクリーンなエラー処理を実行できるようにすることです。これは正しい解釈ですか?

あんまり。副作用、エラー処理、非決定性など、モナドを説明しようとするときに人々が引用する多くの概念について言及しましたが、これらの概念のすべてがすべてのモナドに当てはまるという誤った感覚を持っているようです。しかし、あなたが言及した1つの概念があります:chaining

これには2つの異なるフレーバーがあるので、2つの異なる方法で説明します。1つは副作用なし、もう1つは副作用ありです。

副作用なし:

次の例を見てください。

_addM :: (Monad m, Num a) => m a -> m a -> m a
addM ma mb = do
    a <- ma
    b <- mb
    return (a + b)
_

この関数は、いくつかのモナドに包まれているというひねりを加えて、2つの数値を追加します。どのモナド?関係ありません!すべての場合において、その特別なdo構文は次のように非糖化します。

_addM ma mb =
    ma >>= \a ->
    mb >>= \b ->
    return (a + b)
_

...または、演算子の優先順位を明示した場合:

_ma >>= (\a -> mb >>= (\b -> return (a + b)))
_

これは、すべてが一緒に構成された小さな関数のチェーンであり、その動作は、各モナドに対して_>>=_とreturnがどのように定義されているかによって異なります。オブジェクト指向言語のポリモーフィズムに精通している場合、これは本質的に同じことです。複数の実装を持つ1つの共通インターフェースです。インターフェイスは計算ポリシーを表すので、平均的なOOPインターフェイスよりも少し気が遠くなります。 、動物か形か何か。

さて、addMが異なるモナド間でどのように動作するかの例をいくつか見てみましょう。 Identityモナドは、その定義が簡単なので、開始するのに適切な場所です。

_instance Monad Identity where
    return a = Identity a  -- create an Identity value
    (Identity a) >>= f = f a  -- apply f to a
_

それで、私たちが言うとき何が起こるか:

_addM (Identity 1) (Identity 2)
_

これを段階的に拡張します。

_(Identity 1) >>= (\a -> (Identity 2) >>= (\b -> return (a + b)))
(\a -> (Identity 2) >>= (\b -> return (a + b)) 1
(Identity 2) >>= (\b -> return (1 + b))
(\b -> return (1 + b)) 2
return (1 + 2)
Identity 3
_

すごい。さて、クリーンなエラー処理について言及したので、Maybeモナドを見てみましょう。その定義はIdentityより少しだけトリッキーです:

_instance Monad Maybe where
    return a = Just a  -- same as Identity monad!
    (Just a) >>= f = f a  -- same as Identity monad again!
    Nothing >>= _ = Nothing  -- the only real difference from Identity
_

したがって、addM (Just 1) (Just 2)と言うと、_Just 3_が得られると想像できます。しかし、にやにや笑いの場合は、代わりにaddM Nothing (Just 1)を展開しましょう。

_Nothing >>= (\a -> (Just 1) >>= (\b -> return (a + b)))
Nothing
_

またはその逆、addM (Just 1) Nothing

_(Just 1) >>= (\a -> Nothing >>= (\b -> return (a + b)))
(\a -> Nothing >>= (\b -> return (a + b)) 1
Nothing >>= (\b -> return (1 + b))
Nothing
_

そのため、Maybeモナドの_>>=_の定義は、失敗を説明するために調整されました。 _>>=_を使用して関数をMaybe値に適用すると、期待どおりの結果が得られます。

さて、あなたは非決定論について言及しました。はい、リストモナドはある意味で非決定論をモデル化するものと考えることができます...少し奇妙ですが、リストを代替の可能な値を表すものと考えてください:_[1, 2, 3]_はコレクションではなく、単一の非決定論です-1、2、または3のいずれかになり得る決定論的数。ばかげているように聞こえますが、リストに対して_>>=_がどのように定義されているかを考えると、ある程度意味があります。指定された関数をそれぞれの可能な値に適用します。したがって、_addM [1, 2] [3, 4]_は、実際には、これら2つの非決定論的値の可能なすべての合計を計算します:_[4, 5, 5, 6]_。

では、2番目の質問に答えましょう...

副作用:

次のように、addMモナドの2つの値にIOを適用するとします。

_addM (return 1 :: IO Int) (return 2 :: IO Int)
_

特別なことは何もありません。IOモナドで3つだけです。 addMは可変状態を読み書きしないので、面白くありません。 StateまたはSTモナドについても同じことが言えます。楽しくない。それでは、別の関数を使用しましょう。

_fireTheMissiles :: IO Int  -- returns the number of casualties
_

明らかに、ミサイルが発射されるたびに世界は異なります。明らかに。ここで、完全に無害で、副作用のない、ミサイル発射のないコードを書き込もうとしているとしましょう。おそらく、もう一度2つの数値を追加しようとしていますが、今回はモナドが飛び交っていません。

_add :: Num a => a -> a -> a
add a b = a + b
_

そして突然あなたの手が滑って、あなたは誤ってタイプミスしました:

_add a b = a + b + fireTheMissiles
_

正直な間違い、本当に。キーはとても接近していました。幸い、fireTheMissilesは単なるIntではなく_IO Int_型であったため、コンパイラーは災害を回避できます。

さて、完全に不自然な例ですが、要点は、IOSTとその仲間の場合、型システムは効果を特定のコンテキストに分離しておくということです。副作用を魔法のように排除するわけではなく、コードを参照透過性にするべきではありませんが、コンパイル時に、影響がどの範囲に制限されるかを明確にします。

では、元のポイントに戻ります。これは、関数の連鎖や構成と何の関係があるのでしょうか。さて、この場合、それは一連の効果を表現するための便利な方法です。

_fireTheMissilesTwice :: IO ()
fireTheMissilesTwice = do
    a <- fireTheMissiles
    print a
    b <- fireTheMissiles
    print b
_

概要:

モナドは、計算を連鎖させるためのポリシーを表します。 Identityのポリシーは純粋関数の合成、Maybeのポリシーは失敗の伝播を伴う関数の合成、IOのポリシーは不純関数の合成など。

34
mergeconflict

与えられたモナドmactionsのセット/ファミリー(またはレルム、ドメインなど)として見ることができます(Cステートメントを考えてください)。モナドmは、そのアクションが持つ可能性のある(副作用)効果の種類を定義します。

  • _[]_を使用すると、さまざまな「独立した並列ワールド」での実行をフォークできるアクションを定義できます。
  • _Either Foo_を使用すると、タイプFooのエラーで失敗する可能性のあるアクションを定義できます。
  • IOを使用すると、「外の世界」に副作用をもたらす可能性のあるアクションを定義できます(ファイルへのアクセス、ネットワーク、プロセスの起動、HTTP GETの実行...)。
  • 効果が「ランダム性」であるモナドを持つことができます(パッケージMonadRandomを参照)。
  • モナドを定義して、そのアクションがゲーム内で動き(チェス、囲碁など)を行い、対戦相手から動きを受け取ることができるが、ファイルシステムなどに書き込むことはできません。

概要

mがモナドの場合、_m a_はactionであり、タイプaの結果/出力を生成します。

_>>_および_>>=_演算子は、単純なアクションからより複雑なアクションを作成するために使用されます。

  • _a >> b_は、アクションaを実行してからアクションbを実行するマクロアクションです。
  • _a >> a_はアクションaを実行してから、アクションaを再度実行します。
  • _>>=_を使用すると、2番目のアクションは最初のアクションの出力に依存する可能性があります。

actionとは何か、アクションを実行してから別のアクションを実行することの正確な意味は、モナドによって異なります。各モナドは次のように定義します。いくつかの機能/効果を備えた命令型サブ言語。

単純なシーケンス(_>>_)

与えられたモナドMといくつかのアクションincrementCounterdecrementCounterreadCounterがあるとしましょう:

_instance M Monad where ...

-- Modify the counter and do not produce any result:
incrementCounter :: M ()
decrementCounter :: M ()

-- Get the current value of the counter
readCounter :: M Integer
_

今、私たちはそれらの行動で何か面白いことをしたいと思います。これらのアクションで最初に実行したいのは、それらをシーケンスすることです。 Cのように、次のことができるようにしたいと思います。

_// This is C:
counter++;
counter++;
_

「シーケンス演算子」_>>_を定義します。この演算子を使用して、次のように書くことができます。

_incrementCounter >> incrementCounter
_

「incrementCounter >> incrementalCounter」のタイプは何ですか?

  1. これは、Cのように、アトミックステートメントから構成ステートメントを記述できる2つの小さなアクションで構成されるアクションです。

    _// This is a macro statement made of several statements
    {
      counter++;
      counter++;
    }
    
    // and we can use it anywhere we may use a statement:
    if (condition) {
       counter++;
       counter++;     
    }
    _
  2. サブアクションと同じ種類の効果があります。

  3. 出力/結果は生成されません。

したがって、_incrementCounter >> incrementCounter_をタイプM ()にする必要があります。これは、同じ種類の可能な効果を持つが出力のない(マクロ)アクションです。

より一般的には、2つのアクションが与えられます。

_action1 :: M a
action2 :: M b
_

_a >> b_を、doing(アクションのドメインで意味するものは何でも)a、次にbによって取得され、出力としての結果を生成するマクロアクションとして定義します。 2番目のアクションの実行。 _>>_のタイプは次のとおりです。

_(>>) :: M a -> M b -> M b
_

またはより一般的に:

_(>>) :: (Monad m) => m a -> m b -> m b
_

単純なアクションから、より大きな一連のアクションを定義できます。

_ action1 >> action2 >> action3 >> action4
_

入出力(_>>=_)

一度に1つずつインクリメントできるようにしたいと思います。

_incrementBy 5
_

アクションにいくつかの入力を提供したいのですが、これを行うために、関数incrementByを定義してIntを取得し、アクションを生成します。

_incrementBy :: Int -> M ()
_

これで、次のように書くことができます。

_incrementCounter >> readCounter >> incrementBy 5
_

しかし、readCounterの出力をincrementByにフィードする方法はありません。これを行うには、シーケンス演算子のもう少し強力なバージョンが必要です。 _>>=_演算子は、特定のアクションの出力を次のアクションへの入力としてフィードできます。我々は書ける:

_readCounter >>= incrementBy
_

これは、readCounterアクションを実行し、その出力をincrementBy関数にフィードして、結果のアクションを実行するアクションです。

_>>=_のタイプは次のとおりです。

_(>>=) :: Monad m => m a -> (a -> m b) -> m b
_

(部分的な)例

ユーザーに情報(テキスト)を表示し、ユーザーに情報を尋ねることしかできないPromptモナドがあるとしましょう。

_-- We don't have access to the internal structure of the Prompt monad
module Prompt (Prompt(), echo, Prompt) where

-- Opaque
data Prompt a = ...
instance Monad Prompt where ...

-- Display a line to the CLI:
echo :: String -> Prompt ()

-- Ask a question to the user:
Prompt :: String -> Prompt String
_

質問をしてブール値を生成する_promptBoolean message_アクションを定義してみましょう。

プロンプト_(message ++ "[y/n]")_アクションを使用し、その出力を関数fにフィードします。

  • _f "y"_は、出力としてTrueを生成するだけのアクションである必要があります。

  • _f "n"_は、出力としてFalseを生成するだけのアクションである必要があります。

  • 他のものはアクションを再開する必要があります(アクションを再度実行します)。

promptBooleanは次のようになります。

_    -- Incomplete version, some bits are missing:
    promptBoolean :: String -> M Boolean
    promptBoolean message = Prompt (message ++ "[y/n]") >>= f
      where f result = if result == "y"
                       then ???? -- We need here an action which does nothing but produce `True` as output
                       else if result=="n"
                            then ???? -- We need here an action which does nothing but produce `False` as output
                            else echo "Input not recognised, try again." >> promptBoolean
_

効果なしで値を生成する(return

promptBoolean関数の欠落しているビットを埋めるには、副作用なしでダミーアクションを表す方法が必要ですが、これは指定された値のみを出力します。

_-- "return 5" is an action which does nothing but outputs 5
return :: (Monad m) => a -> m a
_

これで、promptBoolean関数を書き出すことができます。

_promptBoolean :: String -> Prompt Boolean
promptBoolean message :: Prompt (message ++ "[y/n]") >>= f
  where f result = if result=="y"
                   then return True
                     else if result=="n"
                     then return False
                     else echo "Input not recognised, try again." >> promptBoolean message
_

これらの2つの単純なアクション(promptBooleanecho)を構成することにより、ユーザーとプログラムの間のあらゆる種類の対話を定義できます(プログラムのアクションは、モナドに「ランダム性効果」がないため決定論的です)。

_promptInt :: String -> M Int
promptInt = ... -- similar

-- Classic "guess a number game/dialogue"
guess :: Int -> m()
guess n = promptInt "Guess:" m -> f
   where f m = if m == n
               then echo "Found"
               else (if m > n
                     then echo "Too big"
                     then echo "Too small") >> guess n       
_

モナドの操作

モナドは、returnおよび_>>=_演算子で構成できる一連のアクションです。

  • _>>=_アクション構成用。

  • returnは、(副作用)の影響なしに値を生成します。

これらの2つの演算子は、Monadを定義するために必要な最小限の演算子です。

Haskellでは、_>>_演算子も必要ですが、実際には_>>=_から導出できます。

_(>>): Monad m => m a -> m b -> m b
a >> b = a >>= f
 where f x = b
_

Haskellでは、追加のfail演算子も必要ですが、これは実際にはハックです(そして 将来的にはMonadから削除される可能性があります )。

これはMonadのHaskell定義です:

_class Monad m where     
  return :: m a     
  (>>=) :: m a -> (a -> m b) -> m b     
  (>>) :: m a -> m b -> m b  -- can be derived from (>>=)
  fail :: String -> m a      -- mostly a hack
_

アクションは一流です

モナドの素晴らしい点の1つは、アクションがファーストクラスであることです。それらを変数に取り、アクションを入力として受け取り、他のアクションを出力として生成する関数を定義できます。たとえば、while演算子を定義できます。

_-- while x y : does action y while action x output True
while :: (Monad m) => m Boolean -> m a -> m ()
while x y = x >>= f
  where f True = y >> while x y
        f False = return ()
_

概要

Monadは、あるドメインのアクションのセットです。モナド/ドメインは、可能な「効果」の種類を定義します。 _>>_および_>>=_演算子はアクションのシーケンスを表し、モナド式は、(機能的な)Haskellプログラムのあらゆる種類の「命令型(サブ)プログラム」を表すために使用できます。

素晴らしいことは次のとおりです。

  • 必要な機能と効果をサポートする独自のMonadを設計できます

    • 「ダイアログのみのサブプログラム」の例については、Promptを参照してください。

    • 「サンプリングのみのサブプログラム」の例については、Randを参照してください。

  • 独自の制御構造(whilethrowcatch、またはよりエキゾチックなもの)を、アクションを実行し、何らかの方法でそれらを構成して、より大きなマクロアクションを生成する関数として記述できます。

MonadRandom

モナドを理解する良い方法は、MonadRandomパッケージです。 Randモナドは、出力がランダムになる可能性のあるアクションで構成されます(効果はランダム性です)。このモナドのactionは、ある種の確率変数(より正確にはサンプリングプロセス)です。

_ -- Sample an Int from some distribution
 action :: Rand Int
_

Randを使用していくつかのサンプリング/ランダムアルゴリズムを実行することは、ファーストクラス値としてランダム変数があるため、非常に興味深いものです。

_-- Estimate mean by sampling nsamples times the random variable x
sampleMean :: Real a => Int -> m a -> m a
sampleMean n x = ...
_

この設定では、sequencePrelude関数は、

_ sequence :: Monad m => [m a] -> m [a]
_

になります

_ sequence :: [Rand a] -> Rand [a]
_

確率変数のリストから独立してサンプリングすることによって得られる確率変数を作成します。

11
ysdx

優れた " モナドを発明した可能性があります "の記事を指すことから始めましょう。これは、プログラムを作成しているときにモナド構造が自然に現れる方法を示しています。ただし、チュートリアルではIOについては触れられていないため、ここでアプローチを拡張する方法について説明します。

おそらくすでに見たことがあるもの、つまりコンテナモナドから始めましょう。私たちが持っているとしましょう:

f, g :: Int -> [Int]

これを見る1つの方法は、すべての可能な入力に対していくつかの可能な出力を提供することです。両方の関数のcompositionに対して可能なすべての出力が必要な場合はどうなりますか?関数を次々に適用することで得られる可能性をすべて与えますか?

さて、そのための関数があります:

fg x = concatMap g $ f x

これをもっと一般的にすると、

fg x     = f x >>= g
xs >>= f = concatMap f xs
return x = [x]

なぜこのようにラップしたいのですか?主に>>=returnを使用してプログラムを作成すると、いくつかの優れたプロパティが得られます。たとえば、ソリューションを「忘れる」のは比較的難しいと確信できます。別の関数skipを追加するなどして、明示的に再導入する必要があります。また、モナドができたので、モナドライブラリのすべてのコンビネータを使用できます。

それでは、あなたのトリッキーな例にジャンプしましょう。 2つの機能が「副作用」であるとしましょう。これは非決定論的ではありません。理論的には、全世界が入力(影響を与える可能性があるため)と出力(関数が影響を与える可能性があるため)の両方であることを意味します。したがって、次のようなものが得られます。

f, g :: Int -> RealWorld# -> (Int, RealWorld#)

fgが残した世界を取得したい場合は、次のように記述します。

fg x rw = let (y, rw')  = f x rw
              (r, rw'') = g y rw'
           in (r, rw'')

または一般化:

fg x     = f x >>= g
x >>= f  = \rw -> let (y, rw')  = x   rw
                      (r, rw'') = f y rw'
                   in (r, rw'')
return x = \rw -> (x, rw)

これで、ユーザーが>>=return、およびいくつかの事前定義されたIO値しか使用できない場合、Niceプロパティが再び取得されます。ユーザーが実際に使用することはありません参照RealWorld#が渡されます! getLineがデータを取得する場所の詳細にはあまり興味がないので、これは非常に良いことです。そして再びモナドライブラリからすべてのNice高レベル関数を取得します。

したがって、取り上げるべき重要なこと:

  1. モナドは、「コンテナAのすべての要素を常にコンテナBに渡す」、「この実世界のタグを渡す」など、コード内の一般的なパターンをキャプチャします。多くの場合、プログラムにモナドがあることに気付くと、複雑なものは単に適切なモナドコンビネータのアプリケーションになります。

  2. モナドを使用すると、完全に実装をユーザーから隠すことができます。これは、独自の内部状態であろうと、IOが比較的安全な方法で非純度を純粋なプログラムに圧縮する方法であろうと、優れたカプセル化メカニズムです。


付録

私が始めたときと同じように、誰かがまだRealWorld#に頭を悩ませている場合:明らかにもっと魔法が起こっていますafterすべてのモナド抽象化が削除されました。次に、コンパイラーは、「現実の世界」は1つしか存在できないという事実を利用します。それは良いニュースと悪いニュースです。

  1. したがって、コンパイラーはmust関数間の実行順序を保証します(これが私たちが求めていたものです!)

  2. しかし、それはまた、私たちが意味する可能性のあるものが1つしかないため、実際に現実の世界を通過する必要がないことも意味します。

要するに、実行順序が修正されると、RealWorld#は単純に最適化されます。したがって、IOモナドを使用するプログラムは、実際には実行時のオーバーヘッドがゼロです。また、RealWorld#の使用は明らかにoneIOを配置するための可能な方法のみですが、GHCが内部で使用する方法であることに注意してください。モナドの良いところは、繰り返しになりますが、ユーザーは実際に知る必要がないということです。

10
Peter Wortmann

IOモナドに関する3つの主な観察結果があります:

1)そこから値を取得することはできません。 Maybeのような他のタイプでは値を抽出できる場合がありますが、モナドクラスインターフェイス自体もIOデータ型でも抽出できません。

2)「内部」IOは、実際の値であるだけでなく、その「RealWorld」でもあります。このダミー値は、アクションの連鎖を強制するために使用されます型システムによる:2つの独立した計算がある場合、_>>=_を使用すると、2番目の計算が最初の計算に依存します。

3)random :: () -> Intのような非決定論的なものを想定します。これはHaskellでは許可されていません。署名をrandom :: Blubb -> (Blubb, Int)に変更すると、is許可されます。ただし、誰もBlubbを2回使用できないようにする必要があります。その場合、すべての入力は「違う」、出力も違うのも問題ありません。

これで、ファクト1)を使用できます。IOから何かを取得することはできないため、RealWordに隠されたIOダミーを使用してBlubb。アプリケーション全体でIOは1つだけ(mainから取得したもの)であり、2で見たように、適切なシーケンス処理を行います。問題が解決しました。

4
Landei

何かの性質を理解するのに役立つことが多いのは、可能な限り最も簡単な方法でそれを調べることです。そうすれば、私は潜在的に無関係な概念に気を取られることはありません。そのことを念頭に置いて、 Identity Monad の性質を理解することが役立つと思います。これは、可能な限り最も簡単なモナドの実装であるためです(私は思います)。

アイデンティティモナドの何が面白いのですか?他の式で定義された文脈で式を評価するという発想を表現できるのではないかと思います。そして私にとって、それは私が(これまでに)遭遇したすべてのモナドの本質です。

Haskellを学ぶ前に(私がしたように)すでに「主流」のプログラミング言語に多くの露出を持っていた場合、これはまったく面白くないようです。結局のところ、主流のプログラミング言語では、ステートメントは次々に順番に実行されます(もちろん、制御フロー構造を除く)。そして当然、すべてのステートメントは以前に実行されたすべてのステートメントのコンテキストで評価され、それらの以前に実行されたステートメントは環境と現在実行中のステートメントの動作を変更する可能性があると想定できます。

そのすべては、Haskellのような機能的で怠惰な言語ではほとんど外国の概念です。 Haskellで計算が評価される順序は明確に定義されていますが、予測が難しい場合や、制御がさらに難しい場合があります。そして、多くの種類の問題については、それは問題ありません。しかし、他の種類の問題(IOなど)は、プログラム内の計算間に暗黙の順序とコンテキストを確立するための便利な方法がないと解決が困難です。

副作用に関する限り、具体的には、多くの場合、(モナドを介して)単純な状態受け渡しに変換できます。これは、純粋な関数型言語では完全に合法です。ただし、一部のモナドはその性質のものではないようです。 IOモナドやSTモナドなどのモナドは文字通り副作用アクションを実行します。これについて考える方法はたくさんありますが、私が考える1つの方法は、計算が必要だからです。副作用のない世界に存在する場合、モナドは存在しない場合があります。そのため、モナドは、他の計算によって定義された副作用に基づいて、計算を実行するためのコンテキストを自由に確立できます。

最後に、私は間違いなくHaskellの専門家ではないことを否認しなければなりません。そのため、私が言ったことはすべて、このテーマに関する私自身の考えであり、後でモナドをより完全に理解したときに、それらを否定する可能性があることを理解してください。

4
Daniel Pratt

重要なのは、一連の関数、コンテナー、および副作用でクリーンなエラー処理を実行できるようにすることです。

多かれ少なかれ。

副作用の問題はどの程度正確に解決されますか?

I/Oモナドの値、つまりタイプIO aの値は、プログラムとして解釈する必要があります。 IO値のp >> qは、2つのプログラムを1つに結合し、最初にpを実行し、次にqを実行する演算子として解釈できます。他のモナド演算子も同様の解釈をしています。プログラムをmainという名前に割り当てることにより、出力オブジェクトコードによって実行する必要があるプログラムであることをコンパイラに宣言します。

リストモナドに関しては、非常に抽象的な数学的意味を除いて、I/Oモナドとは実際には関連していません。 IOモナドは、副作用を伴う決定論的計算を提供しますが、リストモナドは、Prologの手口にいくぶん似ている、非決定論的(ただしランダムではありません!)バックトラッキング検索を提供します。

2
Fred Foo

このコンテナの概念では、言語は基本的にコンテナ内のすべてが非決定論的であると言います

いいえ。Haskellは決定論的です。整数の加算2+ 2を要求すると、常に4が得られます。

「非決定論的」は単なる比喩であり、考え方です。すべてが内部で決定論的です。このコードをお持ちの場合:

do x <- [4,5]
   y <- [0,1]
   return (x+y)

Pythonコードとほぼ同等です

 l = []
 for x in [4,5]:
     for y in [0,1]:
         l.append(x+y)

ここに非決定性がありますか?いいえ、それはリストの決定論的な構築です。 2回実行すると、同じ番号が同じ順序で表示されます。

次のように説明できます。[4,5]から任意のxを選択します。 [0,1]から任意のyを選択します。 x + yを返します。考えられるすべての結果を収集します。

その方法は非決定性を伴うように見えますが、それはネストされたループ(リスト内包)にすぎません。ここには「本当の」非決定性はありません。すべての可能性をチェックすることによってシミュレートされます。非決定性は幻想です。コードは非決定的であるように見えるだけです。

Stateモナドを使用したこのコード:

do put 0
   x <- get
   put (x+2)
   y <- get
   return (y+3)

5を与え、状態変化を伴うようです。リストと同様に、それは幻想です。 (命令型言語のように)変化する「変数」はありません。ボンネットの下ではすべてが不変です。

コードは次のように記述できます。変数に0を設定します。変数の値をxに読み取ります。 (x + 2)を変数に入れます。変数をyに読み取り、y +3を返します。

その方法は状態を含むように見えますが、それは追加のパラメーターを渡す関数を構成するだけです。ここには「実際の」可変性はなく、構成によってシミュレートされます。可変性は幻想です。コードはそれを使用しているように見えます。

Haskellはそれをこのように行います:あなたは関数を持っています

   a -> s -> (b,s)

この関数は、状態の古い値を受け取り、新しい値を返します。可変性や変数変換は含まれません。それは数学的な意味での関数です。

たとえば、関数「put」は状態の新しい値を取り、現在の状態を無視して新しい状態を返します。

   put x _ = ((), x)

2つの通常の関数を作成できるように

  a -> b
  b -> c

  a -> c

(。)演算子を使用すると、「状態」トランスフォーマーを作成できます

  a -> s -> (b,s)
  b -> s -> (c,s)

単一の機能に

  a -> s -> (c,s)

自分で合成演算子を書いてみてください。これが実際に起こることであり、関数に引数を渡すだけの「副作用」はありません。

2
sdcvvc