Haskellerへの道 #12 - Hello, World! (IOアクション)

Last Edited: 6/25/2024

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

Haskell & Hello World

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 にバインドし、後でその結果の文字列を使用できるようにします。 getLinedo について混乱してしまって以下のような間違いをしてしまう人もいます。

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アクションを持つ必要があり、putStrLnmain の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アクションの出力を greetingname にバインドしています。 しかし、do ブロック内で let を使用することで冗長性を避けることができます。

main = do
  let greeting = "Hello, "
  let name = "Alice!"
  putStrLn $ greeting ++ name

クイズ

この記事では、学習した内容を確認するためのクイズを設けます。記事のメイン部分を読んだ後に、ぜひ自分で問題を解いてみることを強くお勧めします。各問題をクリックすると答えが表示されます。

リソース