🐤

アルゴ式「条件とループ」をHaskellで解いて、その復習

2025/01/19に公開

Haskellに興味を持ち、「始めてみよう!」と思い立ったものの、初学者が理解できるくらい易しく、かつ実践的で参考になるコードを見つけるのは簡単ではありません。特に初心者が直感的に学べるお手本となるコードの例が不足しているように感じます。

そこで、初学者向けのプログラミング問題と解説を提供している「アルゴ式」の問題を実際に解いたコードを紹介し、それをもとにHaskell的な解法の考え方を、自分の復習も兼ねて記録していきたいと思います。

これにより、初学者による、初学者のための、おしゃれなHaskellコード集がここに爆誕します!ぜひ一緒に楽しみながらHaskellを学んでいきましょう。

今回は、アルゴ式のコーディングによる問題解決のロジック実装初級の2つ目の項目である、「条件とループ」の解き方のコード集です。

ここでの問題は、Haskellで競技プログラミング等の問題を解いていく時に必須となる基礎的な関数や考え方が凝縮しています。以前の入出力関連をベースに、今回の条件やループを解けるようになれば、とりあえず、色んな問題にチャレンジするベースはマスターしたようなものです!

条件の判定

Haskellでの条件処理はどうなっているのか、問題を通じて親しんで行きましょう。

パスワードの強度判定(1)

https://algo-method.com/tasks/d8e0821dd17735a1

本問は、入力された文字列が"password"と同じなら"dangerous"、そうでなければ"safe"と表示させるための処理を問う問題です。

Haskellでもこの様に基本的な条件分岐を行う場合に利用できるのがif式です。

if式を使った場合分け

Haskellでは、if式として利用します。基本的な構造は次の通りです。

ifの基本構造
if (真偽値) then (真の場合に返すもの) else (偽の場合に返すもの)

Haskellでのif式の注意点は、if式は、 であり、値を返すものであるということをしっかり把握しておきましょう。上記の構造をよく見てほしいのですが、then、elseの後ろは返すものを置きます。

では、if式を使った具体的なコードを見てみましょう。

let ans = if s == "password" then "dangerous" else "safe"

他の言語に慣れている場合、ifの前に=記号があるのが不自然に見えるかもしれません。しかし、先に話した通り、ifの構文は値を返す式なのです。

実際のコードの中では、上記の様に一旦ans等の変数名をつけて、後でプログラム内の別の何処かで利用するというよりも、次のコードの様にif式自体を関数の引数として渡してしまう様な利用の仕方の方が多いかもしれません。

main = do
    s <- getLine
    putStrLn $ if s == "a"
                 then "Yes"
                 else "No"

また、if式の戻り値がIOアクションであれば、do構文の下では、実行処理の流れの分岐の様に使うことも出来ます。しかし、実際には、この場合もIOアクションが返ってそれが実行されているに過ぎません。

main = do
    s <- getLine
    if s == "a"
        then putStrLn "Yes"
        else putStrLn "No"

なので、Haskellでifを使う場合には、「if式は真偽値をとってそれに応じた値を返す式」と把握しておくと混乱が無くなると思います。

では、このif式を使って本問を解いてみましょう。

解答例 ifを使う
main = do
    s <- getLine
    -- putStrLn (if s == "password" then "dangerous" else "safe") 
    putStrLn $ if s == "password" then "dangerous" else "safe"

bool関数と部分適用と関数合成

競プロの解答のパターンとしてなにかの条件判断をして最終的に、例えば"Yes"と"No"だったり、本問だと"dangerous"と"safe"の「どちらかを表示してください」という問題が良くみられます。

先に説明した通り、Haskellのifは式であり、これを行うのに十分な構文なのですが、同様の事を関数として実現できるのが、bool関数です。

bool関数を使ってコードを書くと、二者択一の処理問題を簡潔に表現できるようになります。

bool関数は、1つ目の引数に3つ目の引数が偽だった時に返す値、2つ目の引数に3つ目の引数が真だった時に返す値、3つ目の引数に真偽値をとります。

bool (3番目の引数が偽の場合に返す値) (3番目の引数が真の場合に返す値) 真偽値

具体的には以下のような感じになります。

ghci> :m Data.Bool
ghci> bool "No" "Yes" False
"No"

実際には、3つ目の引数の部分には真偽値そのものではなく、条件を判断して真偽値を戻す条件式を与えます。

上述の通り、bool関数自体は3つの引数を取りますが、初めの2つは、問題によって返す文字列が決まります。本問では、"dangerous"と"safe"です。そこで、boolに対して、初めの2つの引数を渡してあげると、1つの真偽値引数をとって、この問題専用の適切な文字列を返す関数が出来上がります。

ghci> :type bool "safe" "dangerous"
bool "safe" "dangerous" :: Bool -> String

==演算子で真偽を得る

さて、ここでこの関数が必要とするのは真偽値なので、本問の意図である「与えられた文字列が"password"と同じかどうか」を判別して、真偽を得る必要があります。

そして、この真偽値を得るためには==演算子を使い、具体的に次のように計算します。

ghci> "password" == "hoge"
False
ghci> "password" == "password"
True

当たり前とえいば当たり前と思うかもしれませんが、これは==演算子に2つの引数を渡して、その演算結果をTrue若しくはFalseとして得ています。なので、Haskellでは、セクションという書式を使うと共に部分適用して、一つの文字列を取って真偽値を返す関数に書き換えることが出来ます。
以下を見てください。

ghci> ("password" ==) "hoge"
False
ghci> :t ("password" ==)
("password" ==) :: String -> Bool

こうするだけで、与えられた文字列がpasswordならTrue、そうでなければFalseを返す関数を自分で簡単に作ることが出来るのです。セクションを使った小さな関数は凄く便利なので、使いこなせるようになりましょう。

部分適用関数を合成

さて、ここで作った2つの関数を.演算子を使って関数合成してしまいましょう。

ghci> :t bool "safe" "dangerous" . ("password" ==) 
bool "safe" "dangerous" . ("password" ==) :: String -> String

型を確認すると文字列を受け取って文字列を返す関数になっています。では、これを実際に使ってみましょう!カッコで関数部分を一纏めにします。

ghci> (bool "safe" "dangerous" . ("password" ==))  "hoge"
"safe"

passwordと比較してFalseになる場合は、boolの第1引数で指定している"safe"になり、問題解決の意図どおりに機能しています。

では更に関数を合成していきます。

本問のプログラムでは最終的に文字列を表示するputStrLn関数の引数として文字列を渡します。そして、上述の合成関数は、文字列を出力する関数になっています。そこで、これまでの合成関数をそのままputStrLn関数に合成してあげましょう。

ghci> putStrLn . bool "safe" "dangerous" . ("password" ==) $  "hoge"
safe

以上のように、関数の部分適用で一つの引数をとって一つの返り値を返すという単純な処理をする小さな関数を作り、これを合成で繋いで演算するのがHaskellの楽しさの一つです。

では、これらを利用して実際に問題を解答してみましょう。

解答例 bool関数や関数合成を使う
import Data.Bool
main = do
    s <- getLine
    putStrLn . bool "safe" "dangerous" . ("password" ==) $ s

尚、bool関数のあるあるトラブルとしては、Data.Boolをimportし忘れることと、第一引数が偽であることを忘れること(真と思ってしまう)です。その辺りに注意しておきましょう。

類似問題

問題 「パスワードの強度判定(2)」

与えられた文字列の長さに注目して条件判断する問題です。

Haskellは、文字列は文字のリストなので、リストの要素の数を求めるlength関数で文字列の長さ(文字の数)を求めることが出来ます。

ghci> length "hogehoge"
8
ghci> length [1..10]
10

次に、真偽値を得るセクション部分では、数値の比較を行う<演算子等にももちろんセクションが使えます。本問では6以下かどうかの真偽値を得るために上手に表現して関数化しましょう。

解答例 bool関数や関数合成を使う
import Data.Bool
main = do
    s <- getLine
    putStrLn . bool "safe" "dangerous" . (<=6) $ length s

問題 「正しい点数」

本問の判定基準は、0以上かつ100以下かどうかという複合的な条件になっています。そして、複合的な条件になると上述の様に簡単なセクションを書いて関数合成するという技が使いづらくなります。

真偽値を得る式自体は次のようなものです。

ghci> n = 10
ghci> n >= 0 && n <= 100
True
ghci> n = 101
ghci> n >= 0 && n <= 100
False

複数の比較演算子による真偽値結果を&&演算子で論理演算して、最終的な真偽値を得るわけです。

ですから解決策としては単純に真偽値を得る部分のみを式で表せばOKです。

ghci> n = 10
ghci> putStrLn . bool "invalid" "valid" $ n >= 0 && n <= 100
valid

若しくは、真偽値を返す関数として無名関数を書いてしまう方法があります。
無名関数は、名前のないその場限りの関数定義です。ラムダ関数ともいわれ、そのλという文字に似せて、\バックスラッシュからはじまる定義をします。

無名関数の書き方
\引数 -> 定義

上述の0以上かつ100以下で真を返す無名関数は次のようになります。

0以上かつ100以下で真を返す無名関数
\x -> x >= 0 && x <= 100

一つの数値を貰って、真偽値を返す関数です。定義ではxが仮の引数になっています。この無名関数の定義は、関数自体が返される点(つまり書いた部分に関数を渡すのであって、評価された結果を渡すのではない)に注意です。

これで関数が出来たわけですから、この関数を関数合成して先の式に組み込んであげると次のようになります。

ghci> n = 10
ghci> putStrLn . bool "invalid" "valid" . (\x -> x >=0 && x <= 100) $ n
valid

これらを組み合わせて解答を作ってみましょう

解答例 無名関数と関数合成を使う
import Data.Bool
main = do
    n <- readLn :: IO Int
    putStrLn . bool "invalid" "valid" . (\x -> x >= 0 && x <= 100) $ n

もちろん、if式で書くのもOKです。

解答例 if式で解く
import Data.Bool
main = do
    n <- readLn :: IO Int
    putStrLn (if n >=0 && n <= 100 then "valid" else "invalid")

問題 「成績判定(3)」

順番は前後しますが類題です。
この問題では、条件がやや複雑な計算を必要としますが、基本的には条件を満たすかどうかを真偽値として判定し、それに応じた文字列を返すことで解決できます。

解答例
import Data.Bool
main = do
    [n,m] <- map read . words <$> getLine :: IO [Int]
    putStrLn . bool "No" "Yes" $ n >= 70 && m >= 70 && m + n >= 160

成績判定(1)

https://algo-method.com/tasks/38bc1bb1fab9d052

条件判定による結果が3以上になる場合の処理をどの様にするかがテーマです。

if式で考える

本問では、与えられた点数がグレードA、B、Cのどれに当たるのかを判定してそれを表示します。まずは、以下のコードを見てみましょう。

let ans = if n >= 90 then "A" 
                     else if n >= 80 then "B" 
                                     else "C"

基本的に、if式のelseの中にif式を入れ子にしていくことで、2択以上の選択条件を表現することが出来ます。もちろん、thenの中も入れ子にすることが出来ます。但し、あまり複雑にすると見通しが悪くなるので、分かりやすく整理して、かつ、必ず、thenとelseが返す型も含めて対になっているかどうかを注意して確認しましょう。

if式での解答例

解答例 if式を入れ子にする
main = do
    n <- readLn :: IO Int
    let ans = if n >= 90 then "A" 
                         else if n >= 80 then "B" 
                                         else "C"
    putStrLn ans

場合分けのためにガードを使った関数定義を行う

haskellの関数定義には幾つかの便利な仕組みがあるのですが、その中のガードという仕組みを使って関数を定義すると、場合分けを行う処理が簡単に書ける事があります。

本問の様に3択以上の条件判断を行う場合は特に有効だと考えられます。

では早速、本問の処理をする関数、すなわち、数値を受け取ってその数値によってグレードの文字列を返す関数を考えます。関数名はsolveとう名前にしましょう。

ガードを使った関数定義の具体例
solve n 
    | n >= 90 = "A"
    | n >= 80 = "B"
    | otherwise = "C" 

この書式がどんな構造になっているかといえば、次の通りです。

ガードを使った関数定義の形
関数名 引数
    | 引数をつかった条件式(真偽値を返す) = この条件に該当した場合の関数定義
    | 引数をつかった条件式(真偽値を返す) = この条件に該当した場合の関数定義
    | otherwise = 上記条件に該当しなかった場合の関数定義

引数を使った条件式というのは、先の問題で見た通り引数として与えられたものを処理し、その結果を真偽値で返す式をここに書きます。そうして、その式が真の場合に、その右側の定義が適用されます。

ガードは、上から順に真偽値を調べていき、真の場合に右側の定義が適用されます。そして、ある定義が適用されたら、それ以降の行は見ません。

一方、真になる行が何処にもない場合、関数定義が出来ずエラーになります。そこで、通常は、最後の行の条件部分にはotherwiseを置いておきます。otherwiseは、いつでも真の値を返すので、この行で必ず右側の定義が適用されます。(逆にある行までで全てのパターンで真を返すような並びになっている場合、それ以降の行は絶対に到達しない行になることもあり得ます。)

ガードの書式は、他では見ることのない形なので、初学者の頃に書き方を間違えてしまうことが良くあります。そこで、はじめは特に以下のことを意識しましょう。

まず=記号の位置です。一行目の関数名と仮引数を定義する行の最後に=を書きがちですが、そこに=は書きません!

次に、ガード(棒(|)がガード)の行は、=記号の左が真偽値を返す式で、=記号の右が、関数の定義です。=記号の右にも左にも複雑な式や定義も書けますが、あまりたくさん書いてあると、何をしているのかが分からなくなるかもしれないので、=記号の位置を意識し、そして、その左右で何をしているのかを頭の中で整理して把握しておきましょう。

ガードを使った関数定義での解答例

このsolve関数を使って問題を解きます。

解答例
main = do
    n <- readLn :: IO Int
    putStrLn $ solve n

solve n 
    | n >= 90 = "A"
    | n >= 80 = "B"
    | otherwise = "C"

類似問題

順番が前後しますが、ガードを使うと上手に解ける問題です。

問題 「成績判定(4)」

問題通りにガードを上手く組み合わせると上手に解ける問題です。必要な要素を全て引数にしてsolve関数を作り、うまくガードで場合分けしましょう。

解答例
main = do
    [a,b] <- map read . words <$> getLine :: IO [Int]
    [x,y] <- map read . words <$> getLine :: IO [Int]
    putStrLn $ solve a b x y

solve a b x y
    | a < x = "No"
    | a + b >= y = "Yes"
    | otherwise = "No"

問題 「オリンピック」

本問では2020年や2021年がイレギュラーなパターンになっていますが、Haskellでは、これをパターンマッチで簡単に処理することが出来ます。また、ガードの条件式の部分には、複合的な式も書けます。

解答例
main = do
    n <- readLn
    putStrLn $ solve n

solve 2020 = "No"
solve 2021 = "Yes"
solve n 
    | n `mod` 4 == 0 = "Yes"
    | otherwise = "No"

問題 「うるう年判定」

問題文をよく読んで頭を整理して、上手にガードで条件を組み立てましょう。

解答例
main = do
    n <- readLn
    putStrLn $ solve n

solve n 
    | n `mod` 400 == 0 = "Yes"
    | n `mod` 100 == 0 = "No"
    | n `mod` 4 == 0 = "Yes"
    | otherwise = "No"

問題 「FizzBuzzの判定部分」

みんな大好きFizzBuzz問題

解答例
main = do
    n <- readLn
    putStrLn $ solve n

solve n 
    | n `mod` 3 == 0 && n `mod` 5 == 0 = "FizzBuzz"
    | n `mod` 3 == 0 = "Fizz"
    | n `mod` 5 == 0 = "Buzz"
    | otherwise = show n --ここ引っかかりがち

成績判定(2)

https://algo-method.com/tasks/247403c891a24d32

さて、先の問題「成績判定(1)」の入力は、プログラム内で数値に変換できる数字のみからなる文字列に限定されていました。ですから、問題「成績判定(1)」の解答コードに対して、アルファベットを入力すると、プログラムがクラッシュするはずです。

そこで、本問は通常は数値に応じたグレードを表示してくれますが、数値以外が入力されてもプログラムがクラッシュしないで、「"invalid"」と表示してくれるプログラムを作るのが目標です。このような処理は、一般的に 例外 処理と呼ばれています。そして、Haskellでは、このような例外の内、よくある例外処理については、それに対応する専用の基本的な関数が用意されています。

まず、紹介するのがRead型クラスの変換に関する例外処理用の関数であるreads関数です。

reads関数で例外処理

read関数を使うと失敗するかもしれないような処理の部分にreads関数を使います。最後にsが付いているのを見落とさずに!

まずはreads関数を実際に使ってみましょう。

ghci> reads "123" :: [(Int,String)]
[(123,"")]
ghci> reads "123abc" :: [(Int,String)]
[(123,"abc")]
ghci> reads "abc123" :: [(Int,String)]
[]

reads関数の結果の形がはじめは見慣れなくて凄く複雑な形に見えると思います。しかし、このタプルのリストという形に意味があり、それを理解するとあなたにもコレが納得の形に思えて来るはずです。さっそく詳しく見ていきましょう。

まず、reads関数は、ひとつの文字列を引数に取ります。この際、reads関数はその名前が示す通り、IntやDouble等、色々なものへの変換が可能な関数なので、原則的に変換したい型を型注釈で指定する必要があります。

そうするとreads関数は、渡された文字列を先頭から順番に確認して、変換するように指示された型として変換できる場合には、その変換できた内容をタプルの第1要素に、変換は行ったがそれ以降に何か変換できないものが続いていた場合には、タプルの第2要素に入れ、そのタプルを更にリストの中に入れて返します。一方、指示されたものとして変換することが出来なかった場合には、単に空のリストを返します。

以上をまとめると次のようになっています。

  1. 変換したい型の型注釈を付け、対象の文字列を引数にreads関数を呼ぶ
  2. 変換できた場合、タプルのリストが返され、その第1要素に変換後の値が入る
  3. 変換できなかった場合、空のリストが返る

reads関数の返り値の秘密

まず把握しておきたいのは、変換したい型として型注釈でIntを指示していますが、いずれの入力を行った場合においても、つまり、変換が失敗していた場合でも、エラーが出てプログラムが途中で中断される事はありませんでした。

その代わりに、変換の成功、失敗の情報は、先に見た結果出力の形として表現されているのです。

まずは、変換が成功したか否かは、結果のリストが空か否かで表現されています。
次に、変換が成功している場合、リストの要素であるタプルの第1要素が変換後の値になっています。

さて、人間からみると、このようなで表現されることは一見複雑そうに見えるのですが、Haskellという言語においては、データ構造の形をパターンで区別するパターンマッチという仕組みを使えるのでとても便利なのです。そして、Haskellでは case式という構文で、このパターンマッチを使った処理の場合分けを行うのが得意なのです。逆を言えば、reads関数は、パターンマッチで使いやすいように、あの奇妙なデータ構造の結果出力をしているのです。

パターンマッチで処理を分岐するcase式

では、実際どの様にcase式とreads関数をコードとして利用するかのパターンを紹介しましょう。尚、solve関数は点数としてのIntを受け取りグレードを表す文字列を返すような関数です。

let ans = case reads s :: [(Int,String)] of
            [] -> "invalid"
            [(a,_)] -> solve a

このcase式の構造は次のようになっています。

case式の構造
caseof
    パターンマッチ -> 定義
    パターンマッチ -> 定義

case式は、式の結果を得て、その結果のデータ構造が下に並ぶパターンマッチを上から順に見ていき、合致する行を見つけると、その定義をcase式の結果として返します。ここで、結果のデータ構造に合致するパターンマッチが無い場合はエラーになります。

あらためて、具体的なコードで見てみましょう。

let ans = case reads s :: [(Int,String)] of
            [] -> "invalid"
            [(a,_)] -> solve a

case式に与えられている式は reads s :: [(Int,String)] です。なので、入力の s に”abc”が入った場合、数値変換は失敗して、結果は空リストである [] が帰ります。すると、パターンマッチの [] の行にマッチして、"invalid"という文字列が返されます。

一方で、期待どおりの"10"が渡されると、reads関数は数値変換した結果として、[(10,"")]とうタプルのリストを返します。すると、case式の[(a,_)]のパターンマッチに合致し、更に、パターンマッチ内で仮引数として名付けられているaの部分が右の定義のaと置き換えられ、solve 10として計算されたグレードの文字列が返り、その文字列がcase式の結果として返される事になります。

さて、ここで気にかけておかなければならないことは、caseは式なので、パターンマッチの右で定義するものは、いずれの行も同じ型にならなければならないということです。

尚、case式の使い方ですが、ifの場合と同様で、上述のように名前に定義づけるだけでなく、関数の引数として渡すことも出来ます。

putStrLn $ case reads s :: [(Int,String)] of
    [] -> "invalid"
    [(a,_)] -> solve a

また、IOアクション自体を返す場合は、do構文の下に直接置くことも出来ます。

case reads s :: [(Int,String)] of
    [] -> putStrLn "invalid"
    [(a,_)] -> putStrLn $ solve a

では、reads関数を使った解答例です。

reads関数を使った解答例
main = do
    s <- getLine
    putStrLn $ case reads s :: [(Int,String)] of
                [] -> "invalid"
                [(a,_)] -> solve a

solve :: Int -> String
solve 100 = "S"
solve n
    | n >= 90 = "A"
    | n >= 80 = "B"
    | n >= 70 = "C"
    | n >= 60 = "D"
    | n >= 50 = "E"
    | otherwise = "F"

失敗するかもしれないを扱うMaybe型

さきのreads関数は、case式で扱うのに便利な形をしたデータ構造の結果を出力しました。しかし、私達が初めに戸惑った通り、説明を受けるまで、その形が成功失敗を判別するために使う形であると言うことを普通は知ることが出来ないでしょう。

そこで、Haskellでは、分かりやすく例外処理を扱うために、失敗と成功に関わる型であるMaybe型というものがあらかじめ用意されています。

そして、失敗するかもしれない読み込み処理に関し、このMaybe型で出力してくれるreadMaybe関数というものが用意されています。尚、この関数を使う場合は、Text.Readモジュールをインポートする必要があります。まずは、実際にreadMaybe関数の処理を見てみます。

ghci> :m Text.Read
ghci> readMaybe "123" :: Maybe Int
Just 123
ghci> readMaybe "abc" :: Maybe Int
Nothing

readMaby関数は、ひとつの文字列を引数として取ります。また、読み込みたい型を型注釈で示します。その結果、奇妙な出力がされていますが、これがMaybe型の見た目なのです。

先のreads関数では、読み込みに失敗すると空リストを返していました。しかし、Maybe型は失敗するとNothingというものを返します。一方、reads関数では、読み込みに成功するとその値がタプルとして帰ってきましたが、Maybe型では、Justというものに続いて値が書かれて帰ってきます。

Haskellには、このように見た目が不思議な書式が使われていますが、今の皆さんにとって、Maybe型が何をしたいのかの意味は分かると思います。そして、まずは、HaskellではMaybe型の値というものは、こういう書き方をするのだということを暗記してしまいましょう!

Maybe型の値は二つの事を表しています。

意味
Just a 成功した!取れた値はa Just 123 とか Just "hoge"
Nothing 失敗した! 変換できなかった Nothing

この変な形、実はパターンマッチでめちゃめちゃ役立つのです!

reads関数で行ったcase式の部分をreadMaybe関数で書き換えてみます。

putStrLn $ case readMaybe n :: Maybe Int of
    Notheng -> "invalid"
    Just a -> solve a

どうですか?
既にもう何が起こっているか。何がしたかったか。なんとなく把握できましたよね。

そして、なかなかにクールじゃありませんか?!

reads関数の時のパターンマッチは、していることが分かれば、意味を汲み取れますが、式を見ただけでは意味が分かりません。しかし、Maybe型を使えば、パターンマッチに書かれている事をみるだけで、何をしているかが分かりやすくなりました。

では、readMaybe関数を使った解答例です。

readMaybe関数を使った解答例
import Text.Read

main = do
    s <- getLine
    putStrLn $ case readMaybe s :: Maybe Int of
                Nothing -> "invalid"
                Just a -> solve a

solve :: Int -> String
solve 100 = "S"
solve n
    | n >= 90 = "A"
    | n >= 80 = "B"
    | n >= 70 = "C"
    | n >= 60 = "D"
    | n >= 50 = "E"
    | otherwise = "F"

Haskellでの例外を扱うcatch関数

Haskellにもアルゴ式の解説にかかれているtry-catch系の仕組みもあり、catch関数を利用することでこのパターンの解き方が出来ます。但し、catch関数は、IOアクションしか扱えないという制限があります。

競技プログラミング的には、前述のMaybe系の考え方の方が使い勝手が良いと思いますので、catch関数での解き方は、解答例のみを示しておきます。

catchを使った解答例
import Control.Exception

main = do
    s <- getLine
    printGrade s `catch` errorHandle

errorHandle :: SomeException -> IO ()
errorHandle _ = putStrLn "invalid"

printGrade :: String -> IO ()
printGrade = putStrLn . solve . read

solve :: Int -> String
solve 100 = "S"
solve n
    | n >= 90 = "A"
    | n >= 80 = "B"
    | n >= 70 = "C"
    | n >= 60 = "D"
    | n >= 50 = "E"
    | otherwise = "F"

ループ

Haskellにも何かを繰り返すという処理はありますが、PythonやC++のようにコードの一定部分を繰り返し実行するためのforループやwhileループといった構文は存在しません。

Haskellでは簡単な繰り返し作業ならば、以下のどちらかの方法を使うことで簡単に行えます。

  1. 値や処理(IOアクション)を任意回数複製する
  2. リストを全探索する形で各要素に対する処理を行う

ここで、繰り返し問題を解くために使う関数の概要を紹介しておきます。

同じものを好きなだけ複製するreplicate系

この関数は、同じものを任意回数繰り返したものを作ってくれる関数です。

同じものを好きなだけ複製する
replicate系関数 (繰り返す回数) (繰り返すもの)

普通の値も、IOアクションとしての表示処理も同じことを繰り返すならこの系統の関数で行えます。

リストに好きな関数を適用するmap系

この関数はリストの要素に好きな関数を順番に適用していくことが出来ます。

リストに関数を適用する
map系関数 (リストの要素に適用する関数) (リスト)

map系統以外でも、リストを操作する基本的な関数はhaskellに沢山あり、関数の使い方のパターンは、第一引数にリストに作用させる関数、第二引数にリストという形になっています。

この様に、「単純に複製する処理」、「リストに対して繰り返す処理」の2つが大きなパターンです。
そして、これは以下で具体的に見て行きますが、Haskellでは、普通の値とIOアクションで扱いが異なるので、繰り返すものが普通の値とIOアクションでそれ専用の関数を使うということを頭の片隅に置いておきましょう。

では、問題を解いていきましょう!

Hello Hello Hello

https://algo-method.com/tasks/b56617e7dcd6409d

この問題は、"Hello"という文字列を10個表示する問題です。

基本的に繰り返し処理を行う関数はreplicate系の関数の関数を使います。

そしてその中でも、今回は、表示するIOアクションを任意回数複製するので、replicateM_関数を利用します。尚、この関数は、Control.Monadモジュールが必要です。

replicateM_の使い方
replicateM_ (繰り返す回数) (IOアクションを返す関数)

この関数を利用して10回、putStrLn "Hello" を複製しましょう。

IOアクションを繰り返す 解答例
import Control.Monad
main = do
    replicateM_ 10 $ putStrLn "Hello"

さて、この関数名の最後に_(アンダーバー)が付いていることに着目してください。

このアンダーバーは、IOアクションを繰り返す関数に共通するもので、このアンダーバーが付いている関数とついていない関数が対になって存在します。アンダーバーが付いていないものは、IOアクションの結果がリストとして取れるものです。一方、ついているものは、IOアクションの結果が無く()(ユニット)が返されるものです。アンダーバーは、使わない値の名前付けやパターンマッチで使われる通り、いらないので捨ててしまうということを表現しているそうです。

具体的な場面でどちらを使うかについて、競技プログラミング的には、出力、入力の別で考えればよいです。putStrLn等の表示のIOアクションでは結果はいらないのでアンダーバー付き、一方、入力を受けるgetLine等では、IOアクションの結果で入力を受け取るのでアンダーバー無しを使うことになります。

次に関数名にMはがついていますが、これはモナドのMを表しています。一方、Mが付いた関数がある場合には、これの付かない関数が対としてあります。ここでは、replicate関数です。こちらの方が一般的な関数で、普通の値についての繰り返しを作成する場合にこの普通のreplicate関数を使います。

というわけで、Mの有り無し、アンダーバーの有り無しを纏めると次のようになります。

関数 意味 主に繰り返されるもの
replicateM IOアクションの結果をリストとして受け取る getLineやreadLnの入力
replicateM_ IOアクションの結果は無し printやputStrLnの出力
replicate 普通の値の複製してリストで返す 1や"a"や[2,3]のリストも可

頭の中でパターンを整理しておきましょう。

1, 2, ..., 100

https://algo-method.com/tasks/e1e5b1533e18dcbe

次は、1から100までの整数を表示する問題です。

この問題は同じことを繰り返すのでは無く、1から100という与えられた100個の事について、それぞれの処理を順次行う問題といえるので、リストを使った順次処理を行うmap系の関数を利用して繰り返し処理を行いましょう。

ここでは、問題を解いていくために、1から100の数字を表現する何らかのデータが必要となり、Python等ではこれをループ構文のなかでインデックス変数を確保し、ループごとに1増やして利用する等の作業を行います。

しかし、Haskellの場合、ループに頼らなくても、これをリストとして簡単に準備出来ます。

例えば、1から5までのリストは、次の様の簡単に作ることが出来ます。

ghci> [1..5]
[1,2,3,4,5]

さて、ここで個別の要素に対して行う処理は、表示です。リストの要素は数値なので、print関数を使うことで、個別の要素を表示することが出来ます。そして、このprint関数はIOアクションを返します。

以上の点を踏まえて、map系の関数のうち、リストを処理して表示のIOアクションを繰り返す、mapM_関数を利用することに決めます。mapM_関数を使って、具体的に次のように書くことが出来ます。

ghci> mapM_ print [1..5]
1
2
3
4
5

mapM_を含む、map系の関数は2つの引数を取ります。第1引数に引数を一つ取る関数で、第2引数にリストです。

mapM_関数の引数のパターン
mapM_ (引数を一つ取ってIOアクションを返す(IOアクションの結果は捨てる)) リスト

map系関数も、replicate系と同じで、Mの有無、アンダーバーの有無に応じて以下の様になっています。

関数 返すもの リストを処理する関数の例
map 適用した関数の結果のリスト 純粋関数
mapM_ IOアクション(副作用のみで結果のリストは無い) printやputStrLnと合成
mapM IOアクションの中身の結果のリスト getLine等

では、実際にmapM_関数で解答コードを書いてみましょう。

解答例
main = do
    mapM_ print [1..100]

2, 4, …, 100

https://algo-method.com/tasks/a11d8edd3e69d887

次の問題は、1から100の数字について、それをそれぞれ2倍したものを表示する問題です。

ここで、まず、リストの要素を2倍するコードを考えてみます。先ほどは、繰り返し処理をして出来上がるものがIOアクションだったので、mapM_を使いましたが、単に処理したものをリストとして返すだけの場合には、map関数を使います。

リストの各要素を2倍する関数は、*演算子をセクションにして2を与えることで関数できます。

以下が1から5までについての処理を行うコードです。

ghci> map (*2) [1..5]
[2,4,6,8,10]

map関数を使うと結果としてリストが返ることもあらためて確認できます。

では、ここに1から5までですが、2倍した数値のリストが出来たわけですからこれを引数にして、先の問題の要領でmapM_関数を使用すれば、問題を解くことが出来そうです。

ghci> mapM_ print (map (*2) [1..5])
2
4
6
8
10

さて、ここで関数合成について考えてみます。何か与えられた数値に対して、2倍してから表示を行うときどんなふうにコードを書くか考えてみましょう。

次のようにprintと、セクションを使った2倍する関数を合成して、コードを書くことが出来ます。

ghci> (print . (*2)) 1
2

もちろん、map系に渡す第1関数も合成関数を使えます。但し、最終的にその合成関数が返す結果が通常の値のリストなのか、IOアクションなのかによってどのmap系の関数で処理するかを考えます。最終的に表示する場合にはmapM_でOKです。

ghci> mapM_ (print . (*2)) [1..5]
2
4
6
8
10

map系の第一引数を合成関数で渡す場合、必ず、合成関数の部分をしっかりとカッコで括ってあげましょう。

解答例

解答例
main = do
    mapM_ (print . (*2)) [1..100]

試合の成績

https://algo-method.com/tasks/a59f79e45e0b19dc

本問についての、アルゴ式の解説を見ると、ループ的な解き方の方針は、大まかには次のようなものです。forループのインデックス変数を用いて、各ループで各番目の文字がoと同じかどうかを調べて同じならばカウントを一つ上げるという操作を繰り返し、最終的なカウントを表示するというものです。

では、Haskellではどのような方針になるか考えてみましょう。まずは、問題から文字列、すなわち、文字のリストが与えられます。ですから、本問もリストの処理を順次していく問題として考察しましょう。

さて、今のところリストを順次簡単な関数で処理していくことができる関数としてmap系を紹介しましたが、単純に各要素を処理してその結果のリストを得るだけでけのパターンでは問題をこなすことは難しそうです。

一方で、Haskellにはこのような問題を解くのに適した別の基本関数が用意されています。

今回の問題のテーマの課題をあらためてまとめると、あるリストの中から条件に合致するものは幾つあるのか?という問題になるのですが、Haskellには、あるリストから条件に合致する要素のみを抜き出すfilter関数というものがあります。本問ではまず、この関数を利用して問題を解いていきましょう。

リストから条件に合致する要素を抜き出すfilter関数

まずは、filter関数を使った具体的なコードを見てみましょう。

ghci> filter (=='o') "oxxox"
"oo"

filter関数の使い方は以下の通りです。

filter関数の引数
filter (引数を一つ取とって真偽値を返す関数) (リスト)

filter関数は、第1引数に一つの値を取って真偽値を返す関数を取ります。ここでは、セクションを利用して==演算子で、文字'o'と等しいかどうかの真偽値を返す関数を与えています。そして、第2引数にリストを取り、ここでは、文字のリストである文字列を与えています。

そうすると、結果として受け取ったリストのうち、第1引数の関数を適用して真となるものだけを残したリストを返します。ここでは、'o'と等しい要素である文字のリストとして、文字列が帰っています。

普通の数値のリストに適用した例も見ておきましょう。

ghci> filter (==1) [1,2,3,1,2,1]
[1,1,1]

このように、filter関数を使うと、自分で定義した条件設定に合致する要素だけを取り出したリストを作成することが出来るようになります。また、filter関数に与える引数は、map系と同じように、第一引数に処理を行う関数を、第二引数にリストを渡すパターンになっているので覚えやすいと思います。

カウントする

アルゴ式の解説のループ処理では、ループ内で同じかどうかを判断した後、同じならば、その個数を数えるための変数カウンタを増加させる処理等を行っているはずです。ループに慣れているとこのような何かを数えるためには、こんな処理が必要に思いがちですが、Haskellではそんな作業はいりません。

Haskellでは、リストの要素数はlength関数を使って簡単に数えることが出来ます。

つまり、あるリストにfilter関数を適用した結果のリストは、条件に合致した要素のみが残っているので、その要素数を数えるわけです。

先の式にlength関数を付け足しましょう。

ghci> length (filter (=='o') "oxxox")
2

解答例

以上を組み合わせれば、ループではなくてリストの処理を行う解決法で、本問を解決できます。

解答例
main = do
    s <- getLine
    print . length $ filter (=='o') s

ある関数とリストをとって処理する関数の代表がmap系の関数です。しかし、それ以外にもこのfilter関数の様に、同じような使い勝手である働きに特化している便利な処理関数が幾つかあります。解き方に適したものが出てくるたびにひとつづつ覚えていきましょう。

円周率の K 番目

https://algo-method.com/tasks/406f6932f2c7c144/editorial

Haskellでもリストに対して!!演算子を使うことでインデックス(初めから何番目の要素か)を指定して用を取り出す事が出来ます。インデックスは0スタートです。

ghci> [1,2,3] !! 0
1
ghci> [1,2,3] !! 1
2

この演算子を使えば本問は解けます。

解答例
main = do
    n <- readLn
    print $ [3,1,4,1,5,9,2,6,5,3] !! n

総和

https://algo-method.com/tasks/2584a3191b8e097d

もちろん、リストの要素の合計については、sum関数を利用することが出来ます。

しかし、アルゴ式では本問を「繰り返しを利用して」となっているので、Haskellではリストの処理として問題にアプローチします。

実は、以前に紹介した「foldlで畳み込め!」のfoldlを活用することで、リストとして処理できます。

第1引数の関数として+演算子をセクションで2つの引数をとる関数とし、第2引数の初期値は0にします。最後は問題で指定された整数のリストを与えることで問題を解くことが出来ます。

解答例
main = do
    print $ foldl (+) 0 [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]

このfold系の畳み込み関数は、map系の関数と同じように、リストを引数にとってそれを順次処理するという仕組み自体は同じですが、引数の性質が少し異なり、第一引数の関数は、引数を2つとる関数を与えなくてはいけません。

また、fold系の畳み込み関数はaccと呼ばれる途中経過を引き継ぐ内部変数を使えるので、少し複雑な処理を行うことが可能になります。

ここで、先に行った問題「試合の成績」を、fold系の畳み込みで解く方法を考察してみましょう。

問題「試合の成績」を畳み込みで解く

もともと、Python等のループを使った解き方の方針は、ループ毎に各文字が同じかどうかを調べて同じならカウンタ変数を一つ上げるというものでした。

つまり、本問に則せば、foldl関数の第一引数として渡す関数は、与えられた文字が'o'ならば、accに1を足し、そうでなければ、acc自体を返す関数を作ると良いでしょう。

この関数を実際に定義してみましょう。foldl関数は左側から畳み込まれるので、accは第一引数になります。

foldlに渡すための関数の定義
f :: Int -> char -> Int
f acc x
    | x == 'o' = acc + 1
    | othewise = acc

若しくは、条件付けが2択なので、無名関数でもまだシンプルに書けるかもしれません。

無名関数
(\acc x-> if x == 'o' then acc + 1 else acc) 

以上の関数をfoldlに与えて処理してみましょう。

「試合の成績」をfoldlで解く
main = do
    xs <- getLine
    print $ foldl f 0 xs
f acc x
    | x == 'o' = acc + 1
    | othewise = acc
「試合の成績」をfoldlで解く(無名関数)
main = do
    xs <- getLine
    print $ foldl (\acc x -> if x == 'o' then acc + 1 else acc) 0 xs

この様にfold系の畳み込み関数は、リストを順次処理していく途中に何かメモしておく領域が必要な処理に最適な仕組みなのです。普段はなんでも考えなしにとりあえずループで行っている処理の中で、これはどういう処理なのかを頭で整理すると、意外と畳み込み処理がなされているものもあるかもしれません。

ループを用いた標準入力

まず、Haskellにおいて競技プログラミング的な1行に複数のデータを空白区切りで渡される入力については、定型処理になるので繰り返し処理という感覚は在りません。

何を与えられてもほぼ以下の式で対応します。

xs <- map read . words <$> getLine :: IO [Int]

実際には、今回map関数を紹介したので、リスト内の各文字列要素を繰り返しread関数で変換しているというのは分かると思いますが、実際に問題を解くときには頻出の式なので、パターンとして覚えておいて大丈夫です。(<$>演算子も含めて概念的にはやや高度なので)

絶対的に気を付ける点としては、words関数を適用した時点で文字列のリストになるので、数値変換がいらない場合は、次の式になることを理解しておくことです。

xs <- words <$> getLine

更に、リストとして取り込んでいますが、多くの問題では、一行で渡されるデータの数が決まっています。その場合は、実際のデータ数と同じ構造のリストをパターンマッチで書き、名前も付けてしまうのがリストの入力受け取りのやり方です。

その行でaとbの2つの値を受け取るのが決まっているならパターンマッチ
[a,b] <- map read . words <$> getLine :: IO [Int]

これまでの類題

というわけで、一行での複数データ受け取りに関する問題は、ほぼ、アルゴ式の「標準入出力」の類題です。今までの知識でサクッと解いていきましょう。

問題 「総和」

入力をリストとして受け取れば、後は、上記で解いた「問題 「総和」」と同じです。

なお、入力の1行目で次に与えられるデータの数が与えられますが、その数が無くてもHaskellの場合、入力を適切に受け取ることが出来ます。この場合、処理上必ず一行目を受け取るコードが必要ですが、受け取った内容を利用しません。この様な場合、受けっ取った値が必要ないことを示すために_(アンダーバー)という名前という名前を使います。

_ <- getLine -- この入力は利用しない

前回の「問題 総和」との違いは、入力となる数値を標準入力から受け取るところです。合計する処理は、決まった入力でも、任意の入力でも変わりはありません。

解答例
main = do
    _ <- getLine
    xs <- map read . words <$> getLine
    print $ foldl (+) 0 xs

問題 「総積」

リストに対する処理が先の足し算から掛け算に変わります。初期値に注意しましょう。

解答例
main = do
    _ <- getLine
    xs <- map read . words <$> getLine
    print $ foldl (*) 1 xs

問題「3 の倍数」

まず、あるリストの要素から3の倍数である要素をfilter関数でフィルタリングしてみましょう。そのために、3の倍数の場合に真を返す様な関数を無名関数として自作して、filter関数の第1引数に渡してみましょう。

答えは、各要素を各行に表示することです。リストに対してIOアクション結果として作成する関数を適用するmapM_の出番です。

解答例
main = do
    _ <- getLine
    xs <- map read . words <$> getLine
    mapM_ print $ filter (\x -> x `mod` 3 == 0) xs

問題 「一の位」

数値の1の位の求め方については 問題「一の位比較」で考察したことがあります。

数値を10で割って余りが1の位になることを利用して解いてみましょう。

今回はこの処理をリストに対して行います。更に、出力は各行毎に行うので表示させるIOアクションを適用毎に出力するmapM_の出番です。

解答例
main = do
    _ <- getLine
    xs <- map read . words <$> getLine
    mapM_ (print . (\x -> x `mod` 10)) xs

問題 K 番目の値

問題の順番が前後しますが、類似問題です。
これは、ループは関係なく、リストの任意番目の要素を取り出す類題です。先にやった!!演算子をリストに適用すればOKです。

解答例
main = do
    [_,n] <- map read . words <$> getLine
    xs <- map read . words <$> getLine :: IO [Int] --型注釈が必要
    print $ xs !! n

問題 プラスの個数

問題の順番が前後しますが、類似問題です。
本問はA,Bの2つのデータを受け取りますが、それぞれのリストを処理して得た結果を最終的に比べるという処理を行えばOKです。

リストに対する処理は、正の整数の個数については、filterlength関数を組み合わせて結果を求めましょう。更に、その結果を比較する処理は、if式を使うことも出来ますが、結果は3択になるので、ガードを使った関数定義をするのもお勧めです。

解答例
main = do
    _ <- getLine -- 個数は必要無い
    a <- length . filter (>0) . map read . words <$> getLine
    b <- length . filter (>0) . map read . words <$> getLine
    putStrLn $ solve a b

solve a b 
    | a == b = "same"
    | a > b = "A"
    | otherwise = "B"

文字列の連結

https://algo-method.com/tasks/59

いよいよ「ループを用いた標準入力」のテーマに則した問題です。

本問では、入力データの行数が初めに指示され、その任意の行数の指示に従ってデータを受け取ることになります。

複数行の標準入出力に関しては、形式的にまずreplicateM系の繰り返しで対応します。そのうえで、入力なので、最後にアンダーバーの付かないreplicateMを利用して入力IOアクションを繰り返し、アクションの結果をリストとして受け取るというパターンになります。(Control.Monadモジュールのインポートも忘れずに)

競技プログラミングの入力に関してはパターンなので、どういったパターンで処理するかを問題をこなしていって考えることなく対応できるようになりましょう。

では、本問の入力形式に従って、入力を文字列のリストとして受け取るコードを例示します。
一行目で何行のデータが来るのかを受け取った後、その回数だけ replicateM 関数を使ってIOアクションを複製します。

import Control.Monad
main = do
    n <- readLn
    xs <- replicateM n getLine
    print xs

解答例

複数行での入力を処理して、文字列のリストさえ得てしまえば、後は自分の思いついた解法で解いていけると思います。

そのなかでも、問題が、全ての文字列を連結した場合の長さなのでその通りやってみることもできます。

ここで利用できるのがconcat関数です。concat関数は、与えられたリストの中にあるリストを全てつなげて一つにする関数です。そして、文字列は文字のリストであり、文字列のリストは、文字のリストのリストになっているので、文字列のリストにconcatを適用すると、リストの要素である文字列がつながった文字列が返るようになっているので、文字列の連結に用いられます

ghci> concat ["neko","inu"]
"nekoinu"
解答例1
import Control.Monad
main = do
    n <- readLn
    xs <- replicateM n getLine
    print . length . concat $ xs

もちろん、mapやlengthやsumを組み合わせて、今までの知識を総動員したってかまいません。

解答例2
import Control.Monad
main = do
    n <- readLn
    xs <- replicateM n getLine
    print . sum $ map length  xs

なんにしても、標準入力から複数行で入力を得る場合にはreplicateMで対応する事が大事です。

類似問題

任意に指定された行数の入力を受け取る問題です。

問題 「頭文字の連結」

複数の文字列をリストとして受け取ることから初めましょう。

文字列は文字のリストです。ですから、「頭文字を取る」というのは、リストの1つ目の要素を取り出すということですからhead関数を利用していましょう。

また、取り出されたものは文字です。本問では、この操作をリスト内の各文字列に適用して、取り出された文字はリストとして格納されます。つまり、文字のリストです。それはどういうことかを頭の中で整理しましょう。

解答例
import Control.Monad
main = do
    n <- readLn
    xs <- replicateM n getLine
    putStrLn $ map head xs

合計金額

https://algo-method.com/tasks/fd64102ee302b92d

任意の複数行の読み込みで、かつ、各行に複数のデータがある場合、読み込まれるデータはリストのリストという形になります。

競技プログラミングとしては、このような入力も多く、受け取りはパターンとして暗記しておけばOKです。一般的なパターンは以下のようなもの。Control.Monadモジュールのインポートは忘れずに。

n <- readLn
xs <- replicateM n  (map read . words <$> getLine :: IO [Int])

本問では、各行はA,Bの2つの値になっているので、リストのリストとしてをmapで処理する場合、その第1引数に置く関数には、2つの値を持つリストとしてのパターンマッチとして書くことが出来ます。
具体的には、本問においては、個数と単価から各アイテム毎の合計金額のリストは以下のような式で求められます。

map (\[a,b] -> a * b) xs

更に、本問においては、全てのアイテム代金の合計が必要になるので、sum関数を利用しましょう。

解答例
import Control.Monad

main = do
    n <- readLn
    -- xs <- replicateM n (map read . words <$> getLine :: IO [Int])
    xs <- replicateM n $ map read . words <$> getLine
    print . sum $ map (\[a,b] -> a*b) xs

添字

https://algo-method.com/tasks/390298dbad76c8cb

本問は、任意の複数行入力と複数行出力が組み合わさった問題です。

入力の1行目は、問題の対象となるリストの要素数と、その後に入力されるクエリ(問い合わせ)の行数の2要素が渡されます。通常の数値リストの受け取りで受け取りましょう。

-- パターンマッチが便利
[_,q] <- map read . words <$> getLine

入力の2行目は対象となるリストです。これも通常の数値リストの受け取り方でOKです。

3行目以降は、1行目の2番目の要素で記された数値だけ行数が並びます。replicateM関数を利用して入力のIOアクションを複数作り内容のリストを<-で取り出すことで、クエリの全てをリストで受け取ることが出来ます。

-- qは一行目で受け取った、クエリの行数
k <- replicateM q readLn

本問は、各行に解答を表示するため、処理の結果としてIOアクションを返すmapM_関数を利用します。各処理については、関数合成した関数をmapM_の第一引数に渡せばOK。リストの各要素にアクセスするには、!!演算子が使えますので、この演算子もセクションで関数化して、引数を一つ取る関数にすることが出来ます。

ghci> print . ([1..5]!!) $ 0
1

以上を組み合わせて本問を解いてみましょう。

解答例
import Control.Monad

main = do
    [_,q] <- map read . words <$> getLine
    a <- map read . words <$> getLine :: IO [Int]
    k <- replicateM q readLn
    mapM_ (print . (a !!))  k

練習問題

ここまでの知識を組み合わせて、各問題に挑んでいきましょう。

FizzBuzz 問題

https://algo-method.com/tasks/225

先にあった問題です。個別の数字について判断するだけではなくて、1からnの全てについて処理して表示する必要があります。

まず、解答の形式が結果を各行毎に表示する事から、mapM_関数を利用しましょう。

処理の部分については、数値としての入力に対するFizzBuzzルールに従って変換した文字列を返す関数を別途、定義してみましょう。

そのFizzBuzzを返す関数と表示を行うputStrLn関数を合成させたものををmapM_の第1引数の関数として、入力のリストを処理する流れが自然です。

解答例
import Control.Monad

main = do
    n <- readLn
    mapM_ (putStrLn . solve) [1..n]

solve n
    | n `mod` 3 == 0 && n `mod` 5 == 0 = "FizzBuzz"
    | n `mod` 3 == 0 = "Fizz"
    | n `mod` 5 == 0 = "Buzz"
    | otherwise = show n

成績判定(5)

https://algo-method.com/tasks/b944f80497ab1eae

本問題は、複数のテーマがあります。

  • 複数の数値から平均点を求める
  • データにインデックスを付ける
  • 平均以上のデータを選ぶ
  • 選ばれたデータのインデックスを表示する

平均値を求める

さて、まず、平均値を求める必要がありますが、問題の注意書きでは「平均点が整数とならない場合があります」となっています。この場合、数値の演算をする場合にDoubleを使う必要が出てきます。

しかし、ここで競技プログラミング的テクニックを使うことが出来ます。

まず、問題の制限を見る限り、各人の点数は整数です。また、問題は平均点以上の人を調べる事になっています。そうだとすれば、平均点が整数でない場合でも、それを切り上げた整数にさえすれば、同じ条件で、平均点以上の人を探すことが出来ます。

割り算の答えを切り上げで整数にする技

これは、競技プログラミングで良く出てくる処理です。割られる数に割る数から1を引いた数を足してから割った商になります。

\dfrac{割られる数 + 割る数 - 1}{割る数}

aをnで割る場合、以下のような計算で整数商を求めればよいです。

-- 切り上げられた整数平均値
intAvg = (sum a + n - 1) `div` n

zip関数を使ってインデックスを付ける

さて、次に、本問で注目すべき点は、解答すべき答えが出席番号である点です。

この出席番号は、forループ的な解き方をする場合、処理の構造上使われるインデックス変数(iなどと名付けられるループカウンタも同時にこなす)と内容が一致するので、このインデックス変数に着目して解答することになります。しかし、Haskellでリスト処理を行う場合、ループという処理構造が無く、当然、ループカウンタやインデックス変数もありません。

そこで、本問のように、「インデックスが無くて困ったなぁ。。」と思うような場面があるならば、迷わずインデックスを付けてあげましょう!

Haskellで簡単にインデックスを付ける方法は、zip関数を使ってタプルのリストにする方法です。

zip関数は、二つのリストを引数にとって、それを一つのタプルのリストにして返します。具体的なコードを見てみましょう。まずは、簡単な例を見てみましょう。

ghci> zip [1,2,3] ["inu", "neko", "tori"]
[(1,"inu"),(2,"neko"),(3,"tori")]

インデックスを付けたタプルのリストにすると、その要素が何番目の要素であるのかすぐに分かるようになります。

では、本問と同様のデータにインデックスを付けます。ここで、zip関数は、受け取った2つのリストのうち、短い方のリストの長さの結果リストを出力します。なので、インデックスを付加する場合、インデックス用のリストは開始番号だけを指定した無限数列のリストを渡せばOKです。

ghci> n = 4
ghci> a = [4,8,3,9]
ghci> zip [0..] a
[(0,4),(1,8),(2,3),(3,9)]

zip関数は、ジッパーで閉じるような仕組みになっていることから、zip関数と言われているそうです。覚えやすいですよね!

filter関数でフィルタリング

本問では、平均点以上の人を抜き出す処理を行いますが、条件に合致するリストの要素を抜き出すので、filter関数を利用します。

フィルタリングする関数

filter関数の第1引数にはリストの要素を受け取って、真偽値を返す関数を準備します。

まずは、受け取る要素の型を考察すると、(インデックス,点数)の構造になっています。そして、点数の部分について、平均値以上ならば真、そうでなければ偽を返す関数を考えれば良いことになります。まずは、データの構造に着目したパターンマッチでこの問題を解決するsolve関数を定義してみましょう。

solve avg (_,v) = v >= avg 

引数にタプルを受ける関数を定義する場合、パターンマッチを使うことで、簡単にタプルの要素を定義の式に組み込むことが出来ます。

この選別関数をfilter関数に渡せば上手く機能するはずです。

あらためてghci上で試してみましょう。

ghci> n = 4
ghci> a = [4,8,3,9]
ghci> intAvg = (sum a + n - 1) `div` n 
ghci> solve avg (_,v) = v >= avg
ghci> filter (solve intAvg) $ zip [0..] a
[(1,8),(3,9)]

それほど複雑な関数ではないので、無名関数としてfilter関数の引数に与えることも出来るでしょう。

ghci> filter (\(_,v) -> v >= intAvg) $ zip [0..] a
[(1,8),(3,9)]

また、要素が2つのタプルについては、1番目の要素を取り出すfst関数、2番目の要素を取り出すsnd関数があります。比較対象になる点数は2番目の要素なので、snd関数でとりだせます。これを利用して合成関数を作って、filter関数の引数にそのまま渡すコードもそれほど冗長ではありません。

ghci> filter ((>=intAvg) . snd) $ zip [0..] a
[(1,8),(3,9)]

この様に、リストの要素に適用する関数は、別途定義しても良いですし、無名関数や、小さな関数の合成関数を直接引数として渡すことも出来ます。競技プログラミングにおいては、その時に応じて、式の意味がわかりやすい書き方にするのが良いと思います。

解答を表示

解答はインデックスを改行区切りなので、複数データを表示するmapM_関数で書きましょう。
ここまででの処理が終わって、渡されているのはタプルのリストです。タプルからインデックスを取り出して表示するための処理関数は次のものが簡単です。

ghci> print . fst $ (1,8)
1

この関数をmapM_に与えて、リストを処理しましょう。

ghci> anslist = filter ((>=intAvg) . snd) $ zip [0..] a
ghci> mapM_ (print . fst) anslist 
1
3

以上のように、タプルのリストは見た目的には複雑な処理が必要に思えますが、fstsnd関数を利用することで普通にmapfilterで処理することが出来ます。

解答例

以上のコードを組み合わせて解答コードを書いてみましょう。

解答例
main = do
    n <- readLn :: IO Int 
    a <- map read . words <$> getLine
    let intAvg = (sum a + n - 1) `div` n
    mapM_ (print . fst) . filter ((>=intAvg) . snd) $ zip [0..] a

成績判定(6)

https://algo-method.com/tasks/69f3b6db966d9cdb

今回の問題はいくつもの課題が含まれていているので、ひとつづつ何を処理するのかを意識して解いていきましょう。

この問題の処理を纏めると次のようになります。

  1. カテゴリ(級)と点数のデータを受け取る
  2. カテゴリ(級)に応じた点数の合否判定
  3. 以上の処理をn人分行う
  4. 合格を数えて結果を表示

replicateMで入力を受ける

n人分の処理を行うのでreplicateMで入力に関するIOアクション処理を繰り返し、その結果をリストとして受け取るようにします(アンダーバーの無い方を使う)。一行から2つの数値要素を受け取るため、一般的な次のコードを利用しましょう。

replicate n (map read . words <$> getLine)

合否判定関数を定義

問題のグレード(級)と点数の二つの要素から合否が決定する関数を定義します。

関数定義をする時の引数の型を考える

さて、関数の内容を考察する前に、関数が受け取る引数のデータ構造を考察してみましょう。

ここで、1行に2個の数値が書かれたものは、先に述べた通り次の一般的なようなコードで取り込むこととします。

map read . words <$> getLine :: IO [Int]

このコードの返り値は数値の要素を2個持つリストです。ですので、それをそのまま受けることが出来るように、引数のデータ構造もこれを前提にします。

では、処理をする関数をgradeと名付けて定義してみましょう。

grade :: [Int] -> Bool
grade [1,n] = n >= 90
grade [2,n] = n >= 80
grade [3,n] = n >= 70
grade [4,n] = n >= 70
grade [5,n] = n >= 70

Haskellの関数定義は、場合分けを分かりやすく表現できます。その時のコツは、以下の二つの視点から考察します。

  1. パターンマッチで特定の構造の場合を定義
  2. その構造化でガードを使って場合分けを定義

もちろん、ガードの条件で要素が何かと一致するか判別するような書き方もできます。本件においては、グレード(級)の部分です。ガードで書くと次のようになるかもしれません。

grade :: [Int] -> Bool
grade [a,n] 
    | a == 1 = n >= 90
    | a == 2 = n >= 80
    | a == 3 = n >= 70
    | a == 4 = n >= 70
    | a == 5 = n >= 70

本問題の場合は、まだ、場合分けが単純なのですが、複雑になる場合、まず、構造で場合分けして、その構造の下にガードをぶら下げる書き方が読みやすいので、何かの一致で場合分けを表現するときは、ガードでやるよりもパターンマッチで書く方が良さそうに思います。

また、どちらにせよ=演算子の右部分n >= 90等の式は、場合分けをしているのではなくて、真偽値を求める演算をしていることに注意です。

合否処理を入力処理のreplicateMに組み込む

replicateM関数の引数として渡してしまうパターンです。grade関数を定義したので、合成関数にするとシンプルに式を書くことが出来ます。

    a <- replicateM n (grade . map read . words <$> getLine)

合格者を数える

上記のコードは合否を表したBool型のリストです。リストから必要なものを数えるHaskellアルゴリズムは、リストをfilter関数で処理してlength関数で数えます。
filter関数は、引数として真偽値を返す関数が必要ですが、処理する対象が真偽値である場合、その値をそのまま返す関数があれば良いことになります。そこで、与えられた値をそのまま返すid関数を利用することが出来ます。

以上から、処理されたリストがaとすると次のコードで合格者数が分かります。

print . length $ filter id a

解答例

以上のコードを組み合わせて解答してみましょう

解答例
import Control.Monad

main = do
    n <- readLn
    ans <- replicateM n $ grade . map read . words <$> getLine
    print . length $ filter id ans 

grade :: [Int] -> Bool
grade [1,n] = n >= 90
grade [2,n] = n >= 80
grade [3,n] = n >= 70
grade [4,n] = n >= 70
grade [5,n] = n >= 70

さて、上記の解答の流れは、入力処理時にgrade関数を合成して、ansはBoolのリストになっています。

一方、入力作業時には単純にデータだけを取り込んだ場合、ansは[[a1,b1],[a2,b2],...]のようなリストのリストになります。しかし、そんなリスト処理が出来るように定義したgradeは、filterの引数としても使えます。こちらのほうが、filterを使っている感じがするかもしれません。

解答例 入力はそのまま受け取り、filterで合否判別
import Control.Monad

main = do
    n <- readLn
    ans <- replicateM n $ map read . words <$> getLine
    print . length $ filter grade ans 

grade :: [Int] -> Bool
grade [1,n] = n >= 90
grade [2,n] = n >= 80
grade [3,n] = n >= 70
grade [4,n] = n >= 70
grade [5,n] = n >= 70

成績判定(7)

https://algo-method.com/tasks/3c5f9db3a7dd3e75

この問題では主に3つの処理が必要です。

  • 中間と期末の試験からグレードを判断する数値を計算
  • グレードを計算
  • n人分の処理

グレード判定数値の計算

入力を受け取る時の型を考慮して計算用の関数を定義してみます。max関数は、引数を2つとって大きな方を返します。

calc [a,b] = max b ((a+b) / 2) 

Haskellでは、/演算子は、Double等の小数点以下の数を扱える数値の型を引数にとり、整数を表すInt型を引数に取ることは出来ません。

ここで、入力のコードのパターンとして、以下のものがよく使われます。

xs <- map read . words <$> getLine :: IO [Int]

このコードには型注釈がついていて、受け取った方をInt型に変換するように指示しています。この典型コードをコピペすると上記のcalc関数は、入力する型がIntで演算子/の引数の型が合わないというエラーがでます。この場合、型注釈を変更するか、型注釈を外して型推論に任せるようにしましょう。

尚、max関数の引数の型はOrd型クラスのインスタンスで、IntもDoubleもどちらも使えるので、ここではエラーは起こりません。

実数か整数か

Haskellは型にシビアなので、型注釈が必要か型推論が使えるかや、特にDoubleを使う場合には、何処かでIntに変換するのか等を整理して丁寧に処理しましょう。

尚、本問については全体的な流れとして、扱う数値全部をDoubleとしても特に問題ないので、型推論にまかせて余り考えずにコードを書いてしまうのも一つの方法です。

グレード計算

複数の場合分けなので、ガードを使ってgrade関数を定義してみましょう

grade x 
    | x >= 90 = "S"
    | x >= 80 = "A"
    | x >= 70 = "B"
    | x >= 60 = "C"
    | otherwise = "F"    

n人分の繰り返し

今回の問題は、各人毎に処理をした結果を集計する必要がありません。そこで、replicateM_関数を使って、do構文の中で一行の読み込み毎にその人に対する計算を行って表示するという方法も行えます。以下のコードに示します。do構文はmain関数で使われているのと同じで、式の中でも複数のIOアクションを書くことが出来ます。

main = do
    n <- readLn
    replicateM n $ do
        x <- calc . map read . words <$> getLine
        putStrLn . grade $ x 

また、入力部分と出力部分で処理を分ける場合の参考コードは、次のようになります。

main = do
    n <- readLn
    -- 入力部分
    xs <-replicateM n $ map read . words <$> getLine
    -- 出力部分
    mapM_ (putStrLn . grade . calc) xs

更に、参考コードとしては、先にみたdo構文は、複数のIOアクションを順次書けるようにしていますが、実は、getLineというIOアクションから結果を取り出して、それを利用して、putStrLnというIOアクションを作成するというパターンはもともとモナド演算子>>=が使えます。IOアクションの場合、IOアクションの結果を取り出して、次のIOアクションの関数の引数に渡すことが出来ます。具体的には以下のようなコードになります。

main = do
    n <- readLn
    -- モナド演算子で合体
    replicateM n $ getLine >>= putStrLn . grade . calc . map read . words

解答例

繰り返しをdo構文で表現する解答例
import Control.Monad

main = do
    n <- readLn
    replicateM n $ do
        xs <- map read . words <$> getLine
        putStrLn . grade . calc $ xs

calc [a,b] = max b ((a+b) / 2)

grade x
    | x >= 90 = "S"
    | x >= 80 = "A"
    | x >= 70 = "B"
    | x >= 60 = "C"
    | otherwise = "F"

所持金

https://algo-method.com/tasks/e25200e9c392ccef

この問題の解答は、ある場合に日付、ある場合に最終日の所持金という、違う範疇のものになり、計算途中の条件によって求められるものが異なっています。

そして、通常のループ処理での解き方の第一観としては、次のようなものになると思います。

さて、上記のループ処理では、ループを抜ける条件が2つあります。

  • 最後までデータを受け取った
  • 今日の残高がマイナスになった

このように、ループで処理の分岐を行うプログラムの場合には、何かの条件が起こった時に、ループを抜けて別処理に切り替えるというプログラムの構成が出来ます。

しかし、Haskellの場合は、基本的に処理をループとして考える解き方をしないので、まずは、Haskellらしい別のアプローチを考察してみます。

前準備

まず、初めに問題を単純化するために問題の意図に沿った前処理を考えます。

ここで、この問題のその日に関する入力情報は2つです。

  • プラスかマイナスか
  • その金額

しかし、この情報は結局、残高を計算するうえで、プラスマイナス付きの数値のリストに変換してしまえば、そのリストに対する繰り返し処理(mapやfoldlや、ここで紹介するscanl)で分かりやすく使えるようになります。

いつものように、一行の入力の返り値が2数値のリストで受け取る式を使うならば、次のような関数を定義して合成すると、プラスマイナス付きの1つの数値のリストになります。

一行2数値についての処理
calc . map read . words <$> getList

-- 1要素目の数字で出入りの場合分け
calc [1,x] = x
calc [2,x] = -x

上記のコードは、一行分の処理で1 22となり、2 5-5となります。

これを複数処理してリストに格納するには、replicateM関数の出番です。

n行の取り込み
xs <- replicateM n $ calc . map read . words <$> getList

リストに対する処理として考える

ここで、毎日の増減についてのデータのリストが準備できました。この問題の求めるべきものの一つは、最終日の残高です。
残高は、初期値に今準備しt日々のデータを順次加算していくことで求めることが出来るので、次のようなコードが考えられます。

順次処理した結果を求めるfoldl関数

仮に日々の入力の取り込みに関して既に処理したデータが[7,-12,8]のようになっている場合(問題の入力例1の場合)、初期残高が10とすると、毎日のデータを加算した最終的な残高はfoldl関数を使って以下の通り求めることが出来ます。

ghci> foldl (+) 10 [7,-12,8]
13

しかし、本問ではもう一つの課題として、日々の計算の途中で数値がマイナスになった場合は、その日を答えなければならないのですが、foldlでは、その途中でマイナスになったかどうかを知ることが出来ません。

そこで登場するのが、この計算過程を検証できる関数がscanl関数です。

scanl関数は計算過程のリストを返す。

まず、fold系とscan系はやっていることは同じなのですが、返すものの形が以下のような関係になっていることを把握しましょう。

関数 やること 返すもの
fold系 リストに対する処理 1個の値に凝縮
scan系 fold系と同じ 過程のリスト(リストの最後がfold系の答えと同じ)

scanlの引数の与え方はfoldlと同じです。計算も同様で、acc(初回は初期値)にリストの要素を足して、それを次のaccにするという処理を繰り返します。

scanl (引数を2つ取る関数) (初期値) (リスト)

実際に先のデータをscanl関数で処理してみましょう。

ghci> scanl (+) 10 [7,-12,8]
[10,17,5,13]

この様に、scanlで処理すると日々の残高が分かるようになりました。この場合には、結果のリストの中にマイナスの数値がないので最終日の残高が答えになることになります。

ここで、結果のリストと日々の残高の関係を考察した時、実は、リストの1つ目の要素は初期値で、0日目の残高は2つ目の要素からになっています。ですから、リスト自体でその日の残高を正確に表現するためtail関数を使って、1つ目の要素を捨てて2つ目以降の要素を残す様に処理します。以下がtail関数で1要素目を捨てる具体的なコードです。

ghci> tail $ scanl (+) 10 [7, -12, 8]
[17,5,13]

では、次の例を見てみます。

ghci> tail $ scanl (+) 10 [8, -20, 10]
[18,-2,8]

ここで処理したリストでは、1日目の残高としてマイナスの数値が現れています。つまりこのケースの場合、最終日の残高ではなく、この日付を答える必要があります。

リストの処理を単純に繰り返す目線

さて、ループの考え方に慣れていると、先のダイアグラムで見たように、ループの途中でも条件分岐で脱出するという方法で頭の中がいっぱいになってしまうのですが、haskell的には、処理の途中で分岐という処理は考えません。

リストの処理は、リストの処理で全部やってしまう!とういうよりも、リストの処理はリストの処理という塊!という風に考えてしまうと、Haskell的に考えられるようになると思います。

そこで、問題を解く流れの考え方として、「毎日の金額の増減リストを処理して、毎日の残高リストを作り」ます。これは、今やった作業です。そして、この作業の最中に「マイナスになる日の処理」を入れる必要はなく、次の別のリストに対する作業として、「残高のリストの中にマイナスがあるのか?」という課題に挑むのです。

あるリストを処理してリストを作り、そのリストを処理してリストを作り、、、という、作業を繰り返すイメージです。

インデックスが必要なパターンの定番

さて、ここで処理を続けていく前に、本問の場合、日付の要素が答えの中に必要になってきます。少し前の問題で考えた通り、Haskellのリストはそのままではインデックスの情報が無いので、インデックスに関する情報が必要な場合、zip関数を利用して[(インデックス、値),(インデックス、値),...]の形のデータ型にしてしまうのが、定番のパターンになります。

ghci> zip [0..] $ tail $ scanl (+) 10 [8, -20, 10]
[(0,18),(1,-2),(2,8)]

有るのか無いのかはfilter関数でチェック

さて、以上の処理でインデックスを振ることが出来ました。これで、リストの中から必要なものを抜き出した時に、そのインデックスを答えとすることが出来ます。そして、あるリストから必要なものを抜き出す処理は、filter関数を利用します。今、処理するリストの要素がタプルになっているので、比較すべき方の要素をsnd関数で取り出す処理を関数合成して、フィルタの関数を作成しましょう。

フィルタ関数の例
filter ((<0) . snd) リスト

また、最終的な、答えとして必要なのは、日付なので、インデックスの方です。タプルのリストから1つ目の要素だけを取り出す関数も合成してあげましょう。

フィルタ関数にインデックスを取り出す関数を合成する例
map fst . filter ((<0) . snd) $ リスト

これで、今までの処理を受けます。

ghci> zandaka = tail $ scanl (+) 10 [8, -20, 10]
ghci> map fst . filter ((<0).snd) . zip [0..] $ zandaka 
[1]

つまり、1日めにマイナスになるという結果が得られました。

色々なパターンを場合分けする

ここまでで大まかな処理は大方出来ました。

そこで、あらためて、この処理で起こり得るパターンを考察すると、以下の3つのパターンになります。

  • マイナスになるパターンが無い
  • マイナスのパターンが一つ
  • マイナスのパターンが複数

これを題意に即して、どうしたらよいかを検討します。

マイナスがない

この場合、答えとしては最終日の残高が答えとなります。そして、もう一つ重要なのが、先のリスト処理でマイナスがないというのは、[](空リスト)として答えが得られます。これは、先の問題で見た通り、構造の違いによるパターンマッチで処理しやすいパターンになります。

マイナスが一つ

リストの要素自体がマイナスになった唯一の日になり、これが答えになります。そして、リストの要素が一つという構造もパターンマッチで表しやすいパターンになっています。

マイナスが2つ以上

題意からすると、マイナスになった初めの日なので、インデックスの中で一番小さい数字が答えの日になります。しかし、処理の流れから、filter関数の答え自体が、既に小さい順に並んでいそうなので、1つ目の要素が、答えになります。
そして、2つ以上の要素を持つリストの1つ目の要素も構造的に表すパターンマッチに表しやすい形になっています。

case式のパターンマッチ

上記の場合分けをcase式を使ったパターンマッチの場合分けで具体的に見てみましょう。ここで、マイナスの要素について、インデックス要素が小さい順に並んでいる場合、1つの場合も、複数で1つ目の要素の場合も、実際には同じパターンマッチで表す事が出来るので分けなくてもOKです。

case ans of
    [] -> last zandaka --zandakaはscanl適用後の結果のリスト
    -- [a] -> a -- 要素が一つの場合
    (a:_) -> a

解答例

上記で考えた流れを組み合わせて、Haskell的な解答に望んでみましょう。

解答例
import Control.Monad

main = do
    [n,k] <- map read . words <$> getLine

    -- 入力を取り込みながら、使いやすい(わかりやすい)リストの形に変換
    xs <- replicateM k $ calc . map read . words <$> getLine

    -- 各日の残高を保持するリスト
    let zandaka = tail $ scanl (+) n xs

    -- マイナスになる日付のリスト
    let day = map fst . filter ((<0).snd) . zip [0..] $ zandaka

    -- マイナスになる日のリストで答えるべきものを場合分け
    print $ case day of
                [] -> last zandaka
                (a:_) -> a

calc [1,x] = x
calc [2,x] = -x

ショートカットをする処理の解答例

上記で紹介したテーマは、「リストはリストとして全部処理」という目線で分かりやすく解きました。

しかし、実際には、早いうちにマイナスになるケースの場合に最終日まで計算する必要はないので、上記の処理だと毎回無駄な処理をする可能性を含んでいます。

このあたりについて競技プログラミングの目線から云えば、問題的に最悪ケースを想定する必要が有る場合には結局全探索の時間が問題になるので、マイナスになるのが早いケースがあるということを考慮する必要はありません。一方で、これらの処理をある条件のもとで繰り返す様な問題の場合には、最悪ケースばかりではなく早い処理を考慮しないと間に合わなくなるケースもあるかもしれません。本問題で云えば、基本的にマイナスになるのが早いケースを積極的に処理する必要がない問題です。

ここで、Haskellだからといって、先に述べたように、プログラムの構造がいつでも、個々のリスト処理を完了させて、それを渡していくことしか出来ないわけではありません。当然、ちゃんと、リスト処理の最中に、マイナスになったらそこで答えを返すという処理も書くことが出来ます。

以下の解答例は、マイナスになるのを考慮した処理の考え方の解答例です。

計算途中でマイナスになることを考慮する処理
import Control.Monad

main = do
    [n,k] <- map read . words <$> getLine :: IO [Int]
    ca <- replicateM  k $ calc . map read . words <$> getLine :: IO [Int]
    print $ solve n 0 ca

calc [1,n] = n
calc [_,n] = (-n)

solve ans count []
    | ans < 0 = count
    | otherwise = ans
solve ans count (x:xs)
    | (ans + x) < 0 = count
    | otherwise = solve (ans + x) (count + 1) xs

ここで、ループ処理の途中で本来の終了条件とは別の条件でループを抜ける様な処理を、リスト処理で行う場合のコツを少し紹介します。

一般的にリストを処理する自作関数を定義する時は、再帰関数を定義します。通常のリスト処理を再帰関数で行う時、要素を一つづつとって、残りのリストを再帰するので、リストを最後まで処理した時、原則的な再帰の終了条件は[](空リスト)になります。しかし、それ以外の条件で終了させる必要が有る場合には、単純に、コレ以外の終了条件を関数定義に織り込むだけです。頭をきちんと整理して定義を書いてみるとそんなに難しくはありません。

また、本問の場合、scanlによる残高のリストをどんな状況でも最終まで計算できるので、一括したリスト処理を使いましたが、これが処理できない様な状況が現れる場合には、このように自分で関数を定義する必要がでてきます。

本問に関しての解き方を考える時、具体的なコードでは紹介していませんが、例外処理を行うような組み立て方がしっくりくるかもしれません。例えば、マイナスになる日を探す問題としてマイナスが無い場合を失敗と捉える等です。

平均値

https://algo-method.com/tasks/af2699380eaa6ec5

この問題の課題は「小数点以下を切り捨てて」の部分ですが、競技プログラミング的な知識としては、整数演算した商は小数点以下が切り捨てられたのと同じになるので、これを利用します。

解答例
main = do
    n <- readLn
    a <- map read . words <$> getLine
    print $ sum a `div` n

反転

https://algo-method.com/tasks/e91bb27bb7976b9b

リストの要素を反転させるには、reverse関数が利用できます。

解答例
main = do
    n <- readLn :: IO Int
    x <- map read . words <$> getLine :: IO [Int]

    mapM_ print $ reverse x

最後に

アルゴ式のこのカテゴリの問題を解いてみると、解き方を組み立てる事自体はそんなに難しいものはないので、その組み立てをどうやってHaskellで実現するか?という練習に適していると思いました。そして、その時に必要となるHaskellの基本的な関数、特に、replicate系とmap系のどれを使えばよいかの練習、また、演算子のセクションを使って小さい関数を作り、これを関数合成して、map関数等の引数に組み込む感覚等が訓練できるのではないかと思いました。

この項目の問題のアルゴ式の登録されている解答は、まだまだ10個前後しか無いものが多いので、みんなで解いて、どんどん登録しましょう!!

Discussion