次のミニ言語を取ります:
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
。コンパイラはアクションを実行するプログラムを作成しますが、アクションを実行する他の方法はありません(汚いトリックは別として)。
明らかにGet
、Put
オプションだけでなく、ファイルの操作や同時実行など、他の多くの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を持つHaskell式のみを実行できます。これには副作用がある可能性があります。実行する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)
f
x
純粋さのおかげで、コンパイラーははるかに自由度が高く、より適切に最適化できます。それは計算を再配置することができますが、Cではそれがプログラムの意味を変えるかどうかを考えなければなりません。