🐬

main::IO()について(Haskell)

2022/07/08に公開

この記事ではmain :: IO()の意味について考えてみます。HelloWorldを出力するプログラムのうちこちらの部分です。

main :: IO ()
main = putStrLn "HelloWorld"

mainという変数はこれから何度も書くことになるのですが、mainとは何で、main :: IO()とは何か?ということについて考えてみたいと思います。まず先に結論からです。

main:変数(スタートポイントになる)
「::」: 変数・関数のデータ型を指定
IO():IO()というデータ型を持つ

もっとカンタンに言えば、int appleを宣言しているのと仕組みは同じです。

IO ()型を持つ変数main
int型を持つ変数apple

データ型を持つ変数..という意味では上記2つは全く同じです。1つずつ解説していきます。

mainとは?

mainはHaskellでプログラムを実行する際、スタートポイントとなる変数です。Haskellはmainを通してでしかディスプレイの表示やネットワークへの接続などはできません。たった1つのエントリーポイントであり、ここが窓口となって外部世界とのやり取りを行います。

外部世界とのやり取りのための窓口

そしてなぜmainがたった1つの窓口となっているかと言えば、純粋な部分・不純な部分を分けるためでした。Haskellは数学の関数を利用してプログラムを組み立てる言語です。数学の純粋さを保つためには「外部とのやり取り」などの不純な部分・もっと言えば「状態を変えてしまうような処理」との分離が必要でした。

:: 型宣言

次に「::」です。「::」は型宣言と呼び、変数・関数のデータ型を指定する機能を持ちます。今回は変数mainのデータ型を明示的に決めたいので「::」をつけています。

IO() : IO型のデータ型

そして最後のIO()というのは変数mainのデータ型を意味します。intやcharなどのデータ型と全く同じです。そして注意して頂きたいのが変数mainに対してIO型のデータ型を指定している点です。つまりmainはIO型のデータ型を持つ変数となっています。その意味では冒頭にお話した通り、int appleと全く同じです。

IO型のデータ型とは?

ではIO型のデータ型とは何でしょうか?「IO型の変数mainの中に入っているデータが外部との入出力に使われる」という点です。この変数mainの中に格納したデータがディスプレイに表示されたり、ネットワークに接続されます。ここではputStrLn関数が持つ”HelloWorld”という情報を変数mainへ格納し、それを外部世界(ディスプレイ)へ出力する仕組みとなっています。

()とは?(プログラムの世界には何も影響がない)

ではIOの後にある()は何を意味するのでしょうか?IO ()の()は「結果が何もないこと」を意味します。()は正確にはタプルといって、異なるデータ型を格納する配列のようなモノです。タプルを付けることで少なくともIOを値化します。しかし格納された情報はディスプレイなどへ送れるので何もこちらには影響がないですよね。そこでこちらの世界には「何もない」として()内には何も記述しません。

Haskellのお約束を破っているけれども..

IO型のデータ型には()以外にも様々な種類があります。ちなみにIO型を持つデータのことをHaskellではアクションと呼びます(正確には型IO aを持つ式のこと)。IO型のデータには値を返さないモノ(今回は関数の処理結果はディスプレイへと表示されていましたよね)、入力を取らないモノ、同じ入力が与えられても常に同じ結果にはならないモノ..などHaskellのお約束を破っています。Haskellは状態を変えてしまうような副作用を禁止していました。

全ての式が値をもつ
全ての関数が引数・戻り値を持つ
どんな時でも常に同じ値を返す
副作用を禁止する(値を返却する以外はダメ)

でもディスプレイに文字を表示したり、ファイルに読み書きしたいですよね。そこで特別な値をアクションとして用意しています。そしてmainの中に格納されたアクションのデータだけが実行される仕組みとなっています。

putStrLn(「関数は値を返す」というルールを破る)

IO型のデータ型を持つ関数を2つ例としてご紹介します。まずは先ほど出てきたputStrLn関数です。putStrLn関数のデータ型を調べるとこのように出てきます。

Prelude> :t putStrLn
putStrLn :: String -> IO ()

IO ()の()は「何もないこと」を意味していました。「関数は値を返さなければいけない」というお約束を破っており、これはIO型のアクションとなっています。

getLine(「引数を受け取る」というルールを破る)

次にgetLine関数についてです。こちらもデータ型を調べるとこのようになります。

Prelude> :t getLine
getLine :: IO String

putStrLnは文字列を受け取りますが、その結果は外部世界へと出力されました。(つまりこちらのプログラムの中では値は返却されません)逆にgetLineは引数を取りませんが、IO String型を返します。つまりgetLintは「関数は引数を取らないといけない」というルールに反しています。

do表記:IOアクションをまとめる

ちなみにdo表記によって、一連のIOアクションをまとめることができます。これは遅延評価が基本のHaskellで、アクションが評価される順番を確実に指定できるようにするためでもあります。

main :: IO ()
main = do
    putStrLn "HelloWorld"
    putStrLn "こんにちは世界"

例えばこちらのプログラムを実行すると2つ続けて文字列を表示できます。

実行結果
HelloWorld
こんにちは世界

まとめ

IO型のデータ型はHaskellのお約束を破っている点で少し特殊です。ただ「変数に対してデータ型が指定されている」という点ではint appleなどと変わりはありません。特殊なモノと思い込まず、基本に立ち返って考えるとわかりやすいのではないかと思います。

main:変数(スタートポイントになる)
「::」: 変数・関数のデータ型を指定
IO():IO()というデータ型を持つ

Discussion