競プロ AOJ Haskell

オンラインジャッジを使って体で覚えるHaskell入門 パート3

Haskell完全初心者向けにHaskellを理論ではなく実際にAizu Online Judgeの問題を解きながら体で覚えようというHaskell入門シリーズの第3弾です
Feb. 7, 2020, 7:34 a.m.

この記事の対象読者と目的

Haskell完全初心者向けにHaskellを理論ではなく実際にAizu Online Judgeの問題を解きながら体で覚えようというHaskell入門シリーズの第3弾です。

前回までの内容はこちら。

オンラインジャッジを使って体で覚えるHaskell入門 パート2

目次

ITP1_4_A

問題

整数同士を割り算、剰余算した結果を整数と実数で出力する問題です。

https://onlinejudge.u-aizu.ac.jp/courses/lesson/2/ITP1/all/ITP1_4_A

解答

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import Text.Printf

main = getLine >>= putStrLn . solve . input

-- 入力
input :: String -> [Int]
input = map read . words

-- 解答
solve [a, b] = printf "%d %d %.8f" x y z
  where
    (x, y, z) = (div a b, mod a b, (fromIntegral a / fromIntegral b) :: Double)
solve xs = "Error parsing input"

整数ではなく実数が問題になっていることもあって、新しいことが3つあります。

1つ目は、整数同士の割り算の結果を実数で得るための方法です。
a / bでは切り捨てて整数になってしまいますが、fromIntegral a / framIntegral bとすると実数として計算できます。
深くは理解していませんが、C言語でいう(float)a/(float)bのようなものと理解しています。
:: Doubleという型をあえて書いていますが、これが無いと次のフォーマットでコンパイルエラーでした。

2つ目は、タプルと呼ばれるデータ構造です。
(x, y, z) = (div a b, mod a b, (fromIntegral a / fromIntegral b) :: Double)
今までは整数のみでしたので、同じ型同士でリストに入れられましたが、最後の1つだけ実数を含むのでリストは使えません。
異なる型同士をお手軽にまとめるためにタプルがあります。
パターンマッチで一度にx, y, zにそれぞれ整数、整数、実数を代入しています。

3つ目は、整数や実数の文字列フォーマットです。
実数はshowを使うと1e9のように指数表示されてしまい、この問題の場合には不正解となりました。
なのでフォーマット方法を指定する必要があります。
printf関数はまさにそのために使っていて、Text.Printfというライブラリをimportで取り込むと使えるようになります。
フォーマットの形式は他の言語と同じように見えます。

ITP1_4_B - 円の面積と円周

問題

円の半径rが実数で与えられて、円の面積と円周をそれぞれ実数で答える問題です。

https://onlinejudge.u-aizu.ac.jp/courses/lesson/2/ITP1/all/ITP1_4_B

解答

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import Text.Printf

main = getLine >>= putStrLn . solve . input

-- 入力
input :: String -> Double
input = read

-- 解答
solve r = unwords . (map $ printf "%.8f") $ [a, b]
  where
    a = r * r * pi
    b = 2 * pi * r

入力は型をDoubleに変えるとそのまま実数として読み込めました。便利ですね。

新しいものはpiで、円周率が実数で入っている定数です。

ITP1_4_C - 計算機

問題

簡単な四則演算をする計算機を作る問題です。

https://onlinejudge.u-aizu.ac.jp/courses/lesson/2/ITP1/all/ITP1_4_C

解答

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
main = readAndDo

readAndDo = do
  line <- getLine
  let (a, op, b) = input line
  if op == "?" then return ()
  else do
    print . solve op $ (a, b)
    readAndDo

-- 入力
input :: String -> (Int, String, Int)
input = decode . words
  where
    decode [a,b,c] = (readInt a, b, readInt c)
    readInt = read :: String -> Int

-- 解答
solve op (a, b)
  | op == "+" = a + b
  | op == "-" = a - b
  | op == "*" = a * b
  | op == "/" = a `div` b
  | otherwise = 0

内容はこれまでの知識で解ける問題です。
solveの引数のフォーマットを1つのタプル(op, a, b)にしようか、3つのop, a, bにしようか迷いました。
この場合はその中間のop (a, b)が良いという結論になりました。
タプルを使った部分の引数は部分適用が出来なくなるのでその分不便な関数になります。
一方で、引数が3つだと関数合成が出来ません。
引数をop (a, b)にしておけば、部分適用を使って例えば+をする関数はadder = solve "+"; adder (1, 4)のように使えます。
さらにadderは1引数関数なので関数合成も出来ます。

ITP1_4_D - 最小値、最大値、合計値

問題

要素数が可変の整数のリストが与えられるので、その中から最小値・最大値・合計値を求める問題です。
これも関数型だと楽しく解ける問題です。

https://onlinejudge.u-aizu.ac.jp/courses/lesson/2/ITP1/all/ITP1_4_D

解答

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
main = do
  _ <- getLine
  getLine >>= putStrLn . solve . input

-- 入力
input :: String -> [Int]
input = (map read) . words

-- 解答
solve xs = unwords . map show $ [foldr min infty xs, foldr max (-infty) xs, foldr (+) 0 xs]
  where infty = 1000001

新しい関数はfoldrで、リストを何らかの方法で集約する関数です。
関数型版のforループというイメージです。
最近の言語にはだいたいfoldreduceなどの名前で搭載されて、使い方も似ていますので、分かりにくい場合は自身の得意な言語での使い方を調べてみると良いでしょう。
foldrの引数は3つあります。
第一引数は2つの値から1つの値を計算する関数なら何でもOKです。
第二引数は初期値です。この初期値から開始してリスト先頭から先程の関数で値が1つになっていきます。
第三引数がリストです。
例えば、合計値のfoldr (+) 0 xsの例では、xs = [1, 2, 3, 4, 5]とすると、リストとその先頭に第二引数の0を付けて[0, 1, 2, 3, 4, 5]として先頭から2つずつ取っては足して1つに集約してリストの先頭に戻すことを繰り返すイメージです。

0  | 1 2 3 4 5
1  | 2 3 4 5
3  | 3 4 5
6  | 4 5
10 | 5
15 |

最小値や最大値の場合は足す代わりに小さい方、大きい方を選ぶだけです。