Help us understand the problem. What is going on with this article?

こわくないHaskell入門(初級その2)

More than 3 years have passed since last update.

こわくない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にはどんな型があるのかとか、そういう小難しい話は後回しにして、まずはコードの中に型を明記した例を見てみましょう。

withTypes.hs
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つのメリットが得られます。

  1. コンパイルエラーの理由がわかりやすくなる
  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型の値を使うときはTrueFalseを使います。

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 ()

型に別名をつける

readFilewriteFileの型を調べると、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 ++ "歳"

型を見てその関数が何をしようとしているのかわかりやすくなりました。
特にNameMessageも実態は同じStringでまぎらわしかったのがすっきりしたことに注目してください。

実際に型に別名をつけるには

type 別名 = 実態の型

をコードのどこか好きなところに書いておくだけです。

実はStringFilePath

type String = [Char]
type FilePath = String

と定義されています。

型変数

あなたが実験大好き人間なら、ここを読む前にprint関数の型を調べたはずです。

print :: Show a => a -> IO ()

見たことない太い矢印=>と、謎の小文字aがあります。
このaは「なんか適切な型」を意味していて、型変数と呼ばれます。

printStringでも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型の値として読み取れたら便利なのに。

そんなあなたのために、それを可能にする関数を探してみましょう。
まず、ほしい関数の型を考えます。
getLinereadの型は

getLine :: IO String
read :: Read a => String -> a

です。
すると、今ほしい関数は

Read a => IO a

文字列から変換できる任意の型で、しかも入出力が関わっている型です。

ほしい関数の型を考えたら、hoogleの検索窓にその型を入力しましょう。
"Search"をクリックすると一番最初に

readLn :: Read a => IO a

がでてきました。
説明を読んでみると、どうやら我々が求めていたもののようです。

なんと、Haskellでは「こんな型の関数がほしいなぁ」と思ったら、その関数を検索して簡単に見つけることができるのです。
こわいどころか甘々ですね。ちょろい。

intDivModの改良

最後に「タプル」の項で提示したintDivModのコードを改善して終わりにします。

改善ポイントは2つ。

  1. 標準入力を任意の型として読めるreadLn :: Read a => IO aを使う
  2. intDivModInt以外に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

さらに進んで

  • 例外処理
  • 型クラス

  1. 前回ほどアプローチの仕方に新規性がないですが、次回以降のために型は通らなくてはならないものなので、できるだけ肩肘はらずに学べるように心がけました。 

  2. 残念ながら前回紹介した遊べる環境は端末内で日本語を表示することができないのでエラーが吐かれてしまいます。 日本語部分をローマ字に変えてみてください。 どうしても日本語で田村ゆかりさんを見たければ、GHCをご自分のパソコンにインストールしましょう。  

  3. この記事中の田村ゆかりさんはフィクションです。実在の田村ゆかりさんとは関係ありません。 

arowM
ヤギさんとして自由に生きてるよ さくらちゃんはアーティストだから世の理不尽には頭突きしちゃうよ フリーランスUXハッカー・プログラマー(Elm, Haskell)・技術翻訳・ヤギ語翻訳 ARoW代表 http://arow.info /気吹堂(出版)代表/人材紹介会社CXO http://github.com/arow
https://arow.info
arow-oss
ヘテロジニアスで自律分散型な優しい会社です。 従業員がヘテロジニアスな自律分散ノードとして活躍し、代表が汎用ノードとして全体の仕事の調整や、割り振り先がない仕事の処理を担当するボロ雑巾として活躍してます。 フロントエンド側のヘテロジニアスノードがほしいなー
https://arow.info
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away