今回は Haskell の入出力処理について説明します。Haskell は純粋な関数型言語なので、入出力などのように副作用を含む処理の取り扱いは、他のプログラミング言語とは大きく異なります。
Haskell はプログラムを「純粋な世界」と「副作用のある世界」の 2 つに分離します。純粋な世界から副作用のある世界の処理を呼び出すことはできません。逆に、副作用のある世界から純粋な世界にある関数を呼び出すことはできます。
副作用のある世界でプログラムを作る場合、Haskell でも手続き型言語のようなプログラミングスタイルにならざるをえません。このような副作用のある世界を Haskell は「IO モナド (IO Monad)」を使って操作します。
まずは最初に、文字列を標準出力(画面)に表示してみましょう。次の例を見てください。
ghci> putStr "Hello, World!\n" Hello, World! ghci> putStrLn "Hello, World!" Hello, World! ghci> :t putStr putStr :: String -> IO () ghci> :t putStrLn putStrLn :: String -> IO ()
putStr, putStrLn は画面に文字列を出力する「副作用 (side effect)」が目的の関数です。hello, world は出力結果であり、関数の返り値ではありません。関数の型を見てください。どちらの関数も String を受け取り、返り値の型が IO () になっています。
"IO データ型" は「I/O アクション」を表していて、副作用を伴う何かしらの処理を行って、必要であれば何かしらの結果を返します。putStr, putStrLn の返り値は空のタプル () を格納した I/O アクションで、() をユニット (unit) といいます。unit 型のデータは () しかありません。unit は何も値がない (空の値である) ことを表すために用いられます。
putStr, putStrLn で表示できるデータは文字列だけですが、関数 print を使うと型クラス Show のインスタンスであれば何でも表示することができます。
ghci> :t print print :: Show a => a -> IO () ghci> print 1 1 ghci> print 1.234 1.234 ghci> print "hello, world" "hello, world" ghci> print [1,2,3,4,5] [1,2,3,4,5] ghci> print ('a', 'c') ('a','c')
ghci の対話モードは print を使ってデータを表示しています。
次は端末 (標準入力) からデータを読み込む関数 getLine を説明します。
ghci> :t getLine getLine :: IO String ghci> getLine foo bar baz <--- 入力 "foo bar baz"
getLine は標準入力から 1 行読み込み、それを文字列にして返します。ここで、getLine が返す型は IO String であることに注意してください。インタプリタ ghci では文字列をそのまま表示しているように見えますが、これは対話モードでの動作であり、putStr で getLine の返り値を表示しようとしてもエラーになります。
ghci> putStr getLine => エラー
また、IO は I/O アクションを表すデータ型なので、データ構築子がわかればパターンマッチングで格納されているデータを取り出すことができるのですが、IO 型のデータ構築子は「非公開」となっています。私たちが勝手にその中のデータを取り出すことはできないのです。I/O アクションからデータを取り出す方法はあとで説明します。
関数 readLn を使うと、指定した型のデータを読み込むことができます。
ghci> :t readLn readLn :: Read a => IO a ghci> readLn :: IO Integer 1234567890 1234567890 ghci> readLn :: IO Double 1.2345 1.2345 ghci> readLn :: IO String "abcdefg" "abcdefg" ghci> readLn :: IO [Int] [1,2,3,4,5] [1,2,3,4,5]
readLn のデータ型は IO a なので、型の指定は "IO データ型" となります。
I/O アクションからデータを取り出す簡単な方法は「do 構文」を使うことです。次の例を見てください。
ghci> do {a <- getLine; putStrLn a} hello, world! <-- 入力 hello, world! <-- 出力 ghci> :t do {a <- getLine; putStrLn a} do {a <- getLine; putStrLn a} :: IO ()
do 構文は複数の I/O アクションまたは式を順番に実行していきます。do はレイアウトを使用することができます。矢印 <- は I/O アクションから値を取り出す構文です。
getLine で取得した IO String の String を取り出して変数 a にセットします。a は局所変数として扱われ、有効範囲は do 構文の中だけです。a の型は String になるので、a をそのまま putStrLn に渡して文字列を表示することができます。
do 構文の返り値は最後に実行した I/O アクションの値になります。do 構文は I/O アクションをまとめたものになるので、do 構文 [*1] も I/O アクションのひとつとして扱われます。したがって、do 構文の型も "IO データ型" になります。
それでは簡単な例題として、入力をそのままエコーバックする関数 echo を作ってみましょう。プログラムは次のようになります。
リスト : echo echo :: IO () echo = do a <- getLine if a == "" then return () else do putStrLn a echo
getLine で標準入力から 1 行読み込み、その値を変数 a に束縛します。a が空文字列 "" でなければ、do の中で putStrLn を呼び出して a を画面に出力し、echo を再帰呼び出しします。
このように、do は入れ子にすることができます。また、if の中に do を入れてもかまいません。ただし、then 節と else 節の返り値は同じデータ型、つまり IO () でなければいけません。
空文字列の場合は echo を終了します。return はデータ型にデータを格納して返す関数です。この場合、return () は I/O アクションの中で呼び出されているので IO () を返します。return の詳しい説明は「モナド (Monad)」のところで行う予定です。
他のプログラミング言語の場合、return は関数を終了して呼び出し元に値を返す働きをしますが、Haskell の return はまったく異なる働きをすることに注意してください。
簡単な実行例を示します。
ghci> echo abcd <-- 入力 abcd efgh <-- 入力 efgh foo bar baz <-- 入力 foo bar baz <-- 入力 (リターンキーのみ) ghci>
最後にリターンキーだけを入力すると、getLine は空文字列を返すので、echo を終了することができます。
ghci> getLine >>= putStrLn hello, world <-- 入力 hello, world <-- 出力 ghci>do 構文は I/O アクション専用の構文ではありません。Haskell のモナドは型クラスのひとつであり、モナドのインスタンスであれば do 構文を使用することができます。
do 構文の中で let を使って局所変数を定義することができます。
let 変数1 = 式1 変数2 = 式2 ・・・・ 変数N = 式N
簡単な例を示しましょう。
リスト : 四則演算 calc :: IO () calc = do putStr "Input Integer1 > " x <- readLn :: IO Integer putStr "Input Integer2 > " y <- readLn :: IO Integer let a = x + y b = x - y c = x * y d = x `div` y n1 = show x n2 = show y putStrLn (n1 ++ "+" ++ n2 ++ "=" ++ show a) putStrLn (n1 ++ "-" ++ n2 ++ "=" ++ show b) putStrLn (n1 ++ "*" ++ n2 ++ "=" ++ show c) putStrLn (n1 ++ "/" ++ n2 ++ "=" ++ show d)
標準入力から整数値を 2 つ読み込み、四則演算の結果を表示します。readLn は IO Integer なので、IO から整数値を取り出すために <- を使います。四則演算の結果は let で定義した変数 a, b, c, d に格納します。あとは putStrLn で結果を表示するだけです。
それでは実行してみましょう。
ghci> calc Input Integer1 > 12345 Input Integer2 > 6789 12345+6789=19134 12345-6789=5556 12345*6789=83810205 12345/6789=1
ところで、ghci の対話モードは「副作用のある世界」なので、putStrLn, getLine, echo など副作用のある関数を呼び出すことが可能です。プログラムをコンパイルする場合は、関数 main から副作用のある関数を呼び出します。main は副作用のある処理を呼び出すことができる唯一の関数です。次のリストを見てください。
リスト : echo (echo1.hs) echo :: IO () echo = do a <- getLine if a == "" then return () else do putStrLn a echo main :: IO () main = echo -- 別解 main :: IO () main = do a <- getLine if a == "" then return () else do putStrLn a main
Haskell の場合、コンパイルしたプログラムは関数 main から実行が開始されます。main も I/O アクションを行う関数なので、副作用をともなう関数 echo を呼び出すことができます。また、別解のように main の中で getLine, putStrLn を呼び出すこともできますし、main を再帰呼び出しすることもできます。
それでは実行してみましょう。
$ stack ghc echo1.hs [1 of 2] Compiling Main ( echo1.hs, echo1.o ) [2 of 2] Linking echo1 $ ./echo1 foo bar baz <-- 入力 foo bar baz hello, world <-- 入力 hello, world <-- 入力 (リターンキーのみ) $
I/O アクションを行う関数は通常の関数から呼び出すことはできません。高階関数の場合、たとえば map print [1 .. 10] としても、データが表示されることはなく I/O アクションを格納したリストが返ってきます。
ghci> map print [1..10] <interactive>:1:1: error: [GHC-39999] • No instance for ‘Show (IO ())’ arising from a use of ‘print’ • In a stmt of an interactive GHCi command: print it
I/O アクションを表すデータは Show のインスタンスではないので画面に表示できません。この場合、sequence という関数を使うとリストに格納された I/O アクションを実行することができます。
ghci> :t sequence sequence :: (Traversable t, Monad m) => t (m a) -> m (t a) ghci> :t map print map print :: Show a => [a] -> [IO ()] ghci> :t sequence . map print sequence . map print :: Show a => [a] -> IO [()] ghci> sequence $ map print [1..10] 1 2 3 4 5 6 7 8 9 10 [(),(),(),(),(),(),(),(),(),()]
sequence は I/O アクション専用の関数ではなく「モナド (Monad)」のインスタンスであれば適用することができます。I/O アクションもモナドのひとつです。print の返り値は IO () なので、sequence は IO に格納されている ( ) を取り出して、それをリストに格納して返します。
実際は sequence よりも mapM という関数を使うと便利です。
ghci> :t mapM mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b) ghci> :t mapM print mapM print :: (Traversable t, Show a) => t a -> IO (t ()) ghci> mapM print [1..10] 1 2 3 4 5 6 7 8 9 10 [(),(),(),(),(),(),(),(),(),()]
このように、sequence と map を組み合わせた処理を mapM だけで行うことができます。
返り値が不要な場合は関数 mapM_ を使います。
ghci> :t mapM_ mapM_ :: (Foldable t, Monad m) => (a -> m b) -> t a -> m () ghci> :t mapM_ print mapM_ print :: (Foldable t, Show a) => t a -> IO () ghci> mapM_ print [1..10] 1 2 3 4 5 6 7 8 9 10
このほかにも、Haskell には便利な関数が多数用意されていますが、モナドと関連があるので、ここでまでにしておきましょう。モナドを勉強するときに再度取り上げることにします。