M.Hiroi's Home Page

お気楽 Haskell プログラミング入門

初級編 : 簡単な入出力処理

Copyright (C) 2013-2021 Makoto Hiroi
All rights reserved.

はじめに

今回は 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 データ型" となります。

●do 構文

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 を終了することができます。

-- note --------
[*1] do 構文は「モナド (Monad)」の構文糖衣です。getLine で得た文字列を putStr で表示する場合、モナドの演算子 >>= を使って行うことができます。
ghci> getLine >>= putStrLn
hello, world    <-- 入力
hello, world    <-- 出力
ghci>
do 構文は I/O アクション専用の構文ではありません。Haskell のモナドは型クラスのひとつであり、モナドのインスタンスであれば do 構文を使用することができます。

●do の中で let を使う

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

●関数 main

ところで、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 アクションとマップ関数

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 には便利な関数が多数用意されていますが、モナドと関連があるので、ここでまでにしておきましょう。モナドを勉強するときに再度取り上げることにします。


初版 2013 年 2 月 17 日
改訂 2021 年 1 月 17 日