「Haskellは競技プログラミングには向いている」と言われたりもするが、競プロやオンラインジャッジにHaskellで取り組もうとすると、最初は問題を解くよりも、「どうやって入力を受けて、出力すれば良いのか?」の方が分からなかったりする。
例えば、AtCorderの「はじめてのあっとこーだー」にある解答例とか。
import Control.Applicative
main :: IO ()
main = do
-- 整数の入力
a <- readLn
-- スペース区切り整数の入力
[b, c] <- map read . words <$> getLine
-- 文字列の入力
s <- getLine
-- 出力
putStrLn $ show (a + b + c) ++ " " ++ s
Qiitaの@myuon_myonさんの記事「Haskell競技プログラミング入門」の冒頭に書かれている、このコードとか。
import Control.Applicative
main = do
-- "L M N" ---> [L,M,N]
n <- fmap (read :: String -> Int) . words <$> getLine
-- 複数行バージョン
ns <- fmap (fmap (read :: String -> Int) . words) . lines <$> getContents
これらが何をしているのかスンナリ理解できないという、少し前の私と同じレベルの方と共有したい基礎の基礎。
例
たとえば「各行の数値を加算し、行ごとに出力する」といった問題を仮定して、入力が
10 20
30 40
50 60
だったとする。各行をsum
したいので、これを最終的に
[[10,20],[30,40],[50,60]]
この形で受け取りたい。
main = do
input <- getContents
-- "10 20\n30 40\n50 60\n"
print input
-
getContents
で複数行の入力が一つのStringとして取得できる getContents :: IO String
-
getContents
すると複数行の入力が、改行コードを含む一つの(IO付き)Stringになる
getContents
の戻りはIO String
だが<-
で変数input
にバインドすると、input
はIOが外れて、ただのString
になる - 標準入力を受ける関数は複数があるが、原則、
getContents
で可(getContents
は入力行が可変となるときに便利。入力行数が固定の場合はgetLine
が楽なときもある)
input <- getContents
-- "10 20\n30 40\n50 60\n"
let strList = lines input
-- ["10 20","30 40","50 60"]
print strList
-
getContents
で複数行が一つのStringになったものを行単位に分けるためにはlines
を使う lines :: String -> [String]
input <- getContents
-- "10 20\n30 40\n50 60\n"
let strList = lines input
-- ["10 20","30 40","50 60"]
let sepList = map words strList
-- [["10","20"],["30","40"],["50","60"]]
print sepList
- 1行に複数の値がある場合、
lines
の結果一行分が一つのStringになっているので、words
を使ってバラす words :: String -> [String]
- 各行(リストの各要素)を全てバラしたいので
words
をmap
する - ここまでで入力は、Stringのリストのリスト
[[String]]
になった
main = do
input <- getContents
-- "10 20\n30 40\n50 60\n"
let strList = lines input
-- ["10 20","30 40","50 60"]
let sepList = map words strList
-- [["10","20"],["30","40"],["50","60"]]
let ans = map (map read') sepList
-- [[10,20],[30,40],[50,60]]
-- これでも可
-- let ans = map (map (read :: String -> Int)) sepList
print ans
read' :: String -> Int
read' = read
- リストのリストの要素がStringのままでは演算できない。そこで
read
を使って数値に変換する read :: Read a => String -> a
-
read
は多相関数のため、このままだと何型に変換したいのかが分からないので、ここではread'
を定義することでInt
への変換であることを示している -
(read::String->Int)
のように関数に直接型を記述する方法でも可 - また、後続で数値演算が行われる処理があることで、型推論により型指定しなくても上手くいくケースもある。冒頭のAtCorderの例は
read
後に加算していることから型指定がなくともread
した結果を数字として扱ってくれる - まず内側のリストの各要素に
read'
をmap
するにはmap read'
とすれば良いのはすぐわかる (map read' ["10,"20"] ----> [10,20]
) - 更に外側のリストの各要素に、この関数を喰わせたいので結果として
map (map read')
となる
これで、やっと標準入力を望んだ形式に変換することができたが、いかにも冗長である。
関数合成等により、記述を簡素化する。
整理
まず、仮の変数にバインドした関数を実体と置き換える。
main = do
input <- getContents
print (map (map read')(map words (lines input)))
read' :: String -> Int
read' = read
かっこを$
で置き換える。右から左に進むpipeだと思って置き換えればよい。
hlint
などの力も借りるとmap
の繰り返しを合成関数の一回のmap
に置き換える、等のサジェストをしてくれるので、良いものは従う。
main = do
input <- getContents
print $ map (map read'. words) $ lines input
read' :: String -> Int
read' = read
最後の$
はlines
をその次の関数と合成するために見掛け上移動する。
一番最後の$
は無くせないが、途中の$
は関数合成の.
に置き換えられるので合成に直す。
main = do
input <- getContents
print . map (map read'. words) . lines $ input
read' :: String -> Int
read' = read
これでひとまず完成。
(今は最終結果をprint
しているが実際にはここから処理のための関数と出力のための関数が続く)
慣れてくれば最初からこの形で書き下せるが、完全に理解するまではひとつひとつ処理したものを変数にバインドしてはバケツリレーして引き渡し、処理が完成したところで結合して整理した方が間違えにくい。
応用
私が良くお世話になっているHackerRank (www.hackerrank.com)
このサイトの問題では、入力の先頭に行数が付いていることが多い。
これは他言語が入力を繰り返し受け取るためのループカウントとして使うことを意図しているのだろうが、Haskellの場合、getContents
で一括して複数行を受け取れるし、リストの場合、長さの制約はないので、いらない情報。
3
10 20
30 40
50 60
この3
は、こんな感じで落とせる
main = do
_ : input <- lines <$> getContents
print . map (map read . words) $ input
1行目の先頭の_
はワイルドカードで、リストの先頭要素は参照せず捨ててしまうことを意味する。
<$>
はfmap
の別記法でIO付きのString
に対して普通の関数であるlines
を適用できる。(functor)
本文書冒頭の両例でも<$>
が使われている。
冒頭例ではimport Control.Aplicative
しているが現在はimport
なしでPrelude
から使える。
冒頭の二番目の例の「複数行バージョン」と比較してみて欲しい。
また、最初の入力の処理への受け渡し方としては別の方法として
-- 入力がこれ
-- 10 20
getLine >>= print.sum.map read.words
-- 出力は
-- 30
このように>>=
を使って入力を処理に引き渡すやり方もある。
しかし、見掛け上、データの流れが途中で逆転するので分かりにくいという面もあり、単純なケースをワンライナーで書きたいというようなとき以外は個人的には使っていない。
(データの流れが逆転する、とは、>>=
では左から右に入力データが流れるイメージなのに対して、以降はwords
からprint
に向かって右から左にデータが流れるイメージのため)
ここまでが入力。これでやっと問題を解くことができるようになる。
そりゃあ、Haskell入り口高いって言われるよね。
(また、本気で競技プログラミングするならString
ではなくByteString
のような処理効率の良いものを使うことになるそう。本件、うまくできたら別途記事にします)
出力編に続く・・
追記
@Mizunashi_Mana さんにコメントにてリスト内包表記を使った書き方を教えていただきました。
main = do
_ : input <- lines <$> getContents
print . map (map read . words) $ input
-- これをこう書けるそうです
main = do
input <- getContents
print [ [ read x :: Int | x <- words line ] | line <- tail (lines input) ]
リスト内包表記をこんな風に使えることに驚きました。
感動的なまでに読みやすいです。