[入門]関数プログラミング―質の高いコードをすばやく直感的に書ける!

第2章 関数プログラミングのパラダイム―命令プログラミングと何が違うのか

この記事を読むのに必要な時間:およそ 5.5 分

関数プログラミングの特徴:高いエラー検出率

ここで高いエラー検出率について説明しましょう。そのためには,どうしても型の表記について説明する必要があります。

関数の型を書く方法

Haskellでは,関数や変数の型を,定義の上の行に書きます。例として,次のコードを見てください。

isDigit :: Char -> Bool
isDigit c = c >= '0' && c <= '9'

isDigitは引数に文字を取り,真理値を返す関数です。文字が数字の場合は真を,そうでなければ偽を返します。文字の型はChar,真理値の型はBoolと表記します。このように,Haskellでは(具体的な)型の名前は大文字で始めます。::「の型は」という意味です。->「左の型の値を取って右の型の値を返す」ことを意味します。isDigitの定義のほうは,特に問題なく理解できるでしょう。

このコードをたとえばchar.hsというファイルとして保存し,GHCiに読み込ませて使ってみましょう。

% ghci char.hs
> isDigit '0'
True
> isDigit 'a'
False

Haskellでは真がTrue,偽がFalseと表記されます。

複数の引数を取る関数の型

isDigitは一引数の関数でした。次に二引数の関数の例として,makeStringを見てみましょう。

makeString :: Char -> Int -> [Char]
makeString c 0 = []
makeString c n = c : makeString c (n - 1)

makeStringは第一引数に文字(Char⁠⁠,第二引数に整数(Int)を取り,文字のリスト([Char])を返します。型においても角括弧はリストを意味します。この例からわかるように,N個の引数を取る場合は,->をN個使って表現します。makeStringはどういう文字のリストを返すのか実際に使ってみましょう。上記のコードをファイルstring.hsとして保存し,対話環境に読み込ませてから次のように入力してください。

> makeString 'w' 5
"wwwww"

文字列が返ってきました。このようにHaskellでは,文字列は文字のリストとして実現されています。たとえば,文字列"wwwww"は['w','w','w','w','w']の別表現です。makeStringは,与えられた文字を与えられた数だけ列挙したリストを返す関数だとわかりました。

型変数

たとえば,リストの長さを返す関数を考えましょう。もし,リストにある要素の長さを測る関数を型ごとに定義しないといけないとしたらとても面倒です。関数型言語では,型変数を使って要素の型をパラメータ化することで,要素の型に依存しない関数を定義できます。次は基本関数lengthの型です。

length :: [a] -> Int

aが型変数です。このようにHaskellでは型変数を小文字で書きます注2⁠。型変数aは,具体的な型がなんであろうとも,lengthの計算には影響しないことを表しています。具体的な型が必要であれば,それはコンパイル時に推論されます。実際にlengthを使ってみましょう。

> length "Hello"
5
> length [1..10]
10
注2)
具体的な型は大文字から始めるのと対照的です。

高階関数の型

高階関数mapの型は次の通りです。

map :: (a -> b) -> [a] -> [b]

第一引数に「a->b」という型を持つ関数,第二引数に[a]という型のリストを取り,[b]という型のリストを返します。先ほど作成したchar.hsを対話環境に読み込ませた状態で使ってみましょう。

> map isDigit "H2O"
[False,True,False]

isDigitの型は「Char->Bool」でした。よって,aがChar,bがBoolになります。このことから,第二引数の型は[Char],結果の型は[Bool]に決まります。

型推論と型検査

先ほどHaskellで実装したcalcに型を書いてみましょう。

calc :: [Int] -> Int
calc = foldl (+) 0 . map mul . zip [0..]

そして,このcalcをコンパイルするときに何が起きるか考えてみます。コンパイラがわかるのは,プログラマが指示したcalcの型と基本関数の型です。

たとえば,⁠foldl(+)0」の部分について考えてみましょう。foldlの型は次の通りです。

foldl :: (a -> b -> a) -> a -> [b] -> a

foldlの結果がcalcの結果になるわけですから,aはIntになります。また,演算子+の型は「a->a->a」なので,foldlのaとbは同じ型になります。よって,foldlの具体的な型は次のように推論されます。

foldl :: (Int -> Int -> Int) -> Int -> [Int] -> Int

ここで,第二引数まで部分適用された「foldl(+)0」の型は,次のようになります。

foldl (+) 0 :: [Int] -> Int

同様に,その他の部品の型は,次のように推論されます。

map mul :: [(Int,Int)] -> [Int]
zip [0..] :: [Int] -> [(Int,Int)]

中にカンマのある丸括弧は,組の型です。最後のピースである,関数結合の演算子「.」の型を見てみましょう(演算子の型を書く場合は,演算子を丸括弧で囲む必要があります⁠⁠。

(.) :: (b -> c) -> (a -> b) -> a -> c

この型を字面から理解するのは難しいので,もう一度図1を見てください。演算子「.」は,部品を並べてつなげる役割を果たしています。演算子「.」が要求するのは,ある部品の出力の型が次の部品の入力の型と一致することです。calcの場合は,一致していることが図1から読み取れると思います。どこか1つでも型の矛盾があれば,コンパイル時にエラーとなります。

エラー検出の例

具体的に,エラーが報告される例を見てみましょう。かなり恣意(しい)的な例で恐縮ですが,calcの実装を変えてみます。図1で示されているように,calcは3つの部品から構成されていました。ここで,真ん中の部品であるmap mulを取り除いてみます。

calcErr.hsというファイルに次のコードを書き込んでください。

mul (i,x) = x * i

calc :: [Int] -> Int
calc xs = foldl (+) 0 . zip [0..]

GHCiに読み込ませてみます図2⁠。

図2 エラー検出の例

% ghci calcErr.hs

calcErr.hs:4:11:
    Couldn't match expected type `Int' with actual type `a0 -> c0'
    In the expression: foldl (+) 0 . zip [0 .. ]
    In an equation for `calc': calc xs = foldl (+) 0 . zip [0 .. ]

エラーの意味はわからなくてかまいません。エラーを発見できたことがわかれば十分です。

zip [0..]の出力の型は[(Int,Int)]であり,foldl(+)0の入力の型は[Int]ですから型が合いません。このエラーを発見できたのです。エラーが見つけられたのは,プログラムをすべて式で構成したからです。

このように関数型言語では,式と式との型の関係が厳密に検査されます。このおかげで静的型付きの関数型言語では,たくさんのエラーをコンパイル時に発見でき,コンパイルに通ればおおむね意図通りに動く感覚が味わえます。もちろん,型に関するエラーがないだけで,値に関するエラーは残る可能性はあるので,テストをおろそかにしてはいけません。

著者プロフィール

山本和彦(やまもとかずひこ)

株式会社IIJイノベーションインスティテュート(IIJ-II)技術研究所 主幹研究員。

開発した代表的なオープンソフトにMew,Firemacs,Mightyがある。『プログラミングHaskell』や『Haskellによる並列・並行プログラミング』の翻訳者。職場ではHaskell,家庭では3人の子供と格闘する日々を送っている。

web:http://www.mew.org/~kazu/

twitter:@kazu_yamamoto