38

関数型プログラミングに関してモナドに触れた後、この機能は実際に言語を純粋にしますか、それとも黒板の数学以外の現実世界のコンピューターシステムを推論するための単なる「脱獄カード」ですか?

編集:

これは誰かがこの投稿で言ったような炎の餌ではありませんが、誰かが私を撃墜して、証拠と言うことができることを私が望んでいる本物の質問です、それは純粋です。

また、他のそれほど純粋ではない関数型言語や、優れた設計を使用するいくつかのオブジェクト指向言語に関する質問を検討し、純粋性を比較しています。これまでのところ、私の非常に限られたFPの世界では、モナドの純度をまだ高めていません。しかし、純度の問題ではるかに重要な不変性のアイデアが好きです。

4

8 に答える 8

80

次のミニ言語を取ります:

data Action = Get (Char -> Action) | Put Char Action | End

Get f意味:文字を読み取り、cアクションを実行しますf c

Put c a意味:文字を書きc、アクションを実行しますa

これは、「xy」を出力してから2文字を要求し、それらを逆の順序で出力するプログラムです。

Put 'x' (Put 'y' (Get (\a -> Get (\b -> Put b (Put a End)))))

あなたはそのようなプログラムを操作することができます。例えば:

conditionally p = Get (\a -> if a == 'Y' then p else End)

これはタイプがありますAction -> Action-それはプログラムを取り、最初に確認を求める別のプログラムを与えます。別のものがあります:

printString = foldr Put End

これにはタイプString -> Actionがあります-文字列を受け取り、次のように文字列を書き込むプログラムを返します

Put 'h' (Put 'e' (Put 'l' (Put 'l' (Put 'o' End))))

HaskellのIOも同様に機能します。それを実行するには副作用を実行する必要がありますが、純粋な方法で、実行せずに複雑なプログラムを構築できます。プログラムの説明(IOアクション)を計算していて、実際に実行しているわけではありません。

void execute(Action a)Cのような言語では、実際にプログラムを実行する関数を書くことができます。Haskellでは、を書くことでそのアクションを指定しますmain = a。コンパイラはアクションを実行するプログラムを作成しますが、アクションを実行する他の方法はありません(汚いトリックは別として)。

明らかにGetPutオプションだけでなく、ファイルの操作や同時実行など、他の多くのAPI呼び出しをIOデータ型に追加できます。

結果値の追加

ここで、次のデータ型について考えてみます。

data IO a = Get (Char -> Action) | Put Char Action | End a

前のActionタイプは、と同等ですIO ()。つまり、「void」に相当する「unit」を常に返すIO値です。

このタイプはHaskellIOと非常によく似ていますが、Haskell IOでのみ抽象データ型です(定義にアクセスできず、一部のメソッドにのみアクセスできます)。

これらは、何らかの結果で終了する可能性のあるIOアクションです。このような値:

Get (\x -> if x == 'A' then Put 'B' (End 3) else End 4)

タイプIO Intがあり、Cプログラムに対応しています。

int f() {
  char x;
  scanf("%c", &x);
  if (x == 'A') {
    printf("B");
    return 3;
  } else return 4;
}

評価と実行

評価と実行には違いがあります。Haskellの式を評価して、値を取得できます。たとえば、2 + 2::Intを4::Intに評価します。タイプIOaを持つHas​​kell式のみを実行できます。これには副作用がある可能性があります。実行するPut 'a' (End 3)と、文字aが画面に表示されます。次のようにIO値を評価する場合:

if 2+2 == 4 then Put 'A' (End 0) else Put 'B' (End 2)

あなたが得る:

Put 'A' (End 0)

ただし、副作用はありません。評価を実行しただけで、無害です。

どのように翻訳しますか

bool comp(char x) {
  char y;
  scanf("%c", &y);
  if (x > y) {       //Character comparison
    printf(">");
    return true;
  } else {
    printf("<");
    return false;
  }
}

IO値に?

いくつかの文字を修正し、「v」と言います。comp('v')これがIOアクションで、指定された文字を「v」と比較します。同様に、comp('b')はIOアクションであり、指定された文字を「b」と比較します。一般に、compは文字を受け取り、IOアクションを返す関数です。

comp('b')Cのプログラマーとして、あなたはそれがブール値であると主張するかもしれません。Cでは、評価と実行は同じです(つまり、同じことを意味するか、同時に発生します)。Haskellではありません。実行された後にブール値を与えるIOアクションにcomp('b') 評価されます。(正確には、xの代わりに「b」を使用した場合のみ、上記のようにコードブロックに評価されます。)

comp :: Char -> IO Bool
comp x = Get (\y -> if x > y then Put '>' (End True) else Put '<' (End False))

ここで、にcomp 'b'評価しGet (\y -> if 'b' > y then Put '>' (End True) else Put '<' (End False))ます。

数学的にも意味があります。Cでは、int f()は関数です。数学者にとって、これは意味がありません-引数のない関数ですか?関数のポイントは引数を取ることです。関数int f()はと同等である必要がありますint f。Cの関数は数学関数とIOアクションをブレンドしているため、そうではありません。

ファーストクラス

これらのIO値はファーストクラスです。整数のタプルのリストを作成できるのと同じように[[(0,2),(8,3)],[(2,8)]]、IOを使用して複雑な値を作成できます。

 (Get (\x -> Put (toUpper x) (End 0)), Get (\x -> Put (toLower x) (End 0)))
   :: (IO Int, IO Int)

IOアクションのタプル:最初に文字を読み取って大文字で出力し、次に文字を読み取って小文字で返します。

 Get (\x -> End (Put x (End 0))) :: IO (IO Int)

文字を読み取って終了し、画面xに書き込むIO値を返すIO値。x

Haskellには、IO値を簡単に操作できる特別な機能があります。例えば:

 sequence :: [IO a] -> IO [a]

これは、IOアクションのリストを取得し、それらを順番に実行するIOアクションを返します。

モナド

モナドは(conditionally上記のような)いくつかのコンビネータであり、プログラムをより構造的に書くことができます。タイプで構成する関数があります

 IO a -> (a -> IO b) -> IO b

IOaと関数a->IObを指定すると、タイプIObの値が返されます。最初の引数をC関数として記述しa f()、2番目の引数を.b g(a x)のプログラムを返すように記述した場合g(f(x))。上記のアクション/IOの定義があれば、その関数を自分で作成できます。

モナドは純粋さにとって必須ではないことに注意してください-私が上で行ったように、いつでもプログラムを書くことができます。

純度

純度について重要なことは、参照透過性と、評価と実行を区別することです。

Haskellでは、持っているf x+f x場合はそれを。に置き換えることができます2*f x。Cでは、画面に何かを印刷したり、変更したりできるため、f(x)+f(x)一般的にはと同じではありません。2*f(x)fx

純粋さのおかげで、コンパイラーははるかに自由度が高く、より適切に最適化できます。それは計算を再配置することができますが、Cではそれがプログラムの意味を変えるかどうかを考えなければなりません。

于 2010-06-25T13:30:30.033 に答える
9

モナドには本質的に特別なものは何もないことを理解することが重要です。したがって、モナドはこの点で「脱獄」カードを表すものではありません。モナドを実装または使用するために必要なコンパイラ(または他の)魔法はありません。モナドはHaskellの純粋関数型環境で定義されています。特に、sdcvvcは、実装のバックドアに頼ることなく、純粋関数型の方法でモナドを定義する方法を示しています。

于 2010-06-25T14:39:22.773 に答える
6

「黒板の数学以外の」コンピュータシステムについて推論することはどういう意味ですか?それはどのような理由でしょうか?推測航法?

副作用と純粋関数は視点の問題です。名目上副作用のある関数を、世界のある状態から別の状態に移動させる関数と見なすと、それは再び純粋です。

2番目の引数である世界を与え、それが完了したときに新しい世界を渡すように要求することで、すべての副作用関数を純粋にすることができます。もうまったくわかりませんが、次のような署名があるC++と言います。read

vector<char> read(filepath_t)

新しい「純粋なスタイル」では、次のように処理します。

pair<vector<char>, world_t> read(world_t, filepath_t)

これは実際、すべてのHaskellIOアクションがどのように機能するかです。

これで、IOの純粋なモデルができました。よかった。それができなかった場合は、ラムダ計算とチューリングマシンは同等の形式ではない可能性があり、説明が必要になります。まだ完了していませんが、私たちに残された2つの問題は簡単です。

  • 構造には何が入りworld_tますか?砂のすべての粒、草の葉、失恋、黄金の夕日の説明?

  • ワールドは1回だけ使用するという非公式のルールがあります。つまり、IO操作が行われるたびに、使用していたワールドを破棄します。しかし、これらすべての世界が浮かんでいるので、私たちはそれらを混同させなければなりません。

最初の問題は簡単です。私たちが世界の検査を許可しない限り、私たちはそこに何かを保管することについて自分自身を悩ます必要はないことがわかります。新しい世界が以前の世界と等しくないことを確認する必要があります(コンパイラが、時々行われるように、いくつかの世界を生成する操作を逸脱して最適化しないようにしますC++)。これを処理する方法はたくさんあります。

混同される世界については、図書館の中に通過する世界を隠して、世界にたどり着く方法がないようにしたいのです。結局のところ、モナドは計算で「サイドチャネル」を隠すための優れた方法です。IOモナドを入力します。

少し前に、Haskellメーリングリストであなたのような質問があり、そこで私はより詳細に「サイドチャネル」に入りました。これがRedditスレッド(私の元の電子メールにリンクしている)です:

http://www.reddit.com/r/haskell/comments/8bhir/why_the_io_monad_isnt_a_dirty_hack/

于 2010-06-26T05:30:01.193 に答える
4

私は関数型プログラミングに非常に慣れていませんが、これが私がそれを理解する方法です:

haskellでは、一連の関数を定義します。これらの関数は実行されません。彼らは評価されるかもしれません。

特に評価される関数が1つあります。これは、一連の「アクション」を生成する定数関数です。アクションには、機能の評価とIOおよびその他の「現実の」ものの実行が含まれます。これらのアクションを作成して渡す関数を作成できます。関数がunsafePerformIOで評価されるか、メイン関数によって返される場合を除いて、これらのアクションは実行されません。

つまり、Haskellプログラムは、命令型プログラムを返す他の関数で構成される関数です。Haskellプログラム自体は純粋です。明らかに、その命令型プログラム自体はそうすることはできません。実世界のコンピューターは、定義上、不純です。

この質問にはさらに多くのことがあり、その多くはセマンティクスの質問です(プログラミング言語ではなく人間)。モナドも、ここで説明したものよりも少し抽象的です。しかし、これは一般的にそれについて考えるのに役立つ方法だと思います。

于 2010-06-25T12:39:37.657 に答える
4

私はそれを次のように考えています。プログラムは、有用であるために外の世界と何かをしなければなりません。(任意の言語で)コードを書くときに何が起こっているか(または起こっているはずです)は、できるだけ純粋で副作用のないコードを書き、IOを特定の場所に囲い込むように努めることです。

Haskellにあるのは、エフェクトを厳密に制御するために、この書き込み方向にさらにプッシュされているということです。コアと多くのライブラリには、膨大な量の純粋なコードもあります。Haskellは本当にこれがすべてです。Haskellのモナドは多くのことに役立ちます。そして、それらが使用されてきたものの1つは、不純物を処理するコードの封じ込めです。

非常に容易な言語と一緒に設計するこの方法は、より信頼性の高い作業を作成するのに役立つ全体的な効果があり、動作を明確にするための単体テストが少なくて済み、構成を通じてより多くの再利用が可能になります。

あなたの言っていることが正しく理解できれば、これは偽物ではなく、「脱獄カード」のように私たちの頭の中にあるだけだとは思いません。ここでのメリットは非常に現実的です。

于 2010-06-25T13:12:10.997 に答える
4

sdcwcの一種のIOの構築の拡張バージョンについては、HackageのIOSpecパッケージを見ることができます:http://hackage.haskell.org/package/IOSpec

于 2010-06-25T14:34:55.320 に答える
1

Haskellは本当に純粋ですか?

絶対的な意味で:いいえ。

プログラムを実行するソリッドステートチューリングマシン(Haskellまたはその他)は、ステートアンドエフェクトデバイスです。プログラムがそのすべての「機能」を使用するには、プログラムは状態と効果の使用に頼る必要があります

その蔑称的な用語に起因する他のすべての「意味」については、次のとおりです。

最も顕著な特徴が状態である機械の上に状態のない計算モデルを仮定することは、控えめに言っても奇妙な考えのようです。モデルと機械の間のギャップは広いため、ブリッジするのにコストがかかります。ハードウェアサポート機能はこの事実を脇に置くことはできません:それは練習のための悪い考えのままです。

これはやがて関数型言語の主人公にも認識されてきました。彼らはさまざまなトリッキーな方法で状態(および変数)を導入しました。これにより、純粋関数型の特性が損なわれ、犠牲になりました。古い用語はだまされています。

ニクラウス・ヴィルト


モナディック型を使用すると、実際に言語が純粋になりますか?

いいえ。これは、型を使用して境界を定める1つの方法にすぎません。

  • 目に見える副作用がまったくない定義-;
  • 目に見える副作用をもたらす可能性のある定義-アクション

代わりに、 Cleanと同じように、一意性タイプを使用できます...


モナディックタイプの使用は、黒板の数学の外で、現実の世界のコンピュータシステムを推論するための単なる別の「ジェイルフリーカードから抜け出す」のでしょうか?

Haskell 2010レポートIOに記載されているタイプの説明を考えると、この質問は皮肉です。

IOタイプは、外界と相互作用する操作(アクション)のタグとして機能します。IOタイプは抽象です。ユーザーにはコンストラクターは表示されません。およびクラスIOのインスタンスです。MonadFunctor

...別の答えの用語を借りる:

[…]IOは魔法です(実装はありますが、表記はありません)[…]

抽象的であるため、IOタイプは「ジェイルフリーカードから抜け出す」ではありません。HaskellでのI / Oの動作を説明するには、複数のセマンティクスを含む複雑なモデルが必要です。詳細については、以下を参照してください。


いつもそうだったわけではありません-Haskellはもともと少なくとも部分的に見えるI/Oメカニズムを持っていました。それを持っている最後の言語バージョンはHaskell1.2でした。当時のタイプmainは次のとおりです。

main :: [Response] -> [Request]

これは通常、次のように省略されます。

main :: Dialogue

どこ:

type Dialogue = [Response] -> [Request]

Response大きなデータ型ではありRequestますが、謙虚でした。

対話ベースのI/Oの要求および応答データ型の簡素化された定義

Haskellのモナディックインターフェースを使用したI/Oの出現は、すべてを変えました-もはや目に見えるデータ型ではなく、抽象的な説明だけです。その結果、、、などが実際にどのようにIO定義されるかは、Haskellの各実装に固有のものになりました。return(>>=)

(なぜ古いI / Oメカニズムが放棄されたのですか?「厄介な分隊への取り組み」では、その問題の概要を説明しています。)

最近では、より適切な質問は次のようになります。

オーウェン・スティーブンスが機能的I / Oへのアプローチで述べているように:

I / Oは特に活発な研究分野ではありませんが、新しいアプローチがまだ発見されています[…]

Haskell言語はまだI/Oの参照透過性モデルを持っているかもしれませんが、それほど多くの論争を引き付けることはありません...

于 2020-06-25T09:45:28.397 に答える
-2

いいえ、そうではありません。IOモナドは副作用と可変状態があるため不純です(競合状態はHaskellプログラムで可能であるため、純粋FP言語は「競合状態」のようなものを知りません)。本当に純粋なFPは、一意性タイピングを備えたClean、またはHaskellではなくFRP(関数型リアクティブプログラミング)を備えたElmです。Haskellは大きな嘘の1つです。

証拠 :

import Control.Concurrent 
import System.IO as IO
import Data.IORef as IOR

import Control.Monad.STM
import Control.Concurrent.STM.TVar

limit = 150000
threadsCount = 50

-- Don't talk about purity in Haskell when we have race conditions 
-- in unlocked memory ... PURE language don't need LOCKING because
-- there isn't any mutable state or another side effects !!

main = do
    hSetBuffering stdout NoBuffering
    putStr "Lock counter? : "
    a <- getLine
    if a == "y" || a == "yes" || a == "Yes" || a == "Y"
    then withLocking
    else noLocking

noLocking = do
    counter <- newIORef 0
    let doWork = 
        mapM_ (\_ -> IOR.modifyIORef counter (\x -> x + 1)) [1..limit]
    threads <- mapM (\_ -> forkIO doWork) [1..threadsCount]
    -- Sorry, it's dirty but time is expensive ...
    threadDelay (15 * 1000 * 1000)
    val <- IOR.readIORef counter
    IO.putStrLn ("It may be " ++ show (threadsCount * limit) ++ 
        " but it is " ++ show val) 

withLocking = do
    counter <- atomically (newTVar 0)
    let doWork = 
        mapM_ (\_ -> atomically $ modifyTVar counter (\x -> 
            x + 1)) [1..limit]
    threads <- mapM (\_ -> forkIO doWork) [1..threadsCount]
    threadDelay (15 * 1000 * 1000)
    val <- atomically $ readTVar counter
    IO.putStrLn ("It may be " ++ show (threadsCount * limit) ++ 
        " but it is " ++ show val)
于 2015-05-12T12:35:34.527 に答える