アルゴ式 「標準入出力」をHaskellで解いて、その復習
Haskellに興味を持ち、「始めてみよう!」と思い立ったものの、初学者が理解できるくらい易しく、かつ実践的で参考になるコードを見つけるのは簡単ではありません。特に初心者が直感的に学べるお手本となるコードの例が不足しているように感じます。
そこで、初学者向けのプログラミング問題と解説を提供している「アルゴ式」の問題を実際に解いたコードを紹介し、それをもとにHaskell的な解法の考え方を、自分の復習も兼ねて記録していきたいと思います。
これにより、初学者による、初学者のための、おしゃれなHaskellコード集がここに爆誕します!ぜひ一緒に楽しみながらHaskellを学んでいきましょう。
「標準入出力」の問題全体について
アルゴ式の「コーディングによる問題解決」にある「ロジック実装初級」の「標準入出力」の中の各問題を解いたときの復習ノートです。
一般的な競技プログラミングを行う場合、あるデータを標準入力から受け取って、それを処理した答えを標準出力に表示するという基本的な処理が出来るようになることが、まず必須になります。
そして、これに加えて、Haskellの場合は僕たちのような初学者にとって他のプログラム言語とは違った少し不思議なものや少し変わった仕組みを目にする事になるので、それらHaskellに独特であろう点も丁寧にゆっくりと慣れていきましょう!
標準出力
まずは、標準出力を介した表示に関する各問題です。
問題「Hello Algo-method!」
この問題は、文字列を標準出力に出力する所謂 hello worldコードです。
ただし「アルゴ式」でのこの問題の位置づけは、各言語のコードを問うだけではなくて、アルゴ式のWebサイトのUIの使い方のチュートリアル的な問題にもなっています。なので、初めてアルゴ式で問題を解く場合には、実際にこの問題ページの解説に従って操作してみましょう。流れとしては
- 言語をHaskellに切り替え
- フォームにコードを書く
- コードを試す
- コードを提出する
- ジャッジ結果を見る
となります。
UIを操作してHaskellの環境へ
アルゴ式のデフォルトのコード入力フォームにはC++の HelloWorld コードが入力されていますが、Haskellで解答する場合、入力フォームの上にある言語の切り替えUIで「haskell」を選ぶことから始めます。そうすると、入力フォームに以下のようなHaskellの Hello World コードが表示されます。
module Main where
main = putStrLn "Hello World!"
ここでの問題「Hello Algo-method!」は、このhello worldコードを書き換えて"Hello Algo-method!"という文字列を表示してくださいというものなので、""で括られた文字列を指定のものに書き換えてあげればOKです。コードが完成したら「コードを試す」ボタンで動作を確認し、良さそうならば「提出」ボタンをクリックして提出してみましょう。
Haskellの形
ここで、実際にHaskellのコードに触れたわけでですが、このHaskellの不思議な見た目について、簡単にまとめておきます。
コードはmainを定義している
まず、haskellのコードはPythonのスクリプトの様に上から順に実行されていくわけではありません。C++の様に、各種関数を定義し、最終的にmainという名前の関数が実行されます。
つまり、上述のhello worldコードは、main関数を定義しているコードなのです。
イコール記号は定義
アルゴ式でのC++回答フォームのデフォルトのコードをみると以下のようになっています。
#include <bits/stdc++.h>
using namespace std;
int main() {
cout << "Hello World!" << endl;
return 0;
}
関数名の後ろに、仮引数を示す()が来て、そのあとにコードの本体が複数行{}で囲われている形です。こういう形式で関数を定義するパターンの言語は多いと思います。また一方で、イコール記号は、多くのプログラミング言語では、なにかの変数になにかの値を「代入」するような使われ方をします。
このため、Haskellのmain =
と書かれているのをみると少し不思議が感じがするかもしれません。
Haskellの場合では、まず、イコール記号のニュアンスが異なり、定義を行う場合にイコール記号 = が使われます。ですから、main =
と書かれているのは、mainに何かを代入しているのではなくて、main関数を定義しているのです。
つまり、次のようパターンになっているものは、main関数の定義なのです。
main = なんたらかんたら
そして、アルゴ式の解答を書く場合も、いつもこのパターン、main関数の定義の書式で書いていくことになります。
関数には引数を示すカッコが無い
さて、上述コードについて、mainを定義する中身である「なんたら」の部分は具体的に次の様になっています。
putStrLn "Hello Word!"
大方の皆さんが予想する通り、putStrLn
が文字列を表示するための関数であり、"Hello World!"
の部分が、putStrLn関数の引数で具体的に表示させるための文字列です。
実は、Haskellの関数は、関数名の後に引数を示すためのカッコがありません。一見、カッコがないとコードの構造がわからないのでは!?と思えそうですが、あるルールやなぜカッコがないかのHaskellの構造が解かっていく内に、カッコがないから読みにくい!ということは無い! ということが分かってきます。ちょっとづつ慣れていけるので心配しなくても大丈夫です!
ここまでくるとなんとなく、haskellでのHello Worldコードの構造もすんなりと把握できて来たのではないでしょうか?
もちろん、Haskellの式はどんどん複雑になっていくので、こんなに単純なものばかりではありません。なのでこの記事ではその都度、式がどんな構造になっているのかを一緒に考察していきましょう。
putStrLn
関数
文字列を表示するさて、この1問目に関しては上述までで実際に正解のジャッジが得られたのではないでしょうか?
haskellでコードを書いていく上で、初めにやったほうがいいなーと思うことの一つは、基礎的な関数は、自分なりにまとめておいて、綴りも含めて暗記してしまうことです。どの言語でもそうかもしれませんが、Haskellの場合その「基礎的」で覚えておくべき関数や構文の量が少しだけ多いです。ただ、こういうのは、やっているうちに覚えるというよりも、意識的に暗記してしまった方が、そこでイライラせずにすむのでお勧めです。
そんな中で、一番に覚えるべき関数は当然putStrLn
関数です!何度でも出てくるので、補完なんか使わなくても無意識で打てるように、綴りも大文字小文字も含めて覚えちゃいましょう!
問題「掛け算」
この問題のHaskellで解く際のテーマは大きく3つあります。
- 掛け算という数値に対する演算を行うこと
- 数値を表示すること
- カッコで示されていない関数の引数をどうとらえるかということ
四則演算の演算子
アルゴ式でC++やPythonの説明があるように、Haskellの基本的な演算子を紹介すると次の通り
演算子 | 使用例 | 意味 |
---|---|---|
+ | a + b | aにbを足す |
- | a - b | aからbを引く |
* | a * b | aにbを掛ける |
/ | a / b | aをbで割る(実数) |
`div` | a `div` b | 整数aを整数bで割った商 |
`mod` | a `mod` b | 整数aを整数bで割った余り |
特に、割り算について、/
だけでなく、整数の割り算の商や余りは、`div`や`mod`という、関数をバッククオート「`」で括った中置関数を使うのを覚えましょう。
print
関数
数値を表示するなら先の問題では、putStrLn
関数で文字列を表示しましたが、ここで紹介している演算子の答えは整数(Int型)や実数(Double型)であり、これらの型をputStrLn
関数に渡してもエラーになります。
そこで登場するのが、print
関数です。文字列以外の型の値を標準出力に表示してくれます。
print 10
関数の引数に式を渡す
ここで、先に勉強したとおり、Haskellの関数の引数部分はカッコがありません。この問題では print
関数で表示させます。
なんとなく適当にコードで書けば以下のようになりますが、これは実はエラーになります。
print 123 * 456
実際のところ、まだコードの並びのルールを知らないHaskell初学者にとって関数名の後に引数を示すためのカッコが無いというのは、haskellのコードを読んだり書いたりするときに大問題が勃発します。
まさに、上記のコードのような自分の意図とは違うエラーです。
ではどんなルールに従ってコードを読めばよいのでしょうか?もう一度コードをみてみます。
print 123 * 456
まず、haskellで演算子以外の部分で単語の並びがある場合、その先頭が関数になっていて、それに続いている単語が引数になります。そして、演算子を挟まない関数と引数の結びつきは、演算子との結びつきより強くなります。
つまり、このルールのとおりに見ると上記のコードは、*
演算子の前の部分はprint 123
となっているので、その先頭print
が関数で、それに続いている123
が引数になります。そして、print 123
の部分が他の部分より強い結びつきになります。
つまり、これを計算の優先順位のカッコを使って示すと次のようになります。
(print 123) * 456
こうしてみると、エラーが出るのも当然だとわかります。print 123
の結果と456を掛け算していますが、そもそも、print 123
の結果は数値ではないので掛け算の引数になれずエラーになっているのです。
ということで、意図としては
print (123 * 456)
結合の強弱を意識する
Haskellでは各単語や記号毎の結合の強さが上手に決められているのです。
例えば、次の式を見てください。
わざわざ、
haskellには他の言語のような関数名のあとに引数を特定するカッコはありませんが、一般の計算式のときに使うカッコ、すなわち、
上記で示した、print
に渡す前に計算させるために使ったカッコがそれです。
逆に、例えば、2つの引数を取って、その合計を返す関数add
というものがあったとします。そして、「1と2を足したものと3と4を足したものを合計する」ことを考えると、次のようになります。
add 1 2 + add 3 4
これに優先順位のカッコをあえてつけるならば、次のようになります。
(add 1 2) + (add 3 4)
しかし、Haskellでは、関数と引数部分の結びつきが演算子+
よりも強いので、このカッコはルール上必要ありませんし、慣れてくれば、すぐ無くても解るようになってきます。
いうなれば、関数とその引数部分の結合の強さは、掛け算のようなもので、特に、変数と用いられるような式、
解答例
ここまでのものをまとめて、提出用の解答例となるものを示しています。
解答例
module Main where
main = print (123 * 456)
アルゴ式では、「確認問題」と呼ばれる問題以外では、他の参加者の解答例を閲覧することが出来ます。問題ページのアイコンの一つが「提出一覧」(カーソルを持っていくとホバー表示される)なのでクリックすると一覧が表示され、さらに言語で絞り込みを掛けることが出来るので、そこにHaskellと入れてあげれば、Haskellでの解答の一覧を表示させることが出来ます。
Haskellでは、簡単な問題であっても、全く皆同じということは少なくて、人それぞれ色々な書き方がされています。また、Haskellで参加している人の数は少ないので、一覧を全部見てしまうことも出来ます。他の人のコードを見ると「こんなふうにも書けるのか!」となったり「難しすぎて、わからんすぎる、、」となったりして、どっちにしても楽しいので、是非、覗いて見るようにしましょう!
$
演算子
最頻出のさて、この問題の他の人の解答を見渡してみると、以下のように$
演算子を使っている人が多いのが分かります。
さて、haskellには色んな記号演算子が出てきます。普段、+
とか-
とか*
は見慣れていますが、haskellにはもっとたくさんの演算子が出てきます。別に何か特別な事をするわけでなく、+
演算子と基本は同じように、その両側の要素を用いて演算をするだけです。記号にビビらず、どんどん把握していきましょう。
その中でも$
演算子はアルゴ式の解答で使われるだけでなく、普通にHaskellの式の中で頻出します。
print $ 123 * 456
この式を読み解くには、結合の強弱を考えてどのような塊になっているかを把握することになります。
ここでは、print
、123
、456
の各ワードの間には、それぞれ、$
と*
という演算子が入っています。一方、演算子が無いところはありません。ですから、$
と*
の強弱があるのかという話になりますが、*
演算子は$
に比べて強い結合に決められています。具体的にどうこうことかといえば、次のようになります。
print $ (123 * 456)
*
が+
より強いのと同じですね。
さて、ここまで来て、$
が演算子という事で混乱している人がいるかもしれません。普段よく見る演算子というものは、+
という演算子とその両側に数値が来るのを想像します。若しくは、もうちょっと頑張って、文字列や配列ならなんとなく頭の整理もつくかもしれません。そして、少なくとも演算子の両側に来るものは同じタイプ、例えば、数値だったり、文字列だったりです。
しかし、もう一度上の式をみると、$
の後ろにある(123*456)
は計算結果として、数値になるんでしょうが、$
の前にあるのは、print
であり関数が書かれています。
「は?!どいうこと??」と思える人のほうが普通だと思いますが、しかし、これはそのまま正しいHaskellの式なのです。
これに違和感を覚えるのは、単に、演算子の両側には数値とかの値しか入らないという思い込みと、演算子の両側には同じタイプのものしか入らないという思い込みで頭が曇らされているからなのです。
$
演算子の前には関数が入ります。しかも、引数が満たされていない関数が入ります。
ここでは本来print 10
のように、print
関数は、引数を一つとってその引数を表示しますが、$
の前には、必要な1つの引数を取っていないprint
だけが来るのです。
そして、$
演算子の後ろには、$
演算子の前の関数が必要とする引数の型の値を取ります。
で、結局この$
演算子はどういう演算をする演算子かといえば、$
の後ろにある値を、$
の前にある関数の引数として渡すというものです。
なので、単純にして書けば以下のようになっています。
print $ 10
この単純な式だけをみると、何故わざわざ演算子を介して、引数をやりとりするのか?この演算子の有用性がわかりません。だって、やってることは直接引数を渡している次の式と変わらないんだから。
print 10
しかし、実際には以下のように使われていました。
print $ 123 * 456
ここでのミソは、$
演算子が一般の他の演算子よりも結合の強さが弱いということなのです。
結合が一番弱いので$
演算子の後ろで、いろんな演算子を使って色々と計算をしたとしても、そこが$
よりも必ず先に計算されてひとまとまりになるのです。そして、ひとまとまりになった値が、最後に$
演算子を介して、前の関数に引数として渡されるのです。
なので、$
演算子の前には必ず、引数を一つ必要とする関数の関数の部分が来て、$
演算子の後ろには、前の関数に引数として渡すべき値を求めるための式が来ます。
そして結局は、上述の式は$
演算子を使わなければ、先の解答例と同じになります。
print (123 * 456)
上述の式は$
演算子は別に使わなければならないものではありませんが、もちろん他人が書いてある場合に、その意味を把握できる必要はありますし、自分でも使っているうちに使ったほうが式のまとまりの把握がしやすくなるので、是非、慣れていきましょう!
show
関数
今回、print
関数を用いたのは、値が文字列では無くて数値だったからです。一方、表示すべき値が文字列ならばputStrLn
を使えます。というわけで、数値を文字列に変換すれば、それを表示するためにputStrLn
関数を使うことが出来ます。
そして、数値、若しくは、別の文字列以外の何かの値を文字列にしてくれる関数がshow
関数です。
ここでghciを利用して、プロンプトに1を入れてみましょう。
gichi > 1
1
1という答えが帰ってきます。これは、1という数値を表現しています。
では、次はshow
関数に1を渡してみます。
gichi > show 1
"1"
結果のところが微妙に違って、""で括られており、これは文字列であることを表しています。
show
関数は、数値を文字列に変換する関数ではありません。数値だけでなくshowを介して内容を文字列の形で表現できる値すべて(型クラスという)を文字列になおしてくれます。
ですから、show
関数を使うことで、putStrLn
関数を使って次のように表現している回答者もいます。
putStrLn $ show 1
さて、ついでに上のコードの塊具合を確認しておきましょう。
まず、show 1
の部分でまとめたいのですが、次のように書くとエラーになります。
putStrLn show 1
これは、結合の強弱から、間になにも演算子がないので、3つがひっついて前から解釈されます。そうすると、putStrLn
の引数は1個のはずなのに、引数が2個あるよとして、エラーが出ます。
ですから、カッコでまとめる対策ならば次のようになります。
putStrLn (show 1)
次に$
演算子を使って、putStrLnに処理後の値を渡す場合には、次のようになります。
putStrLn $ show 1
最後に、123 * 456
を表示するにはどうしたらよいでしょうか?
カッコで攻める!
putStrLn (show (123*456))
$
演算子で攻める!
putStrLn $ show $ 123*456
ここで、結合の強さが同じ演算子である、$
が並んだ場合、右側から計算される決まりになっています。つまり、まず123*456
が計算されて、それがshow
に渡されて、文字列になり、その文字列が、$
演算子でputStrLn
に渡されます。
問題「余り」
この問題は、一つ前の問題で紹介した整数除算の余りを計算するmod
関数を使って演算するだけです。
解答例
解答例は以下の通り。
解答例
module Main where
main = print $ 27182 `mod` 818
中置関数
mod
関数は、本来、普通の関数であり、例えば、10を3で割った余りを求めるためには
ghci> mod 10 3
1
というふうに2つの引数を取る関数として利用できます。
しかし、このような2つの引数を取る関数はその関数名をバッククオートで括ることで、演算子のように利用することが出来、これを中置関数と呼んでいます。
ghci> 10 `mod` 3
1
特に2つの数値を取る関数に関しては、中置関数として使われれることも多いので、どちらが使われていても違和感がない程度に、両方使えるように理解しておきましょう。
問題「改行」
この問題は、複数行の出力をどうやってするのかを問う問題です。そして、それは他の言語の感覚から言えば、普通に順番に出力する処理を並べればいいやん?という超簡単な問題なのです。
ところが、この問題は、Haskellの驚きの事実を教えてくれます。
さて、Haskellのコード入力フォームにデフォルトで表示されるHello Worldコードをもう一度よく見てみましょう。
module Main where
main = putStrLn "Hello World!"
今まで書いてきたコードは、main
関数を一行で定義してきました。実は、普通に定義すると、mainは一行の処理しか定義できないのです!!!
つまり、もう一行、別の何かを表示させたい場合、どうすればよいの?!状態なのです。
複数の処理を行うdo構文
しかし、一般に処理が1行の定義で終わる事はありません。もちろん、Haskellでも複数の処理を順番にさせる事が出来ます。その構文が次のようなdo構文です。
module Main where
main = do
putStrLn "Hello World!"
print 2024
あまり難しく考える必要はありません。アルゴ式の問題を解く限りは例え一行のコードであっても、do構文を使っておけばOKです。
main = do
putStrLn "Hello World!"
注意すべき点は、haskellはインデント(行頭の空白)に意味があります。do構文の及んでいる範囲は、同じ量のインデントを行って、mainの定義の範囲を示す必要があります。
これも、そこまで難しく考えづに、上記のコードの様な適当なインデントが必要というのを把握しておきましょう。
解答例
上記で示したdo構文を使い、後は問題の指示に従って、数値、文字列、数値を文字列にして等、それに応じた関数を使ってコードを書いてみましょう。
解答例
module Main where
main = do
putStrLn "1"
putStrLn "2"
putStrLn "3"
さて、ここまで出来ていれば、この後のいくつかの問題は、今までの組み合わせで行けるはずです。
標準入力
ここからは、標準入力からデータを受け取る処理の問題になります。
問題「2倍にする」
一般に標準入力からのデータの受取というのは、テキストのデータを基本的に文字列として受取り、これを必要に応じて数値として解釈する処理を行います。若しくは、受け取るデータが数値のみであると初めからわかっている場合には、数値として受け取る専用の関数を用いたりします。
つまりは、受け取る値が数値か文字列かに着目して、それにあった処理を行うのが一般的です。
ここで、この問題では一つの数値が渡されるパターンです。
readLn
関数
標準入力から数値を1つ受け取る標準入力から一つの数値を受け取るために利用する関数はreadLn
関数です。
標準入力から数値を受け取り、それを表示するコードのパターンを以下に示します。
main = do
n <- readLn :: IO Int
print n
このコードは、Haskellコードとして覚えるべき点が満載のコードです。
まず、全体の構造としてdo構文を用いて、複数の処理(この場合は2つ)を行っています。2つの処理のうちの2つ目の処理は、print
関数を用いて数値を表示する処理です。今までと違うところは、具体的な数値ではなくて、n
という変数が使われている点ですが、特に問題は無いと思います。
そして、ここで着目すべきは1つ目の処理です。
n <- readLn :: IO Int
まず、readLn
というのが標準入力からのデータを受け取るために用いる関数なのですが、Haskellの場合、少しだけ不思議なルールのもとに使うことになります。
Haskellには、IOアクション というものがある
まず、readLn
関数の結果は、標準入力から貰ってきた入力(数値や文字列)ではなく、IOアクションというものであるということを覚えなければなりません。
Haskellでは、Haskellの外の世界での出来事は抽象化したものとして表現されます。ここでは、「標準入力からデータを得られるなにか」として抽象化されています。あまり難しく考えず、readLn
関数自体の結果は、標準入力から受け取る具体的な値ではなくて、「標準入力からデータを得る事そのもの」がIOアクションであると理解しましょう。
<-
演算子
IOアクションから値を取り出すそして、「標準入力からデータを得る事そのもの」というIOアクションから、そのアクションが把握している(標準入力で得られた)データを取り出すための演算子が、readLn
関数の前にある<-
演算子なのです。
まずは、標準入力から値を得る場合、こういう形式で<-
を使うということを暗記してしまいましょう。
<-
と=
を区別する
上で書いたとおり、標準入力を介したデータの取り込みにはIOアクションを使い、そして、そのパターンでは、<-
という演算子を使うことを覚えてしまおう!と提案しました。
あわせて、ここでアルアルな疑問も検証しておきます。
n = readLn :: IO Int
<-
の変わりに=
を使った場合、どうなると思いますか?
さて、Haskellでの=
記号は、なんと説明していたでしょうか??
先に出てきたとおり=
記号は定義の記号です。つまりは、nがreadLnで定義されます。どういうことかといえば、n
自体がreadLn :: IO Int
という関数になるということです。
例えば、こんなコードが掛けます。(do構文の中で定義を行う場合、イコール記号だけでなく先頭にletキーワードを用いたlet構文という構文が必要になります。)
main = do
let n = readLn :: IO Int -- nを定義
a <- n --nが実行されて、標準入力からのデータの読み込み
print a
n
は一文字なので変数のように見えますが、実際にはn
と言う名のIOアクションを返す関数が定義されています。また、実際の標準入力からの取り込みはa <- n
の部分です。n = readLn
の部分ではありません。
なぜ、こんな話をするかといえば、特にPython、また、他の言語であっても、関数が返り値として標準入力から得られるデータを返して、それを何らかの変数に代入するためにイコールが使われるので、Haskellでも同じように書いてしまって、「あれ?うまくいかないなぁ、、」というのがありがちなのです。
なので、はじめは意識して、Haskellでは、標準入力からのデータの受取には、IOアクションを使って、IOアクションからのデータの取り出しには<-
演算子を使うということを暗記してしまいましょう。
PutStrLn
関数もprint
関数もIOアクション
そして、実は、今まで使ってきたputStrLn
とprint
も IOアクションです。
型注釈
次に、readLn
関数の後ろについている:: IO Int
の部分は、型注釈と呼ばれています。
型注釈はその前に書かれているものの型を表していて、ここではreadLn
が、IO Int
型であることを明らかにしています。
このIO Int
の部分が上で述べたIOアクションを型的に表現しています。IOアクションの型表現は見ての通り2つの部分からなっていて、前の部分はIOアクションをあらわすIO
の文字、そして、後ろにはそのIOアクションから取り出すことのできる値の型が示されてます。つまりはここではInt
型です。
Haskellにおいては型というものが凄く重要で、そもそも、計算途中で扱っているものの型がわからない状態になるとエラーになってしまいます。そして、こういうエラーに出くわすものの代表的なシチュエーションがこのread系の関数を扱う時なのです。また、Haskellのコードを読み書きする上では必ず、「型シグニーチャー」や「型注釈」という、型に関する表記を目にすることになります。
そこで、readLn
関数を扱うのと同時に、型に親しんでおきましょう。
しかし、ここでは、まず、IOアクションを返すreadLn
関数ではなく、扱いが簡単で、単純に文字列を引数にとって値を返すread
関数を使っていきます。
read
関数でもっと型注釈に親しむ
文字列を値にするHaskellには先に紹介したshow
関数というものがあります。そこで紹介した通り、show
関数は、ある値を人が認識できる形の文字列に変換する関数でした。実は、これと対をなすのがread
関数です。
read
関数は、単純に文字列を数値に変換するのではなく、「ある値を人が認識できる形として記していた文字列」を元の値に変換する関数なのです。
では、実際にread
関数を使って文字列を値に変換していこうと思うのですが、しかしその前にまず、よく起こるエラーのパターンを確認しておきましょう。
ghci> read "1"
*** Exception: Prelude.read: no parse
実はread
関数は、原則的に元の(変換する)値の型がわからなければ変換することが出来ません。具体的にどういう状況かといえば、"1"という文字列を値に変換する場合、その見た目だけからはIntなのかDoubleなのかわかりません。そこで、このようなエラーが起こるのですが、その原因は変換すべき型がわからないことなのです。
しかしながら、Haskellを初めた頃に混乱してわからなくなるのが、同じ様な式なのに、エラーが出ないこともあります。例えば、次のような場合です。
ghci> read "1" + 1
2
この式のかたまり具合はもう分かりますか?
演算子以外のread "1"
の部分がひとかたまりです。そして、この場合、+
演算子が取るものが数値であり、反対にある値がIntなので、「read "1"はIntに変換するしかない」とghcが推測してくれるのです。
そして、この推測が一意に決まらないような場合にエラーが出るのです。
もちろん、Haskell自体、式の形から明らかに型が分かる場合に、いちいち型を指定するのは煩雑なので、推測してくれるようになっているのですが、原則として、型についての注釈をしっかり書けるようになっておけば、推測してくれようがしてくれまいが、自分の式の意味を明確に表現できるようになるので、ここで億劫がらずに基本的な型の注釈が書けるようになっておきましょう。
型注釈の書き方
型注釈は::
記号を使って指定します。まずは、実物です。
ghci> read "1" :: Int
1
ここでしっかりと、この式のかたまり具合を検証しましょう。
::
は記号演算子なので、それの前後にあるread "1"
とIntに分けられます。左側は、記号が無いのでそこがひと塊で、右は一つです。この演算子は、演算子の左側に型を説明されるものが来て、演算子の右側に型の説明がきます。
そして、この演算子はこれで完結しなくてはなりません。
では実際に、もともとエラーの出なかった次の式について、どんなふうに型注釈をつければよいのか考えてみましょう。
ghci> read "1" + 1
2
何度も言いますが、Haskellには関数の引数を示すカッコはないのですが、計算の優先順位を示すカッコはあります。そして、複雑でわかりにくそうな式には積極的にカッコを使って自分で塊を明示するようにしましょう。つまりは、カッコを使って::
を使った式の部分を分離します。
ghci> (read "1" :: Int) + 1
2
式が組み合わさっているときは、単純にその要素となる式の部分をまずはカッコでくくればOKです。
では、もう少し、型注釈の理解を深めていきましょう。
関数の型シグニチャ
そもそも、Haskellでは、関数等を定義する場合にその定義されているものがどの様な関数であるかを型シグニチャという型をどのように処理するかという書式でを添え書きして、関数の構造を説明します。::
記号演算子はそのときに使われるものです。
もう一度、先の型注釈に着目します。
read "1" :: Int
::
記号の左側は、read "1"
であり、read
関数が引数を一つ取っている状態が記されています。ですから、::
記号の左側は「read
関数が引数を一つ取った」場合のその結果です。
そして、::
の右側がIntなので、read "1"
の結果がIntだと注釈しているのです。
しかしここで、read
関数そのものにも型注釈をつけることができます。
まずは、実物をみます。
(read :: String -> Int) "1"
まずは、式の塊から把握しておきます。::
記号を使ってread
関数の出力自体をその記号の右側の式String -> Int
で指定しています。見ての通り、->
という記号がありますが、これの優先順位は::
より強く設定されています。いつものようにあえてカッコ付すれば、以下のようになっています。
(read :: (String -> Int)) "1"
この::
の右側が塊ルール上一つだと、理解した上で、外側のカッコは、このカッコの中を先に処理してread
関数の戻り値の型を確定させてから、"1"という引数を取るために必要なカッコになっています。(もし、カッコがないと、Intと"1"が引っ付いて、おかしなエラーがでる)
そして、ここが一番重要な部分です。もう1度、::
記号の右側をあらためて見ると、String -> Int
となっています。これはあなたの直感道理で、String型の引数を取って、Int型の値を返すという書式です。
::
の右側では、関数の引数となる型を順番通り->
で繋ぎ、最終的な結果の型を最後に置きます。
型注釈の出てくる場面
さて、結局ここで何をしているかをおさらいして、整理してみましょう。
まず、初めにやった型注釈です。
read "1" :: Int
これは、関数に引数を渡した後の結果自体の型をIntにしようとしています。
一方で、関数自体に型注釈つけたのが次の方法です。
(read :: String -> Int) "1"
これらは結局、初めに説明した通り、read
関数の結果が確定されていない為に必要な処理です。そして、何故確定されていないのかは、Haskell自体の型クラスという便利で賢い機能が関係しています。ghciでreadに対して、:type
コマンドを発行すると、readの型シグニチャが見れます。
ghci> :t read
read :: Read a => String -> a
ここでは、具体的な型ではなくて、String -> a
と書かれています。そして、そのa
については、Read a =>
という表記が前置されています。
これは、readという関数は、文字列を受け取って、Read型クラスに所属するものを返すという説明を行っています。そして、「Read型クラスに所属するもの」というのが、複数あって確定していないので、a
という型変数が用いられています。
実際のプログラムの際、なにかの定義をするだけならば、aを確定させる必要が無い場合もあるのですが、実際の計算の場合には、aを確定させる必要があり、その確定させるための方法の一つが、型注釈をつけるというものなのです。
アルゴ式の初心者向けの問題において、これが関係してくるのはほとんど、このread系の読み込みの際です。型クラス自体に深入りしなくても、readを使う場合、型注釈が必要であるということと、その際の型注釈の付け方のパターンを覚えておけばOKだと思います。
解答例
IOアクション関数を使った入力データ取り込みは、まずはパターンを暗記です!
上述の暗記事項を組み合わせた上で、コードを組み立ててみましょう。
また、アルゴ式での標準入力取り込みを試すには、コード入力フォーム下の「入力」とかかれたフォームに事前にデータを書いてから「コードを試す」ボタンをクリックすればOKです。
解答例
main = do
n <- readLn :: IO Int
print $ n * 2
類似の問題
入力に関しては、同じパターンの問題です。
問題「文字列を 3 回繰り返す」
今までの問題では整数値を受け取っていましたが、この問題では標準入力から文字列を受け取ります。
getLine
関数
標準入力から文字列を受け取るこのgetLine
関数は、IOアクションを返す関数です。
このIOアクションは、文字列を1つ受け取るというよりも標準入力から1行を文字列として受け取るという処理を行います。
以下にgetLine
関数を使った読み込みのパターンを示します。
main = do
s <- getLine
putStrLn s
getLine
関数の前に<-
演算子があり、IOアクションからその内容である文字列を取り出している構造になっています。
ちなみに、このコードでは型注釈がありません。それは、getLine
関数のIOアクションはString型しか返さないので、明示する必要がないのです。あえて書くのならばgetLine :: IO String
となります。
++
文字列を結合する演算子この問題では、受け取った文字列を3回繰り返すわけですから、3個の文字列を結合してあげればOKです。このとき、文字列の結合には++
演算子を使うことが出来ます。
ghci> "ABC" ++ "DEF"
"ABCDEF"
解答例
文字列の場合、標準入力を得る関数が異なるのと同様に文字列を表示する点に気をつけて関数を選びましょう。
解答例
main = do
s <- getLine
putStrLn $ s ++ s ++ s
問題「先頭の文字」
標準入力から文字列を得る方法は先に覚えたとおりです。
この問題では、Haskellにおける文字列操作の基本が主題となります。
文字列は文字のリスト
まず、Haskellでは、文字列は文字のリストとして扱われます。すなわち、Char型のリストです。リストは[]で表され、各要素はカンマ「,」で区切って表されます。
ghci> ['a','b','c']
"abc"
文字は一重引用符で囲って'a'
のように表します。一方、文字列は二重引用符を使って"a"
と表します。
リストを扱う基本関数
Haskellでのリストは基本的なデータ構造で、その基本的な操作を行う以下の4つの関数は最初に暗記してしまいましょう。いずれも一つのリストを引数に取ります。
head
関数とtail
関数
head
関数とtail
関数は対で覚えます。
関数 | 処理 |
---|---|
head | リストの一番初めの要素を返す |
tail | リストから一番初めの要素を除いたリストを返す |
文字のリストの場合
ghci> head "abcde"
'a'
ghci> tail "abcde"
"bcde"
数値のリストの場合
ghci> head [1..5]
1
ghci> tail [1..5]
[2,3,4,5]
last
関数とinit
関数
last
関数とinit
関数は対で覚えます。
また、これはhead
関数とtail
関数の逆のパターンです。
関数 | 処理 |
---|---|
last | リストの一番最後の要素を返す |
init | リストから一番最後の要素を除いたリストを返す |
文字のリストの場合
ghci> last "abcde"
'e'
ghci> init "abcde"
"abcd"
数値のリストの場合
ghci> last [1..5]
5
ghci> init [1..5]
[1,2,3,4]
文字を文字列にする方法
head
関数やlast
関数での戻り値は文字列では無く文字であることに注意。
また、文字列は文字のリストなので、文字を[]で括ると、一文字の文字列になります。
ghci> ['a']
"a"
解答例
上述の知識を組み合わせることで、標準入力から文字列を取得し、その文字列をリストとして処理(一番始めの要素を取り出す等)をする。
解答例
main = do
s <- getLine
putStrLn [head s]
問題「足し算」
この問題では、標準入力から2行の入力を受け取ることが課題です。
Haskellでは、do構文を用いて、順に複数の入力処理ができるので、必要な回数だけ入力用の処理を行います。
解答例
main = do
a <- readLn :: IO Int
b <- readLn :: IO Int
print $ a + b
問題「文字列を繋げる」
文字列をどのように繋げるかがテーマです。
最もシンプルでわかりやすい方法は、先にも実践した++
演算子を使う方法です。
先の問題で文字列を繋げる演算子として++
を紹介しましたが、実は、「文字列を繋げる」のではなく、「リストを繋げる」演算子なのです。これを文字列に使えるのは、文字列が文字のリストだからです。仕組みさえ把握しておけば、文字列を繋ぐと覚えていても問題ありません。
解答例
main = do
a <- getLine
b <- getLine
putStrLn $ a ++ b
空白区切りデータの入力
ここまでも標準入力からデータを取り込む処理についての問題でしたが、入力の形式が一行に一つのデータでした。ここからは、一行に空白で区切られたデータをどのように複数のデータとして読み込むか? が問われる問題が並びます。
これまでのデータの入力は以下のように1行に一つづつデータが渡されていました。
1
2
このような形式の場合、複数のデータは複数行でもその回数だけ、readLn
やgetLine
で取り込むことが出来ましたが。
しかし、今回は、標準入力に以下の形式でデータが入力されます。
1 2
このようなデータの取り込みの方針は、次のように考えます。
- 1行を丸ごと
getLine
関数で文字列として読み込む - 文字列を空白で分割するして、分割された文字列が要素となるリストを得る
- 各要素を適切な型に変換する
問題「A + B」
1行に複数の数値が空白区切りで与えられるのは、アルゴ式をはじめ競技プログラミンでは典型的なものなので、これを受け取るコードは良くあるパターンで必須の暗記コードになります。
この具体的なコードは以下のようになります。
main = do
[a,b] <- map (read :: String -> Int) . words <$> getLine
print a
print b
このコードは、この問題で与えられる空白区切りの数値を受け取り、それを表示するものです。
このコードの中枢は当然以下の一行!
[a,b] <- map (read :: String -> Int) . words <$> getLine
まずは、どんなふうな構造になっているのか、カッコを付けて塊を把握しまます。
[a,b] <- ((map (read :: String -> Int)) . words) <$> getLine
では、このHaskellの魅力が詰まった一行をゆっくりと紐解いて、コードの意味をじっくりと味わいましょう!
words
関数で分解する
words
関数は一つの文字列を貰って、空白部分で分割した文字列のリストを返します。
ghci> words "a b c"
["a", "b", "c"]
この時、入力が文字的には数字であっても、文字列として扱われることに注意が必要です。
ghci> words "1 2"
["1","2"]
競技プログラミング等での値の入力はほぼ空白区切りなので、1行入力から、この関数を使うことで要素毎に分割したリストを得ることが出来ます。
map
関数
リストを処理するmap
関数は、リストの各要素に関数を適用して新しいリストを得る関数です。
このmap
関数は引数を2つ取りますが、1つ目は各リストの要素に適用するための関数を、2つめがその関数を適用するリストになります。
map 関数 リスト
具体的なコードをみてみます。第一引数である関数は、read
関数(型注釈付き)で、第二引数は、文字列のリストです。すなわち、与えられた文字列(数字の文字列)を数値のリストにするコードです。
ghci> map (read :: String->Int) ["1","2","3"]
[1,2,3]
ここで、もう少し詳しく見てみます。まず、map
関数の型シグニチャを見ましょう。
ghci> :type map
map :: (a -> b) -> [a] -> [b]
ファーストクラス関数
ここにでてくる(a->b)
の様に、型シグニチャーの中でカッコに囲まれている部分は関数を表しています。先に説明した通り、1つ目の引数は関数を取るのですが、どんな関数かといえば、一つの引数をとって一つの値を返すような関数です。
さて、Haskellでは、関数自体が引数になることは、$
演算子の項目で体験しました。Haskellの特徴としてよく言われる言葉に「ファーストクラス関数」という言葉があります。Haskellでは、関数自体、他の値と同等に式の中で使えるという意味です。つまり、$
演算子で話したように、普通の演算子の両側には数値等の値が来るのが常識と思っていましたが、実際には、関数が来ました。
同様にmap
関数も引数に関数を取ります。まさにHaskellなのです!
.
演算子
関数を合成する [a,b] <- ((map (read :: String -> Int)) . words) <$> getLine
上述コードの中にある.
演算子の部分に着目します。また、構造を見やすくするためにreadの型注釈は省略します。map read
の部分は結合が強いので実際にはカッコはいりませんがわかりやすいように明示します。
(map read) . words
この.
演算子ですが、両側にある関数を合成するための演算子です。関数を合成するとは、右側の関数を適用した結果を左側の関数に与えて結果をだします。
上記のコードを具体的に読めば、文字列にwords
関数を適用した結果の文字列のリストを(map read)
に与えて、文字列のリストを数値のリストにするコードです。合成されたものだけをみれば、文字列を受け取って、数値のリストを返す関数になります。
さて、この.
演算子も両側に来るものは関数であり、Haskellらしいのです。
引数の数と部分適用
ここで、.
演算子の型シグニチャを確認します。
ghci> :type (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
第一引数と第二引数にそれぞれ引数を一つ取る関数を取ることが示されています。
ここで、上記の例をもう一度見てみましょう。
(map read) . words
第一引数に来ているのは(map read)
です。これってありなの?!という話です。
何が問題なのかといえば、.
演算子の両側に来るのは、引数を1つとる関数なわけですが、そもそもmap
関数は第一引数に関数をとって、第二引数にリストを取る関数であり、合計、引数を2つとる関数です。引数の数があっていません。
しかし、引数を2つ取る関数なのですが、ここでは、map read
というふうに、1つ目の引数にreadが与えられているのです。つまり、map
関数は2つの引数をとるけど、一つはすでに与えられているので、残り一つの引数をとるってことになっているみたいだけれど、、、、これってありなの?!というはなしなのです。
Haskellではありなのです!!
Haskellでは、複数の引数をとる関数の一部の引数を埋めた状態の式は、残りの引数をとる関数になります。この一部の引数を適用して、残りの引数の関数として使うことを部分適用と呼んでいます。
もう少し実例を見てみましょう。単純な+
演算子は、先に紹介したユニットという書式を用いることで、2つの引数をとってこれを足す前置関数のように使うことが出来ます。
ghci> (+) 3 5
8
ここで、(+) 3
とだけすると、受け取った1つの引数に3を加える関数を表していることになります。
つまり、1つの引数をとる関数が出来ているはずです。なので、これをmap
関数で使ってみましょう。map
関数の第一引数は、一つの引数をとる関数を使うことが出来ます。
ghci> map ((+) 3) [1..5]
[4,5,6,7,8]
ユニットは、((+) 3)
を(+3)
と書くことが出来ます。
ghci> map (+3) [1..5]
[4,5,6,7,8]
では、次は、.
演算子も使ってみましょう。
ghci> map ((*2).(+3)) [1..5]
[8,10,12,14,16]
(*2).(+3)
の部分は、一つの数値を受け取って3足す関数に、一つの数値を受け取って2を掛ける関数を合成しています。結果として、一つの数値を受け取って3を足して2を書けた結果を返す関数になっています。この関数をmap
関数の第一引数として渡して、第二引数のリストに適用しています。
では、map read . words
も実際に使ってみましょう。
ghci> map (read :: String->Int) . words $ "1 2 3"
[1,2,3]
<$>
演算子
中身を処理する [a,b] <- ((map (read :: String -> Int)) . words) <$> getLine
上記の式のうち、((map (read :: String -> Int)) . words)
の部分については、「文字列を受け取って、文字列のリストに分解し、そのリストの各要素の文字列を数値に変換して数値のリストを返す」式であることを把握しました。
改めて、式の構造を確認すると、<$>
演算子の左側は、一つの引数(文字列String)を取って、数値のリストを返す関数です。一方、<$>
演算子の右側は、IO Stringと表される、中に文字列を持ったIOアクションです。
そして、<$>
演算子は、右側の中身に左側の関数を適用してくれる演算子なのです。つまりは、<$>
演算子を適用するとIO String
がIO [Int]
というふうに、IOに包まれたまま中身だけを変化させることが出来るのです。
なので、((map (read :: String -> Int)) . words) <$> getLine
の部分は結果的にIO [Int]
になるので、IOの中身を取り出すための演算子<-
を用いて中身の[Int]を取り出しています。
パターンマッチ
Haskellでは、=
や<-
の左側の定義されるものの部分に、具体的な要素のパターンを書いて、その要素を変数に割り当てることが出来ます。
[a,b] <- ((map (read :: String -> Int)) . words) <$> getLine
この問題では、入力の要素が数値2個と分かっているので、具体的に[a,b]
という2つの要素のそれぞれにa
,b
の変数を割り当てています。こうすることで、式の他の部分でこのa
,b
が使えるようになります。
解答例
入力得られれば、後はその入力を処理して表示するだけです。
解答例
main = do
[a,b] <- map (read :: String -> Int) . words <$> getLine
print $ a + b
類似問題
これら、それぞれの問題はに関するヒントとしては以下のものがあります。
- 処理に関しては、
max
やmin
という基本の関数を使うことで、2つの数から大きい方や小さい方を得ることが出来ます。 - 入力される要素の個数に注意しましょう。
- 数値に変換する必要がないものもあります。
問題「倍数の判定」
この問題では、与えられた2つの数値が倍数の関係にあるか、ないかを判別して、その有無によって処理を変更させる、所謂条件判断の処理を行うことが目的です。
Haskellでの条件判断は、ifの式で行えます。
if boolを返す式 then Tureの場合の結果 else Falseの場合の結果
注意点としては、ifは処理の分岐をする構文ではなく、True、Falseに応じた結果を返すだけの式であるということに注意。なので必ずelse部分が必要になります。
ここで、ifが式であることを意識して、次の2つのパターンを見比べてみましょう。
まず、if式の結果が数値として、print
関数の引数に与えられるパターンです。
main = do
print $ if True then 1 else 2
次に、if式の結果がIOアクションとして実行されるパターンです。
main = do
if True then print 1 else print 2
以上を参考に解答のコードを構築してみましょう。
解答例1
解答例1
main = do
[a,b] <- map read . words <$> getLine :: IO [Int]
putStrLn $ if a `mod` b == 0 then "Yes" else "No"
Yes No 問題
競技プログラミングでは、ある条件を満たす満たさないで、答えをYes,Noなどの2種類の文字列を返すパターンの問題がよく見られます。その時に便利なのがbool
関数。但し、Data.Boolをimportする必要があります。
import Data.Bool
bool (Falseの場合の結果) (Trueの場合の結果) (boolを返す式)
解答例2
解答例1
import Data.Bool
main = do
[a,b] <- map read . words <$> getLine :: IO [Int]
putStrLn . bool "No" "Yes" $ a `mod` b == 0
類似問題
要素が文字列なので、取り込み方のパターンに注意しましょう
Discussion