LoginSignup
6
6

More than 5 years have passed since last update.

Haskell入門ハンズオン! #5 - 当日用資料 (3/5)

Last updated at Posted at 2018-10-29

Haskell入門ハンズオン! #5 - 当日用資料 (3/5)

IOモナド

Haskellでは入出力や状態変化をともなう処理は、IOモナドというかたちで、あつかわれる。「モナド」という難しげな言葉に、おどろく必要はない。理解すれば、どうということはない概念だ。圏論とかを理解する必要はない。さまざまな処理を抽象化してくれる便利な道具だ。今回は「モナド」については、あまりふれない。IOモナドを「組み立て」という側面から説明する。

手続き

つぎのような「手続き」をみてみよう。

puts "hello"
s = gets
puts s

これは、いちおうRubyの例だ。puts "hello"をしたあとに、getsをして、キーボード入力を変数sに代入し、その代入された値sをputsしている。この記法では、「AのつぎにBをする」という「関係」が上下の位置によって暗黙に示されている。

Rubyなどでは「暗黙に」示されている関係が、Haskellでは明示的に演算子になる。

putStrLn "hello" >>
getLine >>=
\s -> putStrLn s

「AのつぎにBをする」はA >> Bと表される。「Cの結果を利用してDをする」はC >>= Dだ。ここでDは「Cの結果」を引数とする関数になる。

do記法

Haskellでは入出力は、演算子(>>)や(>>=)によって「明示的に」組み合わされる。これは、Haskellの美しさでもあり、コードを安全に、きれいに書ける性質でもある。ただ、上から下に流れる処理というのは、メタファとして、わかりやすい。Haskellでは「美しさ」「安全性」を犠牲にすることなく、あたかも、暗黙の前後関係をもつような書きかたができる。それがdo記法だ。前述のコードを再掲する。

putStrLn "hello" >>
getLine >>=
\s -> putStrLn s

これをdo記法を使って書くと、つぎのようになる。

do      putStrLn "hello"
        s <- getLine
        putStrLn s

今回は、時間がないので、このdo記法のかたちだけ覚えておこう。そのうらがわに、明示的な演算子による結合があると、知っておけばいい。do記法で入出力処理を書いてみよう。

% vim samples/do.hs
samples/do.hs
io1 :: IO ()
io1 = do
        putStrLn "hello"
        s <- getLine
        putStrLn s

IOモナドの型は、入力される値がなければ、つぎのようになる。

IO()

変数io1を、「do記法によって組み立てた入出力」を表す値で束縛した。試してみる。

> :load samples/do.hs
> io1
hello
(適当に入力してエンター)world
world

do記法のなかで使える予約語letを使った構文がある。

% vim samples/do.hs
samples/do.hs
io2 :: IO ()
io2 = do
        n <- getLine
        let     msg = "Hello, " ++ n ++ "!"
        putStrLn msg

このように、do記法のなかで変数を定義できる。試してみる。

> :reload
> io2
(名前を入力)Yoshio
Hello, Yoshio!

標準入出力

標準入出力への入出力について学ぶ。標準入力はデフォルトではキーボードからの入力。標準出力はデフォルトでは画面への出力になる。このふたつは、すでに出てきたが、もういちど確認する。対話環境で試してみる。

> putStrLn "hello"
hello
> getLine
(適当に入力)world
"world"

関数putStrLnは文字列を引数にとり、その文字列を標準出力に出力する。入出力getLineは標準入力から1行入力し、それを「つぎの」入出力にわたす。型をみてみよう。

> :type putStrLn
putStrLn :: String -> IO ()
> :type getLine
getLine :: IO String

つぎにわたす値がないとき、入出力の型は、つぎのようになる。

IO ()

つぎにわたすのがString型の値のとき、つぎのようになる。

IO String

改行を追加しないで出力するには、つぎのようにする。

> putStr "hello"
hello>

このように、関数putStrを使う。文字列以外の値を表示するには関数printを使う。

> print 123
123

関数printは、関数showによって文字列化したものを、関数putStrLnによって表示するのと、おなじことだ。

> putStrLn (show 123)
123

ファイル入出力

つぎに、ファイルへの入出力をみてみよう。

> writeFile "tmp_hnh.txt" "Haskell Handson!\n"
> readFile "tmp_hnh.txt"
"Haskell Handson!\n"

writeFileは第1引数のファイルに、第2引数の文字列を書き込む。readFileは第1引数のファイルの内容を読み込む。

現在時刻

現在の時刻を取得する。時刻は入力値として取得される。

> :module Data.Time
> getCurrentTime
2018-10-15 05:54:10.471924992 UTC

:moduleコマンドでモジュールData.Timeを、対話環境に導入している。入出力getCurrentTimeによって現在時刻が得られる。入出力getCurrentTimeの型は、つぎのようになる。

> :type getCurrentTime
getCurrentTime :: IO UTCTime

分岐

順接、分岐、反復の3つで、すべてのアルゴリズムが記述可能だ。「順接」は、「AのつぎにB」ということだ。すでにみた。つぎは「分岐」をみてみよう。

標準出力に表示するか、ファイルに保存するこわ選ぶ例だ。つぎのようなコードを書く。

% vim samples/currentTime.hs
samples/currentTime.hs
import Data.Time

currentTime :: IO ()
currentTime = do
        putStrLn "現在時刻を取得します"
        now <- getCurrentTime
        putStrLn "ファイルに保存しますか?(Y/N)"
        s <- getLine
        if s == "Y" || s == 'y'
                then writeFile "currentTime.txt"
                        (show now ++ "\n")
                else putStrLn (show now)

if式を使って分岐を記述することができた。試してみよう。

> :load samples/currentTime.hs
> currentTime
現在時刻を取得します
ファイルに保存しますか?(Y/N)
(nを入力)n
2018-11-21 04:34:15.538628867 UTC

反復

反復、つまり「くりかえし」は再帰で書くことができる。

% vim samples/sum.hs
sampls/sum.hs
total :: Integer -> IO Integer
total s = do
        ln <- getLine
        let     n = read ln
        if n < 0
                then return s
                else total (s + n)

入力値が0より小さければ、sの値をかえし、そうでなければ、s + nを引数に自身を呼び出す。試してみる。

> :load samples/sum.hs
> total 0
3
24
11
- 1
38

3, 24, 11を入力し、終了させるために- 1を入力した。3 + 24 + 11 = 38なので、38が表示される。

独立して実行できるプログラム

ここまで、対話環境で試してきた。ここで、コンパイルして独立実行できる実行可能形式の作りかたを示す。Haskellでは、変数mainを束縛した入出力が実行される。sum.hsに入出力mainを追加する。

% vim samples/sum.hs
samples/sum.hs
main :: IO ()
main = do
        s <- total 0
        print s

つぎのようにしてコンパイルして、実行する。

% stack ghc -- samples/sum.hs -o sum
% ./sum
3
24
11
-1
38

IOモナドのまとめ

Haskellでは入出力や状態変化をともなう処理には、IOモナドという仕組みが使われる。IOモナドはモナドの一種だが、ここでは「モナド」については説明しない。手続き型言語では「暗黙に」されていた「組み立て」が、明示的な「演算子」によって、おこなわれる。手続き型言語のような書きかたのできるdo記法がある。順接、分岐、反復の実装をみた。Haskellでは、コンパイルされた実行可能形式は、変数mainを束縛した入出力を実行する。

追加の知識

このあと、プロジェクト例を示すが、そのために必要な追加の知識を、ここで学ぶ。まずは型Intを紹介する。つぎに、Haskellにおいて、かつては、標準的だった乱数生成ライブラリについて学ぶ。

整数型Int

Haskellには多倍長整数Integerのほかに、固定長整数Intがある。Int型の値は処理系依存の最小値、最大値をもつ。

> minBound :: Int
-9223372036854775808
> maxBound :: Int
9223372036854775807

minBound, maxBoundは多相的な値であり、それぞれの型について、その最小値、最大値を示す。うえの例は64ビットOSでの値であり、32ビットOSでは異なった値になる。

乱数生成

かつて標準的だったモジュールSystem.Randomはパッケージrandomに含まれている。

> :module System.Random
> randomIO :: IO Integer
7070170094128852650
> randomIO :: IO Double
0.19573673642621692

毎回ちがう乱数値を得るには、「なんらかの状態変化」が必要なので、randomIOは「入出力」になっている。randomIOは多相的なので、型注釈で型を指定している。randomIOで乱数値を生成するには、生成される値の型が、型クラスRandomのインスタンスである必要がある。型クラスRandomは、だいたい、つぎのように定義されている。

class Random a where
        randomR :: RandomGen g =>
                (a, a) -> g -> (a, g)
        random :: RandomGen g => g -> (a, g)

RandomGen gは型クラス制約であり、型gが型クラスRandomGenのインスタンスであることを示している。型gが型クラスRandomGenのインスタンスならば、型gにたいして、つぎのような関数が存在する。

next :: g -> (Int, g)

これは、g型の値が「乱数の種」であり、関数nextによって、Int型の乱数値と新しい「乱数の種(g型の値)」とが生成される、ということだ。

じゃんけん型

じゃんけん型を定義する。

% vim samples/janken.hs
samples/janken.hs
import System.Random

data Janken = Rock | Paper | Scissors
        deriving Show

じゃんけん型を型クラスRandomのインスタンスにする。

% vim samples/janken.hs
samples/janken.hs
instance Random Janken where
        randomR = undefined
        random g = let (n, g') = next g in
                case n `mod` 3 of
                        0 -> (Rock, g')
                        1 -> (Paper, g')
                        2 -> (Scissors, g')
                        _ -> error "never occur"

関数randomRは「範囲を指定して乱数値を得る」関数だ。いまは使わないので、とりあえずundefinedにしておく。関数randomは「乱数の種」から、乱数値と新しい種をつくる。関数nextでInt型の値をつくり、それを3通りの値に変換している。試してみる。

> :load samples/janken.hs
> randomIO :: IO Janken
Scissors
> randomIO :: IO Janken
Paper
> randomIO :: IO Janken
Scissors
> randomIO :: IO Janken
Rock

バッファリング

標準入出力はバッファリングされている。標準入出力のバッファリングのモードを表示する。

% vim samples/buffer.hs
samples/buffer.hs
import System.IO

main :: IO ()
main = do
        bi <- hGetBuffering stdin
        bo <- hGetBuffering stdout
        print bi
        print bo

コンパイルして実行する。

% stack ghc -- samples/buffer.hs -o buffer
% ./buffer
LineBuffering
LineBuffering

標準入力、標準出力ともにLineBufferingだ。つまり、改行が入力(出力)されるまで入力(出力)はバッファリングされる。標準出力への出力は改行の出力まで、おくらされる。これは、たとえば「質問」の出力のあとで、改行したくないようなとき、こまる。

% vim samples/yesno0.hs
samples/yesno0.hs
main :: IO ()
main = do
        putStr "どうする?(Y/N)"
        a <- getLine
        putStrLn $ "Your answer is " ++ a ++ "."

コンパイルして実行する。

% stack ghc -- samples/yesno0.hs -o yesno
% ./yesno
(入力)foo
どうする?(Y/N): Your answer is foo.

質問が表示されるまえに、入力が必要になる。バッファをフラッシュする必要がある。yesno.hsの先頭にimport文を追加する。

% vim samples/yesno1.hs
samples/yesno1.hs
import System.IO

質問文の表示のあとにhFlushを追加する。

% vim samples/yesno1.hs
samples/yesno1.hs
...
        putStr "どうする?(Y/N): "
        hFlush stdout
...

コンパイルして実行する。

% stack ghc -- samples/yesno1.hs -o yesno
% ./yesno
どうする?(Y/N): (入力)Y
Your answer is Y.

hFlushをすることで、改行を待たずに表示される。

その4へ

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6