6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-30

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

その1はこちら

演習1から4の模範解答と解説

できただろうか。模範解答を示し、それを解説する。

演習1. Hello, world!

演習1は、標準出力への"Hello, world!"の書き出しだ。模範解答は、つぎのようになる。

samples/hello.hs
main :: IO ()
main = putStrLn "Hello, world!"

mainはIOであり、つぎのIOにわたす値はないので、型はIO ()になる。関数putStrLnの型はString -> IO ()であり、文字列を引数としてとり、その文字列を出力するIO値をかえす。そのIO値が変数mainを束縛する。そして、処理系がそのIO値を実行する。コンパイルして実行してみよう。

% stack ghc -- samples/hello.hs -o hello
% ./hello
Hello, world!

演習2. 再帰、またはリスト

演習2は、1からnまでの積をもとめる関数の作成だ。模範解答は、つぎのようになる。

samples/product0.hs
productN :: Integer -> Integer
productN 0 = 1
productN n = n * productN (n - 1)

productN 0は1で、productN nはnにproductN (n - 1)をかけたものだ。ただ、これだと負の引数に対して、無限ループになる(本当は「無限」ではないけど、ね)ので、型宣言のつぎの行に、以下を追加しておく。

samples/product0.hs
productN n | n < 0 = error "negative argument"

試してみる。

% stack ghci
> :load samples/product0.hs
> productN 5
120

演習2の別解

演習2の別解を、つぎに示す。

samples/product1.hs
productN :: Integer -> Integer
productN n = product [1 .. n]

[m .. n]は、mからnまで1ずつ増やしていった値の、すべてを要素とするリストだ。関数productはリストの要素の、すべてをかけあわせた値を計算する関数である。このように、直接、再帰を使わずにリストや、それをあつかう関数を利用して、その組み合わせで関数を作るほうがスマートだ。

演習3. 代数的データ型

演習3-1では、円と長方形を含むデータ型を作成する。模範解答は、つぎのようになる。

samples/figure.hs
data Figure = Circle Double | Rectangle Doubl Double deriving Show

値構築子CircleとRectangleをもつ型Figureを定義した。値構築子Circleはふたつの、RectangleはひとつのDouble型の値を保持する。deriving Showは対話環境で表示するために必要だ。対話環境で試してみる。

% stack ghci
> :load samples/figure.hs
> Circle 3
Circle 3.0
> Rectangle 4 7
Rectangle 4.0 7.0

演習3-2では、円と長方形の面積をもとめる。模範解答は、つぎのようになる。

samples/figure.hs
area :: Figure -> Double
area (Circle r) = r * r * pi
area (Rectangle w h) = w * h

パターンマッチで、それぞれの場合の面積を計算している。対話環境で試してみる。

> :reload
> area (Circle 3)
2874333882308138
> area (Rectangle 4 7)
28.0

演習4. 型クラス

演習4-1では、型クラスBoolLikeを定義する。型クラスBoolLikeはBool値に変換できるという性質を示す。型クラスBoolLikeはクラス関数toBoolをもつ。模範解答は、つぎのようになる。

samples/boolLike.hs
class BoolLIke b where
        toBool :: b -> Bool

型クラスを定義している。クラス関数であるtoBoolは、インスタンスとなる型からBool型の値への変換をする。対話環境に読み込んでみる。

> :load samples/boolLike.hs
> :info BoolLike
class BoolLike b where
  toBool :: b -> Bool
  ...

コマンド:infoで、指定した要素の情報をみることができる。型クラスを定義しただけでは、あまり、なにもできないので情報をみてみた。

演習4-2では、型Integerを型クラスBoolLikeのインスタンスにする。模範解答は、つぎのようになる。

samples/boolLike.hs
instance BoolLike Integer where
        toBool 0 = False
        toBool _ = True

引数が0のときにはFalseに、それ以外のときにはTrueにしている。対話環境で試してみる。

> :reload
> toBool (0 :: Integer)
False
> toBool (123 :: Integer)
True

数値リテラルは多相的なので、型注釈で型を指定している。

演習4-3では、型Charを型クラスBoolLikeのインスタンスにする。模範解答は、つぎのようになる。

samples/boolLike.hs
instance BoolLike Char where
        toBool '\NUL' = False
        toBool _ = True

引数が'\NUL'のときにはFalseを、それ以外のときにはTrueをかえす。試してみる。

> :reload
> toBool '\NUL'
False
> toBool 'c'
True

演習4-4では、型クラスBoolLikeのインスタンスである型の値を、ブール値として使用するmyIfを作成する。模範解答は、つぎのようになる。

samples/boolLike.hs
myIf :: BoolLike b => b -> a -> a -> a
myIf b t e = if toBool b then t else e

型クラス制約で型bをBoolLikeのインスタンスにしぼる。関数toBoolの返り値によって、Trueならtをかえし、Falseならeをかえすような定義だ。対話環境で試してみる。

> :reload
> myIf (0 :: Integer) "OK!" "Bad!"
"Bad!"
> myIf (123 :: Integer) "OK!" "Bad!"
"OK!"
> myIf '\NUL' "OK!" "Bad!"
"Bad!"
> myIf 'c' "OK!" "Bad!"
"OK!"

演習5の模範解答と解説

演習5は、小さなアプリケーションの例だ。簡単なたし算、ひき算、かけ算のクイズゲームの例だ。Stackを使って、新規のプロジェクトとして作成する。0から100までの整数どうしのたし算、ひき算と、0から9までの整数どうしのかけ算とを、ランダムに10問、出題し、最後に結果を表示する。

プロジェクトの作成、モジュールの導入など

実際には、すでに用意してあるディレクトリを参照してほしい。Stackのプロジェクトを新規に作成するやりかたを示す。

% stack new calc-quiz

実際には、ディレクトリcalc-quiz/が用意してあるので、そこに移動する。

% cd ../calc-quiz

まず(僕のポリシーとして)、ほかのすべての警告を有効にして、タブ文字への警告をつぶす。ファイルの先頭に、つぎのように追加する。

app/Main.hs
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}

必要なモジュールを導入する。module Main whereのしたにimport文を書く。

app/Main.hs
module Main where

import System.IO
import System.Random

モジュールSystem.Randomはパッケージrandomに含まれる。package.yamlを編集する。dependenciesにrandomを追加する。24行目が追加した行だ。

package.yaml
dependencies:
- base >= 4.7 && < 5
- random

ビルドを試す。

% stack build

1問1問の計算問題をあらわす型Quizを定義する。

% vim app/Main.hs
app/Main.hs
data Quiz
        = Integer :+: Integer
        | Integer :-: Integer
        | Integer :*: Integer
        deriving Show

対話環境で試してみる。

% stack ghci
> 12 :+: 5
12 :+: 5

型Quizをランダムに出題するために、クラスRandomのインスタンスにする。

app/Main.hs
instance Random Quiz where
        randomR = undefined
        random g = let
                (a, g') = randomR (0, 100) g
                (b, g'') = randomR (0, 100) g'
                (o, g''') = randomR (0, 2) g''
                q = case o :: Integer of
                        0 -> a :+: b
                        1 -> a :-: b
                        2 -> (a `mod` 10) :*: (b `mod` 10)
                        _ -> error "never occur" in
                (q, g''')

randomRは最小値と最大値を指定することで、特定の範囲内でのランダム値を生成する関数だ。Quiz型では定義されない。ここでは、値undefinedで束縛してある。0から100までのランダム値をふたつ(a, b)と、0から2までのランダム値をひとつ(o)用意する。case式でoの値によって演算を選んでいる。かけ算のときは、10の剰余をとることで0から9までの値にしている。最後に結果の値と「新しい乱数の種」をペアにしてかえす。試してみる。

> :reload
> randomIO :: IO Quiz
58 :-: 23
> randomIO :: IO Quiz
63 :+: 96
> randomIO :: IO Quiz
17 :-: 44
> randomIO :: IO Quiz
4 :*: 0

問題を文字列に変換する関数を定義する。

app/Main.hs
showQuiz :: Quiz -> String
showQuiz (a :+: b) = show a ++ " + " ++ show b ++ " = "
showQuiz (a :-: b) = show a ++ " - " ++ show b ++ " = "

それぞれの値について、それぞれの文字列を組み立てている。試してみる。

> :reload
> randomIO :: IO Quiz
9 :*: 6
> showQuiz it
"9 * 6 = "

答えを計算する関数を定義する。

app/Main.hs
answer :: Quiz -> Integer
answer (a :+: b) = a + b
answer (a :-: b) = a - b
answer (a :*: b) = a * b 

それぞれの計算をしている。試してみる。

> :reload
> randomIO :: IO Quiz
49 :+: 69
> answer it
118
> randomIO :: IO Quiz
88 :-: 96
> answer it
-8

問題を、ひとつ出題する関数を定義する。

app/Main.hs
quiz1 :: IO Bool
quiz1 = do
        q <- randomIO
        putStr $ showQuiz q
        hFlush stdout
        a <- getLine
        let     r = read a == answer q
        putStrLn $ if r then "正解!!" else "残念..."
        return r

randomIOでランダムな問題を作成し、それで変数qを束縛する。showQuizで文字列化して表示する。標準出力のバッファをフラッシュする。標準入力から1行入力する。「正答かどうか(read a == answer q)」で、変数rを束縛する。それぞれについて「正解!!」「残念...」を表示する。「正答かどうか」をつぎの入出力にわたす。試してみる。

> :reload
> quiz1
58 - 91 = -33
正解!!
True
> quiz1
83 - 9 = 123
残念...
False

問題を複数出題して、結果をかえす関数を定義する。

% vim app/Main.hs
quiz :: Integer -> Integer -> IO Integer
quiz n p
        | n < 1 = return p
        | otherwise = do
                r <- quiz1
                quiz (n - 1) (if r then p + 1 else p)

引数nは出題する問題数を示す。再帰的に呼び出されるたびに、1ずつ減少する。1より小さくなったら終了。引数pは得点。正解なら1増やし、まちがいなら、そのまま。nが1より小さくなった時点で、pの値がかえされる。試してみる。

> :reload
> quiz 3 0
46 - 90 = -44
正解!!
87 + 48 = 135
正解!!
6 * 0 = 123
残念...
2

入出力mainを定義する。

app/Main.hs
main :: IO ()
main = do
        p <- quiz 10 0
        putStrLn $ show p ++ "/10"

関数quizに引数として「10問」と「得点の初期値0」とをあたえて実行し、結果の得点で変数pを束縛する。そして、得点pを表示する。

% stack build
% stack exec calc-quiz-exe
1 * 0 = 0
正解!!
17 - 61 = -44
正解!!
34 - 43 = 123
残念...
(中略)
3 - 65 = -62
正解!!
8/10

Stackを使って、新規プロジェクトを作成した。簡単な計算問題を出題する例を作った。

6
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?