M.Hiroi's Home Page

Functional Programming

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

[ PrevPage | Haskell | NextPage ]

モジュール

プログラムを作っていると、以前作った関数と同じ処理が必要になる場合があります。いちばんてっとり早い方法はソースファイルからその関数をコピーすることですが、賢明な方法とはいえません。このような場合、自分で作成した関数をライブラリとしてまとめておくと便利です。

ライブラリの作成で問題になるのが「名前の衝突」です。複数のライブラリを使うときに、同じ名前の関数や変数が存在すると、そのライブラリは正常に動作しないでしょう。この問題は「モジュール (module)」を使うと解決することができます。

モジュールを簡単に説明すると、データ構造とそれを操作する関数を一つにまとめるための仕組みです。最近は、モジュールに相当する機能を持つプログラミング言語が多くなりました。Haskell にもモジュールがあるので、データ構造や関数をモジュールにまとめておけば、ユーザーにとって使いやすいライブラリを構築することができます。

実際、Haskell には多くのモジュールが標準で添付されています。これらのモジュールを使うことで、プログラムを効率的に開発することができます。

●モジュールの使い方

Haskell の場合、モジュールは import 文を使って読み込みます。

import モジュール名

import 文で指定できるモジュールは一つだけです。複数のモジュールを読み込む場合は、モジュールの数だけ import 文を記述してください。

ghci の場合、コマンド :m でモジュールを読み込むことができます。

:m + モジュール名 ...

:m コマンドは複数のモジュールを指定することができます。その場合はモジュール名を空白で区切ってください。

簡単な例を示します。

Prelude> :t union

<:interactive>:1:1: error: Variable not in scope: union
Prelude> :m + Data.List
Prelude Data.List> :t union
union :: Eq a => [a] -> [a] -> [a]
Prelude Data.List> union [1,2,3,4] [3,4,5,6]
[1,2,3,4,5,6]

union は集合 (リスト) の和を求める関数で、モジュール Data.List に定義されています。Data.List をロードすると union を利用することができます。

必要な関数 (変数) だけインポートすることもできます。

import モジュール名 (名前, ...)

モジュール名の後ろのカッコの中にインポートする関数を指定します。逆に、インポートしたくない関数を指定することもできます。

import モジュール名 hiding (名前, ...)

hiding を付けると、指定された関数はインポートされません。それ以外の関数はすべてインポートされます。

複数のモジュールをインポートすると、名前が重複する場合があります。名前の重複を避けるため、Haskell には別名を付ける方法が用意されています。

import qualified モジュール名

qualified を指定すると、"モジュール名" + "." + "名前" でアクセスすることができます。これを「修飾付きインポート」といいます。このとき、モジュール名が長いとプログラムを書くのが面倒になるので、次のように別名を指定することができます。

import qualified モジュール名 as 別名

as の後ろに名前 (別名) を指定します。これで "別名" + "." + "名前" でアクセスすることができます。

●モジュールの作り方

モジュールは module を使って定義します。

module モジュール名 (名前, ...) where

通常はファイルの先頭に module を記述します。たとえば、モジュール名を Foo とすると、ファイル名はモジュール名と同じ名前 Foo.hs としなければなりません。モジュール名の後のカッコの中にはエクスポート (export) する名前 (関数名、変数名、型構築子、データ構築子など) を記述します。省略した場合はモジュールで定義されたすべての名前がエクスポートされます。そして、where 以降にモジュールに格納するデータ構造や関数を定義します。where はレイアウトが使えますが、モジュールの場合はインデントしないで行頭からプログラムを書くのが一般的なようです。

import でインポートされる名前は、module でエクスポートされている名前だけです。エクスポートされていない名前はインポートすることができません。モジュール内だけで使用する関数は、エクスポートしなければ「非公開」となります。同様に、データ構築子をエクスポートしないと、データを生成したりパターンマッチングでデータを取り出すことができなくなります。この場合、公開されている関数だけを使ってデータ構造にアクセスすることになります。

簡単な例を示しましょう。

リスト : Fruit.hs

module Fruit (Fruit(..), getPrice) where

data Fruit = Apple | Grape | Orange deriving (Show, Eq)

priceList :: [(Fruit, Integer)]
priceList = [(Apple, 100), (Grape, 150), (Orange, 200)]

getPrice :: Fruit -> Maybe Integer
getPrice x = lookup x priceList


リスト : fruit1.hs

import Fruit

sumPrice :: [Fruit] -> Integer
sumPrice xs = foldl (\a x -> let (Just v) = getPrice x in a + v) 0 xs

モジュール Fruit にはデータ型 Fruit の定義と果物の値段が記述されています。型構築子とデータ構築子は次のようにエクスポートします。

1. 型構築子(データ構築子, ...)
2. 型構築子(..)

型構築子の後ろのカッコの中にデータ構築子を指定します。データ構築子が複数ある場合はカンマで区切ります。2 番目のようにカッコの中で記号 .. を指定すると、型構築子で定義されているデータ構築子がすべてエクスポートされます。priceList は果物の値段を表す連想リストです。getPrice は priceList から果物の値段を求める関数です。priceList はエクスポートされていないので、果物の値段は getPrice を使って求めることになります。

fruit1.hs では、モジュール Fruit をインポートして、関数 sumPrice を定義しています。sumPrice は getPrice を使ってリストに格納された果物の合計値を求める関数です。

それでは実行してみましょう。

Prelude> :l fruit1
[1 of 2] Compiling Fruit            ( Fruit.hs, interpreted )
[2 of 2] Compiling Main             ( fruit1.hs, interpreted )
Ok, modules loaded: Fruit, Main.
*Main> priceList

<interactive>:3:1: Not in scope: `priceList'
*Main> :t Apple
Apple :: Fruit
*Main> getPrice Apple
Just 100
*Main> sumPrice [Apple, Grape, Grape, Orange, Orange, Orange]
1000

priceList はアクセスできませんが、getPrice で Apple の値段を求めることができます。最後に、sumPrice で合計値を求めています、

●スタックとは?

次は簡単なデータ構造の例題として「スタック (stack)」と「キュー (queue)」を取り上げます。最初にスタックの動作を説明します。次の図を見てください。


                  図 : スタックの動作例

上図は、バネがついた容器を表していて、上から品物を出し入れすることができます。初めは空の状態です。ここに品物を乗せると、重さによってバネを圧縮し、品物が容器に格納されます。さらにもう一つ品物を上に乗せると、さらにバネを圧縮し、その品物も容器に格納することができます。バネが限界まで圧縮されると、もう品物は追加できなくなります。取り出す場合は、上にある品物から行います。一つ取り出すと、その分バネが伸びて下にある品物が上に押し出されます。

この容器の動作が、スタックの動作なのです。スタックにデータを追加する操作をプッシュ (PUSH) といい、スタックからデータを取り出す操作をポップ (POP) といいます。品物をデータに見立てれば、データ A をスタックにプッシュし (2)、次にデータ B をプッシュします (3)。データを取り出す場合、あとから入れたデータ B が先にポップされ (4)、その次にデータ A がポップされてスタックが空になります (5)。このように、スタックはあとから入れたデータが先に取り出されるので、後入れ先出し (LIFO : Last-In, First-Out) と呼ばれます。

●スタックの実装

Haskell の場合、スタックはリストを使って簡単に実現することができます。データを追加するときはリストの先頭に追加し、データを取り出すときはリストの先頭から行うように操作を限定すると、それはスタックの動作と同じになります。

プログラムは次のようになります。

リスト : スタック

module Stack (
  Stack,
  emptyStack,
  singleton,
  push,
  pop,
  top,
  isEmptyStack
) where

-- スタックの定義
data Stack a = S [a] deriving Show

-- 空のスタック
emptyStack :: Stack a
emptyStack = S []

-- 要素が一つのスタックを作る
singleton :: a -> Stack a
singleton x = S [x]

-- データの追加
push :: Stack a -> a -> Stack a
push (S xs) x = S (x:xs)

-- データの削除
pop :: Stack a -> (a, Stack a)
pop (S []) = error "Empty Stack"
pop (S (x:xs)) = (x, S xs)

-- データの取得
top :: Stack a -> a
top (S []) = error "Empty Stack"
top (S (x:_)) = x

-- スタックは空か
isEmptyStack :: Stack a -> Bool
isEmptyStack (S []) = True
isEmptyStack (S _)  = False

最初に data 宣言でスタック Stack a を定義します。スタックの本体はリストです。新しいスタックを生成するため、空のスタックを変数 emptyStack にセットします。関数 shigleton は引数 x を格納したスタックを返します。

スタックの操作関数は簡単です。push はデータをリストの先頭に追加します。pop はリストの先頭要素とそれを取り除いたスタックを返します。データの取得は関数 top で行います。スタックが空の場合、pop と top を適用することができないので、error でエラーを送出します。関数 isEmptyStack はスタックが空かチェックする述語です。

Haskell は純粋な関数型言語なので、スタックを破壊的に書き換えることはできません。push と pop は新しいスタックを返すことに注意してください。

簡単な使用例を示します。モジュール Stack.hs は ghci のコマンド :m でロードできなかったので、コマンド :l で Stack.hs を読み込んでいます。

Prelude> :l Stack
[1 of 1] Compiling Stack            ( Stack.hs, interpreted )
Ok, modules loaded: Stack.
*Stack> a = push emptyStack 1
*Stack> :t +d a
a :: Stack Integer
*Stack> a
S [1]
*Stack> singleton 1
S [1]
*Stack> b = push a 2
*Stack> b
S [2,1]
*Stack> c = push b 3
*Stack> c
S [3,2,1]
*Stack> top c
3
*Stack> (x, d) = pop c
*Stack> x
3
*Stack> d
S [2,1]
*Stack> e = foldl push emptyStack [1..10]
*Stack> e
S [10,9,8,7,6,5,4,3,2,1]
*Stack> isEmptyStack emptyStack
True
*Stack> isEmptyStack d
False

正常に動作していますね。純粋な関数型言語の場合、変数の値を書き換えることができないので、push や pop の返り値を別の変数に格納する必要があります。ご注意くださいませ。

●キューとは?

次はキューについて説明します。キューは「待ち行列」といわれるデータ構造です。たとえばチケットを買う場合、窓口に長い列ができますが、それと同じだと考えてください。チケットを買うときは、列の途中に割り込むことはできませんね。いちばん後ろに並んで順番を待たなければいけません。列の先頭まで進むと、チケットを購入することができます。

このように、要素を取り出す場合は列の先頭から行い、要素を追加する場合は列の後ろに行うデータ構造がキューなのです。キューは「先入れ先出し (FIFO : first-in, first-out)」とも呼ばれます。

キューにデータを入れることを enqueue といい、キューからデータを取り出すことを dequeue といいます。リストを使ってキューを実装する場合、上図のようにキューの先頭とリストの先頭を対応させます。すると、キューからデータを取り出すには、リストの先頭からデータを取り出すだけですみます。これはとても簡単ですね。

ただし、キューにデータを入れるには、リストの最後尾にデータを追加することになるため、ちょっとした工夫が必要になります。たとえば、データの追加に演算子 ++ を使うと、データを追加するたびにリスト(キュー)がコピーされてしまいます。このため、キューに格納されているデータが多くなると時間がかかるようになります。

これを回避する方法はいろいろ考えられるのですが、今回は SML/NJ や OCaml などの関数型言語で使われている方法を紹介します。次の図を見てください。

上図は 2 つのリストでキューを表しています。データを取り出すときは front のリストを、データを追加するときは rear のリストを使います。front と rear で一つのキューを構成し、rear のリストはデータを逆順で格納することになります。ようするに、front が先頭で rear が最後尾になるわけです。上図のキューを一つのリストで表すと [0, 1, 2, 3, 4, 5] になります。

したがって、front が空リストでも rear にデータがあれば、キューは空ではありません。rear のリストを逆順にして front にセットし、rear を空リストにします。これで front からデータを取り出すことができます。キューが空の状態は front と rear が両方とも空リストの場合です。

●キューの実装

それではプログラムを作りましょう。次のリストを見てください。

リスト : キューの実装

module Queue (
  Queue,
  emptyQueue,
  singleton,
  enqueue,
  dequeue,
  front,
  isEmptyQueue
) where

-- キューの定義
data Queue a = Q [a] [a] deriving Show

-- 空のキュー
emptyQueue :: Queue a
emptyQueue = Q [] []

-- 要素が一つのキューを返す
singleton :: a -> Queue a
singleton x = Q [x] []

-- データの追加
enqueue :: Queue a -> a -> Queue a
enqueue (Q front rear) x = Q front (x:rear)

-- データの取り出し
dequeue :: Queue a -> (a, Queue a)
dequeue (Q [] []) = error "Empty Queue"
dequeue (Q [] rear) = dequeue (Q (reverse rear) [])
dequeue (Q (x:xs) rear) = (x, Q xs rear)

-- 先頭データの参照
front :: Queue a -> a
front (Q [] []) = error "Empty Queue"
front (Q [] rear) = front (Q (reverse rear) [])
front (Q (x:_) _) = x

-- キューは空か
isEmptyQueue :: Queue a -> Bool
isEmptyQueue (Q [] []) = True
isEmptyQueue (Q _  _)  = False

まず data 宣言でデータ型 Queue a を定義します。データ構築子は Q [a] [a] で、第 1 要素が front で第 2 要素が rear になります。emptyQueue は空のキュー (Q [ ] [ ]) を表す変数です。関数 singleton は引数 x を格納したキューを返します。

関数 equeue はキューにデータ x を追加します。これは x を rear の先頭に追加するだけです。関数 dequeue はキューからデータを取り除きます。キューが空の場合は error でエラーを送出します。front が空リストの場合は、新しいキュー Q (reverse rear) [ ] を作って dequeue を再帰呼び出しします。front にデータがある場合は先頭要素を取り除くだけです。関数 front はキューの先頭要素を返します。処理は dequeue とほとんど同じで、違いは front の先頭データ x を返すだけです。関数 isEmptyQueue は、キューが空であれば True を、そうでなければ False を返します。

それでは簡単な実行例を示します。

Prelude> :l Queue
[1 of 1] Compiling Queue            ( Queue.hs, interpreted )
Ok, modules loaded: Queue.
*Queue> a = enqueue emptyQueue 1
*Queue> a
Q [] [1]
*Queue> :t +d a
a :: Queue Integer
*Queue> singleton 1
Q [1] []
*Queue> b = enqueue a 2
*Queue> b
Q [] [2,1]
*Queue> c = enqueue b 3
*Queue> c
Q [] [3,2,1]
*Queue> front c
1
*Queue> (x, d) = dequeue c
*Queue> x
1
*Queue> d
Q [2,3] []
*Queue> (y, e) = dequeue d
*Queue> y
2
*Queue> e
Q [3] []
*Queue> (z, f) = dequeue e
*Queue> z
3
*Queue> f
Q [] []
*Queue> isEmptyQueue f
True
*Queue> isEmptyQueue e
False
*Queue> g = foldl enqueue emptyQueue [1..10]
*Queue> g
Q [] [10,9,8,7,6,5,4,3,2,1]
*Queue> dequeue g
(1,Q [2,3,4,5,6,7,8,9,10] [])

正常に動作していますね。

●問題

つぎの変数と関数を格納したモジュール Prime.hs を作成してください。

  1. 素数を格納したリスト primes
  2. 引数 n が素数か判定する関数 isPrime n
  3. 引数 n を素因数分解する関数 factorization n
primes :: [Integer]
isPrime :: Integer -> Bool
factorization :: Integer -> [(Integer, Integer)]

モジュール Prime を使って次の数列を定義してください。

  1. フィボナッチ素数 (フィボナッチ数で素数) を格納したリスト fiboPrime
  2. 双子素数を格納したリスト twinPrime












●解答

リスト : 解答例 (Prime.hs)

module Prime (primes, isPrime, factorization) where

isPrime :: Integer -> Bool
isPrime n = iter primes
  where
    iter (p:ps)
      | p * p > n = True
      | n `mod` p == 0 = False
      | otherwise      = iter ps

primesFrom :: Integer -> [Integer]
primesFrom n
  | isPrime n = n : primesFrom (n + 2)
  | otherwise = primesFrom (n + 2)

primes = 2 : 3 : 5 : primesFrom 7

factor :: Integer-> Integer -> Integer -> (Integer, Integer)
factor n m c
  | n `mod` m /= 0 = (c, n)
  | otherwise      = factor (n `div` m) m (c + 1)

factorization :: Integer -> [(Integer, Integer)]
factorization n = iter primes n []
  where
    iter (p:ps) x a
      | x == 1    = reverse a
      | x < p * p = reverse ((x, 1) : a)
      | otherwise = case factor x p 0 of
                      (0, _) -> iter ps x a
                      (c, m) -> iter ps m ((p, c) : a)

モジュール Prime.hs はコンパイルすると速くなります。

$ stack ghc -- -O -c -dynamic Prime.hs
$ stack exec ghci
GHCi, version 8.8.4: https://www.haskell.org/ghc/  :? for help
Prelude> :l Prime
Ok, one module loaded.
Prelude Prime> :show modules
Prime            ( Prime.hs, Prime.o )

Prelude Prime> take 25 primes
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97]
Prelude Prime> isPrime (2^31 - 1)
True
Prelude Prime> isPrime (2^32 - 1)
False

Prelude Prime> factorization (2^31 - 1)
[(2147483647,1)]
Prelude Prime> factorization (2^32 - 1)
[(3,1),(5,1),(17,1),(257,1),(65537,1)]

Prelude Prime> fibo = 0 : 1 : zipWith (+) (tail fibo) fibo
Prelude Prime> take 15 fibo
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377]
Prelude Prime> fiboPrime = filter isPrime (drop 3 fibo)
Prelude Prime> take 8 fiboPrime
[2,3,5,13,89,233,1597,28657]

Prelude Prime> twinPrime = filter (\(x, y) -> y - x == 2) $ zipWith (,) primes (tail primes)
Prelude Prime> take 20 twinPrime
[(3,5),(5,7),(11,13),(17,19),(29,31),(41,43),(59,61),(71,73),(101,103),(107,109),(137,139),
 (149,151),(179,181),(191,193),(197,199),(227,229),(239,241),(269,271),(281,283),(311,313)]

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

簡単な入出力処理

今回は Haskell の入出力処理について説明します。Haskell は純粋な関数型言語なので、入出力などのように副作用を含む処理の取り扱いは、他のプログラミング言語とは大きく異なります。Haskell はプログラムを「純粋な世界」と「副作用のある世界」の 2 つに分離します。純粋な世界から副作用のある世界の処理を呼び出すことはできません。逆に、副作用のある世界から純粋な世界にある関数を呼び出すことはできます。

副作用のある世界でプログラムを作る場合、Haskell でも手続き型言語のようなプログラミングスタイルにならざるをえません。このような副作用のある世界を Haskell は「IO モナド (IO Monad)」を使って操作します。

●標準入出力

まずは最初に、文字列を標準出力(画面)に表示してみましょう。次の例を見てください。

Prelude> putStr "Hello, World!\n"
Hello, World!
Prelude> putStrLn "Hello, World!"
Hello, World!
Prelude> :t putStr
putStr :: String -> IO ()
Prelude> :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 のインスタンスであれば何でも表示することができます。

Prelude> :t print
print :: Show a => a -> IO ()
Prelude> print 1
1
Prelude> print 1.234
1.234
Prelude> print "hello, world"
"hello, world"
Prelude> print [1,2,3,4,5]
[1,2,3,4,5]
Prelude> print ('a', 'c')
('a','c')

ghci の対話モードは print を使ってデータを表示しています。

次は端末 (標準入力) からデータを読み込む関数 getLine を説明します。

Prelude> :t getLine
getLine :: IO String
Prelude> getLine
foo bar baz          <--- 入力
"foo bar baz"

getLine は標準入力から 1 行読み込み、それを文字列にして返します。ここで、getLine が返す型は IO String であることに注意してください。インタプリタ ghci では文字列をそのまま表示しているように見えますが、これは対話モードでの動作であり、putStr で getLine の返り値を表示しようとしてもエラーになります。

Prelude> putStr getLine
=> エラー

また、IO は I/O アクションを表すデータ型なので、データ構築子がわかればパターンマッチングで格納されているデータを取り出すことができるのですが、IO 型のデータ構築子は「非公開」となっています。私たちが勝手にその中のデータを取り出すことはできないのです。I/O アクションからデータを取り出す方法はあとで説明します。

関数 readLn を使うと、指定した型のデータを読み込むことができます。

Prelude> :t readLn
readLn :: Read a => IO a
Prelude> readLn :: IO Integer
1234567890
1234567890
Prelude> readLn :: IO Double
1.2345
1.2345
Prelude> readLn :: IO String
"abcdefg"
"abcdefg"
Prelude> readLn :: IO [Int]
[1,2,3,4,5]
[1,2,3,4,5]

readLn のデータ型は IO a なので、型の指定は "IO データ型" となります。

●do 構文

I/O アクションからデータを取り出す簡単な方法は「do 構文」を使うことです。次の例を見てください。

Prelude> do {a <- getLine; putStrLn a}
hello, world!    <-- 入力
hello, world!    <-- 出力
Prelude> :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 はまったく異なる働きをすることに注意してください。

簡単な実行例を示します。

*Main> echo
abcd          <-- 入力
abcd
efgh          <-- 入力
efgh
foo bar baz   <-- 入力
foo bar baz
              <-- 入力 (リターンキーのみ)
*Main>

最後にリターンキーだけを入力すると、getLine は空文字列を返すので、echo を終了することができます。

-- note --------
[*1] do 構文は「モナド (Monad)」の構文糖衣です。getLine で得た文字列を putStr で表示する場合、モナドの演算子 >>= を使って行うことができます。
Prelude> getLine >>= putStrLn
hello, world    <-- 入力
hello, world    <-- 出力
Prelude>
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 で結果を表示するだけです。

それでは実行してみましょう。

*Main> 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 を再帰呼び出しすることもできます。

それでは実行してみましょう。

$ ghc echo1.hs
[1 of 1] Compiling Main             ( echo1.hs, echo1.o )
Linking echo1 ...

$ ./echo1
foo bar baz    <-- 入力
foo bar baz
hello, world   <-- 入力
hello, world
               <-- 入力 (リターンキーのみ)

$ 

●I/O アクションとマップ関数

I/O アクションを行う関数は通常の関数から呼び出すことはできません。高階関数の場合、たとえば map print [1 .. 10] としても、データが表示されることはなく I/O アクションを格納したリストが返ってきます。

Prelude> map print [1..10]

<interactive>:1:1: error:
    • 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 アクションを実行することができます。

Prelude> :t sequence
sequence :: (Traversable t, Monad m) => t (m a) -> m (t a)
Prelude> :t map print
map print :: Show a => [a] -> [IO ()]
Prelude> :t sequence . map print
sequence . map print :: Show a => [a] -> IO [()]
Prelude> 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 という関数を使うと便利です。

Prelude> :t mapM
mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
Prelude> :t mapM print
mapM print :: (Traversable t, Show a) => t a -> IO (t ())
Prelude> mapM print [1..10]
1
2
3
4
5
6
7
8
9
10
[(),(),(),(),(),(),(),(),(),()]

このように、sequence と map を組み合わせた処理を mapM だけで行うことができます。

返り値が不要な場合は関数 mapM_ を使います。

Prelude> :t mapM_
mapM_ :: (Foldable t, Monad m) => (a -> m b) -> t a -> m ()
Prelude> :t mapM_ print
mapM_ print :: (Foldable t, Show a) => t a -> IO ()
Prelude> mapM_ print [1..10]
1
2
3
4
5
6
7
8
9
10

このほかにも、Haskell には便利な関数が多数用意されていますが、モナドと関連があるので、ここでまでにしておきましょう。モナドを勉強するときに再度取り上げることにします。


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

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

[ PrevPage | Haskell | NextPage ]