こわくないHaskell入門(初級その2)
手続き型に慣れた人にもやさしい、こわくないHaskell入門記事の第2弾です。
初級のつぎは中級だと思った? 残念。
まえがき
前回のやつのViewsが1000を超えた記念に続編を書いてみます。
前回は「Haskellって関数型(この言葉を見るだけで、うっ頭が...)とか呼ばれるけど、ふつうに手続き型でこわくないよ」を示しました。
今回はHaskellの型についてです。1
Haskellは強い静的な型付言語と呼ばれていてこわそうですが、実際にはこわくないです。
むしろ僕みたいにプログラム書くのがうまくない人がよろしくやれるようになります。
型を使えるようになって、いままで以上にコンパイラちゃんと仲良くおはなししましょう。
対象者
- なんかもっと生産性の上がる言語を習得したい人
- なんかHaskell入門しようとして挫折しかけた人
- なんかマサカリ投げたい怖いお兄さんたち
- なんか前回のを読んで楽しみにしてくれた殊勝な人
結論
やっぱりHaskellこわくないよ。
とっても便利だよ。
とっても学習障壁ひくいよ。
なのに、Haskellをお勉強すると他の言語も美しく書けるようになるよ。
Haskellの型
さて、前回の記事では、まるでスクリプト言語のようにサクサクとコードを書いていました。
でも実際に遊べる環境で遊んでみた人はあれに出会ってしまったのではいでしょうか。
「型エラー」に。
No instance for (Num [Char]) arising from a use of `+'
こういうやつです。
なんかいきなりよくわからないこと言われてびっくりしましたか?
大丈夫、数週間後のあなたは「型エラー?!ヒャッハー!!うんこーどは消毒だぜー!」と言っています。
その数週間後のあなたをこの現実世界に召喚するために、Haskellの型について一緒にお勉強しましょう。
ともかく型を書いてみる
いままでは「型」を明記せずにスクリプト言語みたいにコードを書いてきました。
main = do
let message = howOldAreYou "田村ゆかり" 17
putStrLn message
howOldAreYou name age = nameSan ++ ageSai
where
nameSan = name ++ "さん"
ageSai = show age ++ "歳"
Haskellにはどんな型があるのかとか、そういう小難しい話は後回しにして、まずはコードの中に型を明記した例を見てみましょう。
main :: IO ()
main = do
let message = howOldAreYou "田村ゆかり" 17
putStrLn message
howOldAreYou :: String -> Int -> String
howOldAreYou name age = nameSan ++ ageSai
where
nameSan = name ++ "さん"
ageSai = show age ++ "歳"
このコードはちゃんとコンパイルも通って実行できます。2
$ runghc withTypes.hs
田村ゆかりさん17歳
もっと詳しく親の仇のようにしつこく型を書くこともできます。
main :: IO ()
main = do
let
message :: String
message = howOldAreYou "田村ゆかり" 17 :: String
putStrLn (message :: String) :: IO ()
howOldAreYou :: String -> Int -> String
howOldAreYou name age = nameSan ++ ageSai :: String
where
nameSan :: String
nameSan = name ++ ("さん" :: String) :: String
ageSai :: String
ageSai = show age ++ ("歳" :: String) :: String
なにか血の気が引くコードですね。ここまでがっつり明記しても実用性はあまりありません。病院に行きましょう。
Haskellでは、このように型を明記することもできますが、
型を書かなくてもコンパイラちゃんが頑張ってあなたの気持ちを読み取ってくれます。
裏側で型を推論する「型推論」をしています。
なぜ型を書くのか
さて、型を明記できるのはわかりました。
でも、ここで疑問が浮かびます。
「なぜなくてもいいものをわざわざ書くの?ばかなの?死ぬの?」
ばかじゃないです。死なないです。
型をコード中に明記することで大きく2つのメリットが得られます。
- コンパイルエラーの理由がわかりやすくなる
- お前の下手なコメントより型の方が上等なドキュメントだ
まずは前者について考えます。
先ほどのコードをすこし変更したこちらのコードをコンパイル(あるいはrunghc
で実行)してみてください。
main = do
let message = howOldAreYou "田村ゆかり" 17
putStrLn message
howOldAreYou name age = nameSan ++ ageSai
where
nameSan = name ++ "さん"
ageSai = age ++ "歳"
エラーがでます。
No instance for (Num [Char]) arising from the literal `17'
Possible fix: add an instance declaration for (Num [Char])
日本語に訳すと
はうぅ〜、お兄ちゃん、文字列は数字じゃないよ〜
`17'ってところ見てみて☆
と言っているみたいです。
とてもあほかわいいですね。文字列が数字じゃないのは当たり前じゃないですか。
17
ってところを見てみても、もともと意図している通り数字で年齢を渡しているだけです。
では、今度は型を明記してみましょう。
main = do
let message = howOldAreYou "田村ゆかり" 17
putStrLn message
howOldAreYou :: String -> Int -> String
howOldAreYou name age = nameSan ++ ageSai
where
nameSan = name ++ "さん"
ageSai = age ++ "歳"
細かいことは後まわしにしますが、
お兄ちゃんはね、
howOldAreYou
関数に文字列と整数をあげて、その結果として文字列がほしいんだよ
とコンパイラに手取り足取り教えています。
なんだか性的倒錯していますね。
ではこの型を加えたコードをコンパイルするとどうなるでしょうか。
もちろん、型を明記しただけなのでエラーが出るのは変わりません。
でも、エラーメッセージはさっきと違います。
Couldn't match expected type `[Char]' with actual type `Int'
In the first argument of `(++)', namely `age'
はうぅ〜、お兄ちゃん、`++'は文字列がほしいのに`age'は整数だよ?
今度はコンパイラちゃんが何言っているかわかります。
そう、文字列を連結する関数(2項演算子)の++
に対して、整数を与えてしまっています。
ごめんね、お兄ちゃん、整数を文字列に変換する
show
を忘れてたよ...
これでこの兄妹はその後も幸せに暮らしていくことができます。
幸せとはひとそれぞれなので、みんなで暖かく見守っていきましょう。
このように、コンパイラちゃんに「この関数はなにをしようとしているのか」を教えてあげることで、コンパイラちゃんもそれにあったお返事をくれます。
「あいつマジでつかえねぇ」って愚痴ってるけど、それお前のコミュニケーション不足だようんこ
みたいなことってありますよね?
日頃からHaskellを使ってコンパイラちゃん相手に意思疎通の練習をしていれば、そういううんこにならない効果もあるんです!
ぜんぜんこわくないどころか、まっとうな人間になれる効果があるなんてやっぱりHaskellはすごいですね。
あの会社の人事の人たちもHaskell勉強すればいいのに。
もちろん、型を明記するメリットの2つ目にあったように、数日後の自分や初めてこのコードを読む他の人にとっても、下手くそなコメントを書くよりずっと上等なメッセージになります。
このように、なぜ型を書いた方がいいのかわかると
よし、お兄ちゃん型のお勉強しちゃうぞ〜
とやる気になりますね!
型の書き方
ではお待ちかね、型の書き方を見てみましょう。
-- [関数の型の書き方]
-- 引数を好きなだけ並べて最後に返値の型
-- 全部矢印でつなげる
関数名 :: 最初の引数の型 -> 次の引数の型 -> 返値の型
関数の定義
-- 値に型を明記する
-- :: をつけて型を明記する
(値 :: 値の型)
-- = 以降の部分の型を示す
なんちゃらかんちゃら = 式 :: 式の部分の型
親の仇のように型を明記したコードの中の実例と比べてみてください。
-- howOldAreYou の型はStringとIntをもらってStringを返す
howOldAreYou :: String -> Int -> String
-- 以下howOldAreYouの定義
howOldAreYou name age = nameSan ++ ageSai :: String
where
nameSan :: String -- nameSanの型はString
-- "さん"の型はString
-- カッコを省略すると`=`以降の型を表す
-- name ++ "さん" の型はString
nameSan = name ++ ("さん" :: String) :: String
ageSai :: String
ageSai = show age ++ ("歳" :: String) :: String
ね、簡単でしょ?
基本の型
Haskellの型の中からよく使うものをピックアップしました。
Char
文字です。
a
とかあ
とかみたいな1文字のCharacterです。
新しくChar
型の値を作るときはシングルクォート'
で囲みます。
main = do
-- もちろん `:: Char`は書かなくても動く。
print ('a' :: Char)
String
文字列です。
実態は文字のリストです。
main = do
-- abcd と表示される
-- Char型の値のリストがString
putStrLn (['a'..'d'] :: String)
新しく文字列の値を作るときにはダブルクォートで囲みます。
main = do
-- abcd と表示される
putStrLn ("abcd" :: String)
Int
整数です。
世の中にありがちなように、32bitとか64bitとかで表現できる上限までしか表現できません。
ghci(対話環境)で試してみましょう。
Prelude> (1000000000000000000 :: Int)
1000000000000000000
Prelude> (10000000000000000000 :: Int)
-8446744073709551616
環境によって結果は変わりますが、下の例はInt
の上限値を超えてしまったため、正しく値が評価されていません。
Integer
先ほどのサンプルプログラムの田村ゆかり3さんの年齢のように、Int
の範囲でおさまる整数の場合は問題ありませんが、もっと大きな整数をあつかうにはどうしたらいいでしょうか。
なんとHaskellには、(メモリとかリソースが許す限り)無限に大きい整数をあつかうことができる型Integer
が用意されています。
Int
のときと同じようにghciでためしてみましょう。
Prelude> (1000000000000000000 :: Integer)
1000000000000000000
Prelude> (10000000000000000000 :: Integer)
10000000000000000000
Haskellまじかっけー。
Float
, Double
浮動小数点型です。
Float
が単精度、Double
が倍精度とか呼ばれるあれです。
もちろん、丸め誤差とかの影響を受けない厳密な数値をあつかいたいこともありますよね?
そうしないと古代言語COBOLからHaskellに移行できないですもんね。
その実現方法の1つであるRatio
についてはちょっと特殊なのであとでお勉強します。
Bool
真偽値です。
if
とかの条件に使えるあれですね。
ちなみに世の中の一部の言語で可能な、
if
の条件に整数型とかを与えてもちゃんと動く
みたいなことはありえません。
それが原因でバグを引き起こす人が多いですから。
新しくBool
型の値を使うときはTrue
かFalse
を使います。
main = do
-- もちろん、ふつうはifを使う
case (3 > 4) of
True -> do
putStrLn "そうだよ"
False -> do
putStrLn "ちがうよ"
リストふたたび
これまで配列みたいに使ってきたリストにももちろん型は存在します。
たとえばInt
のリストの型は[Int]
ですし、Char
のリストの型は[Char]
です。
文字列の実態がChar
のリストだったことも思い出せば、こういうコードを書くこともできます。
main = do
print ([23, 45, 56] :: [Int])
print ("hi" :: String)
-- String と [Char]は同じもの
print ("hi" :: [Char])
でもリストの型はBool
とかDouble
とかとは少し様子が異なります。
他の型(リストの中身の型)を元にして作られています。
厳密には「代数的データ型(Algebraic data type)」と呼ばれるものですが、深追いすると長くなるのでまた今度です。
タプル
リストのように特殊な型にタプルがあります。
リストで遊んでいて、こんなコードを書いてコンパイラちゃんに怒られませんでしたか?
-- コンパイラちゃんエラー
main = do
print [23, "foo", True]
CとかJavaとかみたいな言語を使っている人はまずやらないと思いますが、ふだんJSをおさわりしているいやらしい人はやっちゃうかもしれません。
リストは中身の要素がすべて同じ型でないといけません。
もしも型が異なる値の集合みたいなものをあつかいたいならタプル(Tuple)を使いましょう。
使い方は簡単、リストの[]
が()
になっただけです。
main = do
print (23, "foo", True)
型は中身の要素の型に応じて決まります。
main = do
print ((23, "foo", True) :: (Int, String, Bool))
このタプルのおかげで、複数の値を返す関数が気軽に書けるようになりました。
main = do
strA <- getLine
strB <- getLine
let
a = read strA :: Int
b = read strB :: Int
(d, m) = intDivMod a b
putStrLn ("商は" ++ show d)
putStrLn ("あまりは" ++ show m)
-- | 商とあまりを返す関数
intDivMod :: Int -> Int -> (Int, Int)
intDivMod a b = (div a b, mod a b)
IO
最後にIO なんちゃら
という特殊な型を紹介すれば終わりです。
入出力が関わっているときにでてくる型です。
ghciで:t getLine
を実行してみてください。
ghciには:t
のあとに型を調べたい関数名を与えると、その型を教えてくれる便利な機能があります。
Prelude> :t getLine
getLine :: IO String
文字列を返す関数だけど、入出力が関わっているのでそのまま
String
にするのは悔しい!ビクンビクン!
という声が聞こえてきますね。
逆に、IO
がくっついているときは入出力が関わっていることの目印になるので、
この関数は同じ引数を与えても気分で返値がかわるめんどくさいやつだよ
と判断することができて便利です。
型にIO
がくっついている関数は定数 <- 関数
で値を取り出しましょう。
ところで、main
関数の型はIO ()
でした。
main
関数は、特に返値を返さないので、空のタプル()
にIO
をくっつけています。
()
が他の言語でありがちなvoid
と同じようなものだと思っておきましょう。
そのほかの入出力に関わる関数の型も例示しておきます。
-- FilePath は Stringの別名 (意味がわかりやすいでしょ?)
-- ファイルのパスをもらって、入出力が関わったString型の値を返す
readFile :: FilePath -> IO String
-- ファイルのパスと書き出す内容をもらって、入出力が関わった処理を行う
writeFile :: FilePath -> String -> IO ()
putStrLn :: String -> IO ()
型に別名をつける
readFile
やwriteFile
の型を調べると、FilePath
という、実態はString
の謎の型がありました。
これはドキュメントとしての効果を高めるために、型に別名をつける機能を活用したものです。
もちろん自分で型に別名をつけることもできます。
type Age = Int
type Name = String
type Message = String
main = do
let message = howOldAreYou "田村ゆかり" 17
putStrLn message
howOldAreYou :: Name -> Age -> Message
howOldAreYou name age = nameSan ++ ageSai
where
nameSan = name ++ "さん"
ageSai = show age ++ "歳"
型を見てその関数が何をしようとしているのかわかりやすくなりました。
特にName
もMessage
も実態は同じString
でまぎらわしかったのがすっきりしたことに注目してください。
実際に型に別名をつけるには
type 別名 = 実態の型
をコードのどこか好きなところに書いておくだけです。
実はString
もFilePath
も
type String = [Char]
type FilePath = String
と定義されています。
型変数
あなたが実験大好き人間なら、ここを読む前にprint
関数の型を調べたはずです。
print :: Show a => a -> IO ()
見たことない太い矢印=>
と、謎の小文字a
があります。
このa
は「なんか適切な型」を意味していて、型変数と呼ばれます。
print
はString
でもInt
でも[Bool]
でも、引数をとにかく標準出力に表示してくれる融通のきく便利なやつでした。
この「型はなんでもいいよ」というのを示すのに使うのが型変数です。
a
以外にも任意の小文字のアルファベットの文字列が使われます。
b
でもunko
でもOKです。
さて、print
はなんでも表示してくれると言いましたが、正しくは
なんでもは表示しないわよ。表示できるものだけ。
です。
その「表示できるものだけ」という制約をa
に課すのが
Show a =>
です。
つまり、
print :: Show a => a -> IO ()
は
printは表示できる型の値をなんか引数にして、入出力の処理をする
という意味になります。
その他よくある制約は以下のとおりです。
-
Show a
: 表示できるものだけ -
Read a
: 文字列から変換できるものだけ -
Eq a
: 等しいか比較できるものだけ -
Ord a
: 大きさの順序を比べられるものだけ -
Num a
: 数字っぽいものだけ -
Integral a
: 整数っぽいものだけ
ghciで
:t read
, :t div
, :t (/)
, :t (>)
, :t (==)
あたりを実行して型を見てみると面白いです。
(2項演算子は()
で囲まないとエラーになります)
Hoogle
いままで、何か標準入力から値を読み取って、String
以外の型の値として使うとき、
main = do
strA <- getLine
let a = read strA :: Int
print (a + 3)
みたいにgetLine
の結果を仮の定数にいれたあとにread
で型を変換していました。
ちょっと面倒ですよね?
直接Int
型とかBool
型の値として読み取れたら便利なのに。
そんなあなたのために、それを可能にする関数を探してみましょう。
まず、ほしい関数の型を考えます。
getLine
とread
の型は
getLine :: IO String
read :: Read a => String -> a
です。
すると、今ほしい関数は
Read a => IO a
文字列から変換できる任意の型で、しかも入出力が関わっている型です。
ほしい関数の型を考えたら、hoogleの検索窓にその型を入力しましょう。
"Search"をクリックすると一番最初に
readLn :: Read a => IO a
がでてきました。
説明を読んでみると、どうやら我々が求めていたもののようです。
なんと、Haskellでは「こんな型の関数がほしいなぁ」と思ったら、その関数を検索して簡単に見つけることができるのです。
こわいどころか甘々ですね。ちょろい。
intDivMod
の改良
最後に「タプル」の項で提示したintDivMod
のコードを改善して終わりにします。
改善ポイントは2つ。
- 標準入力を任意の型として読める
readLn :: Read a => IO a
を使う -
intDivMod
をInt
以外にInteger
にも使えるようにする
main = do
-- 1行標準入力から読み取って任意の型(今回はInteger)に変換する
a <- readLn :: IO Integer
b <- readLn :: IO Integer
let (d, m) = intDivMod a b
putStrLn ("商は" ++ show d)
putStrLn ("あまりは" ++ show m)
-- | 商とあまりを返す関数
intDivMod :: Integral n => n -> n -> (n, n)
intDivMod a b = (div a b, mod a b)
コンパイラちゃんにも人にもやさしいコードが簡単に書けました。
宣伝
ぼくと契約してHaskellを書こうよ。
ARoW
完全週休4日、標準労働時間5時間(残業禁止)副業OKです。
正社員でもアルバイトでも好きにしてください。
次回
相変わらず需要がどれだけあるのかわからないですが、次回があるとしたら以下の内容をお届けします。
もっと便利な関数をつくろう
- 高階関数
- 代数的データ型
もっと書きやすく
$
- 中置記法
- ラムダ式
- カリー化
- パターンマッチング
- ガード
forのその先へ
- map
- fold
- unfold
コードをラクして改善する
- hlint
- doctest, quickcheck
さらに進んで
- 例外処理
- 型クラス