このブログ記事では、Haskellの重要な概念であるIOアクションを紹介します。

Hello, World!
ついに、Haskellで"Hello, World!"を出力する方法について説明する準備が整いました。それでは、コードを見てみましょう。
main :: IO ()
main = putStrLn "Hello, World!"
各部分を理解するために分解してみましょう。まず、main
という名前を使うのは命名規則であり、
GHCがこの関数を実行することを認識するためです。したがって、GHCiではなくGHCを使用して ghc --make hello-world.hs
でコンパイルし、./hello-world
で実行すると、main
関数が実行されます。(あるいは、runhaskell hello-world.hs
を使ってプログラムを即時実行することもできます。)
main 関数の型が IO ()
と割り当てられていることがわかります。IO
は入力出力(IO)アクションを指し、
環境(例えばキーボードや画面)と相互作用しながら入力を出力にマッピングできます。()
はこのIOアクションの出力タイプであり、
出力がないことを示す空のタプルです。関数の純粋性を破っていることに気づいたかもしれませんが、IOアクションは副作用を持っているため、
それは正しいです。実際には純粋さをどこまで追求できるかには限界があります。完全に純粋にすると、
関数の実行結果を見ることすらできなくなるからです。IOアクションは、環境と相互作用しつつ、可能な限り純粋さを保つように設定されています。
putStrLn
は文字列を出力するためのIOアクション(型は IO ()
)です。入力は文字列でなければならないため、
putStrLn 3
はエラーになります。print
関数は、Show
タイプクラスに属する任意の型の値を出力できます。
これは putStrLn . show
によって実現されます。しかし、文字列の場合は print
が "" を文字列に追加してしまうため、
print
よりも putStrLn
が一般的には好まれます。実際、GHCiでは関数の出力を表示するために print
が裏で使用されます。
"Hello, World!"を出力するだけのプログラムにしては多くの説明がありましたが、各部分を理解することは、 将来的にIOアクションを扱う上で非常に役立ちます。
Do と Return
do
記法を使用して、複数のIOアクションを1つのIOアクションにグループ化できます。
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn "Hello, " ++ name ++ "!"
name <- getLine
が何をしているのかを理解しましょう。getLine
は型 IO String
のIOアクションで、ユーザー入力を受け取り、文字列を出力します。
<-
記法を使用して結果を変数 name
にバインドし、後でその結果の文字列を使用できるようにします。
getLine
と do
について混乱してしまって以下のような間違いをしてしまう人もいます。
main = do
putStrLn "What is your name?"
putStrLn "Hello, " ++ getLine ++ "!"
しかし、getLine
の型は IO String
であり、String
ではありません。getLine
の出力にアクセスするには、
<-
記法を使用する必要があります。また、IOアクションは環境と相互作用する関数であるため、IOアクションを再帰的に使用できます。
main = do
putStrLn "What is your name?"
name <- getLine
if null name
then return ()
else do
putStrLn $ "Hello, " ++ name ++ "!"
main
上記のIOアクションでは、ユーザー入力を取得し、"Hello, name!"を出力します。しかし、実行を終了する代わりに、ユーザー入力がnull
になるまで実行を繰り返します。
これは、if
文を使用し、main
の中で main
を使用することで実現されます。また、else
の中で do
が使用されていることがわかります。
これは、各ケースが正確に1つのIOアクションを持つ必要があり、putStrLn
と main
の2つのIOアクションを1つにグループ化する必要があるためです。
return ()
はどうでしょうか?前述したように、各ケースが正確に1つのIOアクションを持つ必要があります。しかし、ユーザーが名前を提供しなかった場合、何もしたくありません。
したがって、何もしないIOアクション return ()
を使用します。()
部分は、何も出力しないこと、 ()
を出力することを示します。わかりずらい場合return
は <-
の反対と考えると良いでしょう。
<-
はIOアクションの結果をバインドし、return
は逆に特定の結果を持つIOアクションを作成します。この性質を用いて、以下のようなプログラムを作ることもできます。
main = do
greeting <- return "Hello, "
name <- return "Alice!"
putStrLn greeting ++ name
上記では、return
を使用して"Hello"と"Alice!"という出力を生成するだけのIOアクションを作成し、<-
でそれらのIOアクションの出力を greeting
と name
にバインドしています。
しかし、do
ブロック内で let
を使用することで冗長性を避けることができます。
main = do
let greeting = "Hello, "
let name = "Alice!"
putStrLn $ greeting ++ name
クイズ
この記事では、学習した内容を確認するためのクイズを設けます。記事のメイン部分を読んだ後に、ぜひ自分で問題を解いてみることを強くお勧めします。各問題をクリックすると答えが表示されます。
リソース
- Philipp, Hagenlocher. 2020. Haskell for Imperative Programmers #15 - IO. YouTube.
- NA. Input and Output. Learn You a Haskell for Great Good.