Haskellでプログラムを書くときは、必ずIOモナドを使いますよね。なんてったって、main関数がIOモナドですから!!
main :: IO ()
main = do
print "Hello"
print "World"
-- > Hello
-- > World
でも、なんでIOモナドなんてややこしい道具を使うのでしょうか?
これには二つの理由があります。
IOモナドは副作用のカプセル化
一つ目の理由は、IOモナド以外の関数について参照透過性を維持するためです。
ユーザーからの入力を受け付ける関数にgetLine
がありますよね。もし、この関数がIOモナドを使わずにgetLine :: String
と定義されていたら、何が起こるか考えてみましょう。
addTitle :: String -> String
addTitle tt = getLine ++ tt
関数getLineを使って、関数addTitleを定義してみました。
あるユーザーの入力が"斎藤 浩司"1だったときの関数addTitleの動きを確認しましょう。
引数 | 返り値 |
---|---|
"さん" | "斎藤 浩司さん" |
"さま" | "斎藤 浩司さま" |
"社長" | "斎藤 浩司社長" |
次に、別のユーザーの入力が"木村 栞"1だったときの関数addTitleの動きを確認しましょう。
引数 | 返り値 |
---|---|
"さん" | "木村 栞さん" |
"さま" | "木村 栞さま" |
"社長" | "木村 栞社長" |
同じ引数でも返り値が異なっていますね。
これは、参照透過性のルールに反しています。しかし、この関数が参照透過性のルールに反していることは関数の型を見てもわからないので、処理内容を逐一確認する必要があります。どうにかして、関数の型を見るだけで参照透過性の有無が分かるようにしたいですよね。そこで、参照透過性の無い処理(= 副作用を伴う処理)をIOモナドに包んでしまうことにしたのです。
IOモナドは同期処理の仕組み
もう一つ理由は、(1)すべての副作用が、(2)意図した順番で、実行されることを保証するためです。
(1)すべての副作用が実行されることの保証について
普通にprint関数をprint :: String -> ()
で定義して、処理を順番に行い最後の処理の結果を返す演算子;
を定義して2、
main :: ()
main = print "Hello" ; print "World"
みたいにしたらダメなんでしょうか?
これではダメなんです。こうすると"Hello"が表示されなくなるんです。
これは、Haskellが遅延評価でプログラムを実行するからです。言い換えると、必要になるまではプログラムは実行されないということです。演算子;
は最後の処理(= 式)の結果を返す演算子でした。そのため、main関数の結果を計算するうえでは、print "World"
の部分の実行結果のみが必要となり実行されます。一方、print "Hello"
の部分の実行結果は必要とされないので、まったく実行されずにプログラムが終了します。その結果、"Hello"が表示されません。
(2) 副作用が意図した順番で実行されることの保証について
渡された文字列を画面に表示した後、画面に表示した文字数を返すような関数printNを考えます。
関数printNがprintN :: String -> Integer
と定義されているとき、
main :: ()
main = let x = print "Hello", y = print "World" in y + x
このプログラムを実行すると、画面に"World Hello"と表示されます。。。あれ??
"Hello"と"World"が逆さまになってしまいました。これは、y + x
の部分を評価するときに、変数y
つまりprint "World"
のほうが先に実行されてしまうために起こります。
main = do
x <- unsafeInterleaveIO (print "eval")
print "end"
print x
UnsafeIO
こうして、IOモナドによって副作用がうまく扱えるようになりました。めでたしめでたし。。。
とはならないんです。
IOモナドは副作用の扱いを楽にしてくれる代わりに、多くの制約を伴います。そのため、副作用の取り扱いは慎重に行うから、IOモナドを経由せずに副作用を使いたいという要望がでてきます。そういう人のために、IOモナドの制約を緩める関数群がSystem.IO.Unsafeパッケージに用意されています。
ここでは、System.IO.Unsafeパッケージの関数のうち、重要な二つの関数を紹介します。
unsafePerformIO
unsafePerformIO :: IO a -> a
IOモナドを使う理由の一つ目として、IOモナドは副作用のカプセル化であると説明しました。この関数は、そのカプセル化の制約を緩めます。つまり、IOモナドから副作用の結果を取り出します。
この関数がunsafe(= 安全でない)とされているのは、IOモナドから副作用の結果を取り出すことによって、参照透過性が損なわれる可能性があるからです。そのため、この関数を使う際は、参照透過性に十分注意する必要があります。
addTitle :: String -> String
addTitle tt = (unsafePerformIO getLine) ++ tt
main = print $ addTitle "さん"
-- < 斎藤 浩司
-- > 斎藤 浩司さん
-- < 木村 栞
-- > 木村 栞さん
また、IOモナドは同期処理の仕組みで説明した、副作用の実行保証と実行順序保証もなくなります。
printN = \s -> do
print s
return (length s)
let x = unsafePerformIO (print "Hello"); y = unsafePerformIO (print "World") in y + x
-- > World
-- > Hello
さらに、この関数は型安全ではありません。例えば、次のプログラムは実行時エラーとなります。
test :: IORef [a]
test = unsafePerformIO $ newIORef []
main = do
writeIORef test [42]
bang <- readIORef test
print (bang :: [Char])
(System.IO.Unsafe - Hackageより引用)
詳しくは、System.IO.Unsafe - Hackageを参照してください。
unsafeInterleaveIO
unsafeInterleaveIO :: IO a -> IO a
この関数は、IOモナドは同期処理の仕組みで説明した、副作用の実行保証と実行順序保証の制約を緩めます。
printN = \s -> do
print s
return (length s)
main = do
x <- unsafeInterleaveIO (printN "Hello")
y <- unsafeInterleaveIO (printN "World")
print x
-- > Hello
-- > 5
printN = \s -> do
print s
return (length s)
main = do
x <- unsafeInterleaveIO (printN "Hello")
y <- unsafeInterleaveIO (printN "World")
print (y + x)
-- > World
-- > Hello
-- > 10
練習問題 (難易度★)
次のプログラムを実行すると、画面に何と表示されるでしょうか。
printN = \s -> do
print s
return (length s)
hello = unsafePerformIO (printN "Hello")
main = do
let x = hello
y = hello
print (x + y)
答え
> Hello
> 10
元ネタは禁断の機能「unsafePerformIO」の深淵なので、詳しくはそちらの記事を参照してください。