M.Hiroi's Home Page
http://www.geocities.jp/m_hiroi/

Functional Programming

お気楽 Haskell プログラミング入門

[ PrevPage | Haskell | NextPage ]

Haskell の基礎知識

●使ってみよう

それでは、さっそく Haskell を使ってみましょう。本ページではインタプリタ ghci を使って Haskell を勉強していきます。Windows の場合、コマンドプロンプトで ghci を実行するか、スタートアップメニューから WinGHCi を選択してください。

C>ghci
GHCi, version 7.4.1: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude>

Prelude> は Haskell (ghci) のプロンプトです。終了する場合は :quit (:q) と入力してください。Windows の場合、Ctrl-D (Ctrl キーを押しながら D を押す)を入力しても終了します。プロンプトのあとに式を入力すると、Haskell は式を評価して結果を返します。

Prelude> 1 + 2 * 3
7
Prelude> (1 + 2) * 3
9
Prelude> -3 * 4
-12
Prelude> 4 * (-3)
-12
Prelude> 5 `div` 2
2
Prelude> 5 / 2
2.5
Prelude> 10 / 3
3.3333333333333335

Haskell の場合、4 * -3 はエラーになります。4 * (-3) としてください。

●整数と実数

データの種類や種別のことを「データ型」、またはたんに「型 (type) 」といいます。Haskell でよく使われる数値のデータ型を示します。

Int     : 固定長整数
Integer : 多倍長整数 (桁数に制限なし)
Float   : IEEE 単精度浮動小数点数
Double  : IEEE 倍精度浮動小数点数

通常、データ型の名前は英大文字から始まりますが、記号から始まるデータ型の名前もあります。

簡単な例を示しましょう。

Prelude> 2147483647 :: Int
2147483647
Prelude> 2147483647 + 1 :: Int
-2147483648
Prelude> -2147483648 :: Int
-2147483648
Prelude> -2147483648 - 1 :: Int
2147483647
Prelude> 11111111 * 11111111 :: Int
-2047269199
Prelude> 11111111 * 11111111 :: Integer
123456787654321
Prelude> 1 / 9 :: Float
0.11111111
Prelude> 1 / 9 :: Double
0.1111111111111111

記号 :: はデータ型を指定するために用います。SML/NJ や OCaml と違って :: はコンス演算子ではありません。ご注意くださいませ。

M.Hiroi がダウンロードした Windows 版 GHC (ver 7.3.1) の場合、Int の範囲は -2147483648 (231) から 2147483647 (231 - 1) まででした。通常、整数は 10 進数で表しますが、先頭に 0o または 0O を付けると 8 進数、0x または 0X を付けると 16 進数で表すことができます。

Haskell のデフォルトの設定では、整数を Integer で、実数を Double で扱います。Int や Float を使いたい場合は型を指定する必要があります。このほかに、有理数 (分数) や複素数を扱うライブラリ (モジュール) も用意されています。

●算術演算子

ここで、よく使われる算術演算子をまとめておきましょう。

+, -, * は整数と実数どちらにも適用することができます。Haskell は型を厳密にチェックするプログラミング言語なので、異なる型を混在させて計算することはできません。Haskell の場合、div と mod は関数として定義されています。関数を二項演算子として使う場合はバッククォート (`) で囲んでください。

簡単な例を示します。

Prelude> 1 + 2 + 3 - 4
2
Prelude> 1.234 + 5.678
6.912
Prelude> 3 `div` 2
1
Prelude> 3.0 / 2.0
1.5
Prelude> 3 / 2
1.5
Prelude> 3 / (2 :: Integer)
=> エラー
Prelude> 3 div 2.0
=> エラー

ghci の対話モードで 3 / 2 を入力すると、ghci は 3 と 2 を実数 (Double) と解釈して計算します。2 の型を Integer に指定するとエラーになります。同様に 3 div 2.0 も 2.0 は Double と解釈されるのでエラーになります。

●文字と文字列

一つの文字を表すデータを文字型 (Char) といいます。Haskell の場合、文字はユニコードで表され、'a' のように引用符 ' で囲んで表します。' を表す場合はエスケープシーケンス ( \ ) を使います。

Prelude> 'a'
'a'
Prelude> '\''
'\''
Prelude> '\\'
'\\'

文字列 (String) は "foo" や "bar" のように二重引用符 ( " ) で囲みます。C言語と同様にエスケープシーケンスを使うことできます。たとえば、\n が改行で \t がタブになります。

Prelude> "foo"
"foo"
Prelude> "bar"
"bar"
Prelude> "foo" ++ "bar"
"foobar"

文字列は演算子 ++ で連結することができます。なお、Haskell の文字列は「文字を格納したリスト」のことです。リストはあとで詳しく説明します。

●比較演算子

比較演算子は =, /=, <, >, <=, >= があります。値が等しいかチェックする述語が == で、等しくないかチェックする述語が /= です。

簡単な例を示しましょう。

Prelude> 1 == 1
True
Prelude> 1 == 2
False
Prelude> 1 /= 2
True
Prelude> 1 < 2
True
Prelude> 1 > 2
False
Prelude> 'a' == 'a'
True
Prelude> 'a' == 'A'
False
Prelude> 'a' < 'b'
True
Prelude> "foo" == "bar"
False
Prelude> "foo" == "foo"
True

Haskell は真・偽を型 Bool で表します。True が真で False が偽になります。比較演算子は整数や実数だけではなく、文字や文字列にも適用することができます。

●論理演算子

Haskell には not, &&, || という論理演算子があります。

簡単な例を示します。

Prelude> 1 < 2 && 3 < 4
True
Prelude> 1 < 2 && 3 > 4
False
Prelude> 1 > 2 || 3 > 4
False
Prelude> 1 > 2 || 3 < 4
True
Prelude> not True
False
Prelude> not False
True

●条件分岐

条件分岐は if-then-else を使います。if E then F else G は最初に E を評価して、結果が真 (True) であれば式 F を評価し、偽 (False) であれば式 G を評価します。式 F または式 G の評価結果が if の返り値になります。式 F と G の返り値はどんな型でもかまいませんが、同じ型でなければいけません。型が違うとエラーになります。

簡単な例を示しましょう。

Prelude> if 1 < 2 then 1 + 2 else 1 - 2
3
Prelude> if 1 > 2 then 1 + 2 else 1 - 2
-1

Haskell の場合、if-then-else の else は省略することができません。ご注意ください。

●変数

Haskell の「変数 (variable) 」は記号 = を使って定義します。

名前 = 式

ただし、インタプリタ ghci の対話モードで変数を定義する場合は let を使います。

let 名前 = 式

プログラムをファイルに書いて読み込む場合、let は不要になります。let はあとで説明する局所変数の定義で用います。

関数型言語の場合、変数に値を割り当てることを「束縛 (binding) 」といいます。純粋な関数型言語の場合、束縛された変数は値を書き換えることができません。手続き型言語は代入により変数の値を書き換えることができますが、純粋な関数型言語に代入操作はありません。ちなみに、Lisp / Scheme は不純な関数型言語なので、変数の値を書き換えることができます。

名前(識別子)は、先頭が英小文字またはアンダースコア ( _ ) で、そのあとに英大文字、英小文字、数字、アポストロフィ ( ' )、アンダースコアが続きます。英大文字で始まる名前やアポストロフィから始まる名前は使えません。ご注意ください。また、Haskell は英大文字と英小文字を区別するので、たとえば foo と fOO は異なる名前になります。

簡単な例を示しましょう。

Prelude> let a = 10
Prelude> a
10
Prelude> let b = 20
Prelude> b
20
Prelude> let c = 1.2345
Prelude> c
1.2345
Prelude> let d = "hello, world"
Prelude> d
"hello, world"

対話モードの場合、変数名を入力するとその値が表示されます。また、同じ名前の変数を再定義することもできます。

Prelude> a
10
Prelude> let a = "foo"
Prelude> a
"foo"

トップレベルで変数を再定義すると、元の変数は隠蔽されて値を参照することができなくなります。値を書き換えたわけではありません。たとえば、変数 a (10) を参照しているプログラムがある場合、トップレベルで a の値を "foo" に書き換えたとしても、そのプログラムが参照している変数 a の値は 10 のままです。

●タプル (tuple)

Haskell は複数のデータ型を組み合わせて新しいデータ型を定義することができます。新しいデータ型の定義方法はいくつかあるのですが、もっとも簡単な方法が「タプル (tuple) 」です。タプルは複数のデータや式をカンマ ( , ) で区切り、カッコ ( ) で囲んで表します。次の例を見てください。

Prelude> let a = (1, 2)
Prelude> a
(1,2)
Prelude> :t a
a :: (Integer, Integer)
Prelude> let b = (10, 1.2345)
Prelude> b
(10,1.2345)
Prelude> :t b
b :: (Integer, Double)
Prelude> let c = (1, 2.5, 'a')
Prelude> :t c
c :: (Integer, Double, Char)
Prelude> (1 * 2 + 3, 4 * 5 * 6)
(5,120)

:type (または :t) はデータ型を調べる ghci のコマンドです。変数 a のタプル (1, 2) は整数を 2 つ持っていて、データ型は (Integer, Integer) になります。変数 b のタプル (10, 20.5) は整数と実数なので (Integer, Double) になります。変数 c のタプル (1, 2.5, 'a') は (Integer, Double, Char) になります。また、最後の例のようにカッコの中に式を書くと、それを評価した値がタプルの要素になります。

タプルは入れ子にしてもかまいません。次の例を見てください。

Prelude> let a = ((1, 2), 3)
Prelude> a
((1,2),3)
Prelude> :t a
a :: ((Integer, Integer), Integer)
Prelude> let b = (1, (2, 3))
Prelude> b
(1,(2,3))
Prelude> :t b
b :: (Integer, (Integer, Integer))

変数 a のタプルは、第 1 要素が (Integer, Integer) のタプルで、第 2 要素が Integer です。変数 b のタプルは、第 1 要素が Integer で第 2 要素が (Integer, Integer) のタプルになります。どちらのタプルも 3 つの整数が含まれていますが、データ型は異なることに注意してください。

タプルから要素を取り出すには、「パターンマッチング (pattern matching) 」という機能を使うと簡単です。次の例を見てください。

Prelude> let (a, b) = (1, 2)
Prelude> a
1
Prelude> b
2
Prelude> let (a, b) = ((1, 2), 3)
Prelude> a
(1,2)
Prelude> b
3
Prelude> let ((c, d), e) = ((1, 2), 3)
Prelude> c
1
Prelude> d
2
Prelude> e
3

記号 = の左辺 (a, b) がパターンを表します。要素が 2 つ並んでいるので、2 要素のタプルを表すパターンになります。パターン (a, b) と左辺の (1, 2) を照合して、変数部分に対応する要素を取り出します。そして、変数をその値に束縛します。次の例のように、(a, b) と ((1, 2), 3) を照合すると、a は (1, 2) になり、b は 3 になります。

パターンは入れ子にしてもかまいません。((c, d), e) と ((1, 2), 3) を照合すると、c = 1, d = 2, e = 3 となります。このように、パターンを使ってタプルの要素を取り出すことができます。ただし、データ型が違うと照合に失敗してエラーになるので注意してください。

また、パターンマッチングのほかにタプル (a, b) の第 1 要素 a を取り出す関数 fst と第 2 要素を取り出す関数 snd があります。

簡単な例を示しましょう。

Prelude> fst (1, 2)
1
Prelude> snd (1, 2)
2
Prelude> fst ("foo", "bar")
"foo"
Prelude> snd ("foo", "bar")
"bar"

●リスト (list)

もうひとつ、とても重要なデータに「リスト (list) 」があります。Haskell のリストは Lisp のリスト(連結リスト)と同じで、複数のデータを格納することができます。ただし、Lisp のリストとは違って、リストの要素は同じデータ型でなければいけません。リストの構造を図で表すと次のようになります。

 ┌─┬─┐    ┌─┬─┐    ┌─┬─┐
 │・│・┼─→│・│・┼─→│・│/│  終端は/で表す 
 └┼┴─┘    └┼┴─┘    └┼┴─┘
   ↓            ↓            ↓
   1            2            3

        図 : リスト内部の構造

リストは貨物列車にたとえるとわかりやすいでしょう。車両に相当するものを「コンスセル (cons cell) 」といいます。貨物列車には多数の車両が接続されて運行されるように、リストは複数のコンスセルを接続して構成されます。1 つのコンスセルには、貨物(データ)を格納する場所と、連結器に相当する場所があります。

上図では、コンスセルを箱で表しています。コンスセルの左側がデータを格納する場所で、右側が次のコンスセルと連結しています。この例では、3 つのコンスセルが接続されています。それから、最後尾のコンスセルには、リストの終わりを示すデータが格納されます。

Haskell のリストは、要素をカンマ ( , ) で区切り、角カッコ [ ] で囲んで表します。次の例を見てください。

Prelude> let a = [1,2,3,4]
Prelude> a
[1,2,3,4]
Prelude> :t a
a :: [Integer]
Prelude> let b = ["foo", "bar", "baz"]
Prelude> b
["foo","bar","baz"]
Prelude> :t b
b :: [[Char]]
Prelude> let c = [(1,2), (3,4), (5,6)]
Prelude> c
[(1,2),(3,4),(5,6)]
Prelude> :t c
c :: [(Integer, Integer)]
Prelude> let d = [[1],[2,3],[4,5,6]]
Prelude> d
[[1],[2,3],[4,5,6]]
Prelude> :t d
d :: [[Integer]]
Prelude> let e = [1+2+3, 4*5*6]
Prelude> e
[6,120]
Prelude> :t e
e :: [Integer]

リストのデータ型は "[ 要素のデータ型 ]" で表されます。変数 a のリストは要素が Integer なのでデータ型は [Integer] になります。リストに格納された要素の個数を「リストの長さ」といいます。リストのデータ型はリストの長さとは関係なく、格納する要素の型によって決まります。[1] や [2, 3] もデータ型は [Integer] になります。

文字列は文字 (Char) を格納したリストなので、データ型は [Char] となります。なお、[Char] には String という別の名前もあります。変数 b のリストは要素が文字列なので型は [[Char]] になります。これは [String] と書くこともできます。

タプルをリストに入れてもかまいません。変数 c はタプル (Integer,Integer) を格納するリストなので、データ型は [(Integer,Integer)] になります。このリストに (1, 2, 3) というタプルを入れることはできません。(1, 2, 3) のデータ型は (Integer,Integer,Integer) で、(Integer,Integer) とはデータ型が異なるからです。

リストは入れ子にすることができます。変数 d のリストは [1], [2, 3], [4, 5, 6] という [Integer] を格納しています。したがって、データ型は [[Integer]] になります。このリストに [[7]] を入れることはできません。[[7]] のデータ型は [[Integer]] になるので、要素のデータ型 [Integer] とは異なるからです。また、最後の例のように角カッコの中に式を書くと、それを評価した値がリストの要素になります。

リストは関数 head, taill を使って分解し、演算子 : (コンス演算子) で合成することができます。また、演算子 ++ でリストを連結することができます。次の例を見てください。

Prelude> a
[1,2,3,4]
Prelude> head a
1
Prelude> tail a
[2,3,4]
Prelude> let e = 0 : a
Prelude> e
[0,1,2,3,4]
Prelude> let f = [1,2,3] ++ [4,5,6]
Prelude> f
[1,2,3,4,5,6]

関数 head a は Lisp の関数 car と同じで、リスト a の先頭要素を取り出します。リスト [1, 2, 3, 4] の先頭要素は 1 なので、head a は 1 を返します。tail a は Lisp の関数 cdr と同じで、リスト a から先頭要素を取り除いたリストを返します。tail a は [1, 2, 3, 4] から 1 を取り除いた [2, 3, 4] を返します。演算子 : は Lisp の関数 cons と同じで、リストの先頭にデータを付け加えます。演算子 ++ は Lisp の関数 append と同じで、2 つのリストをつないだリストを返します。

head, tail, コンス演算子の関係を図に表すと次のようになります。

                      ┌──┐
                ┌─→│head│→ 1  ────┐
                │    └──┘               ↓
                │                        ┌──┐
 [1, 2, 3, 4] ─┤                        │ : │→ [1, 2, 3, 4]  
                │                        └──┘
                │    ┌──┐               ↑
                └─→│tail│→ [2, 3, 4] ─┘
                      └──┘

                図 : リストの分解と合成

この関係は、リストを操作する関数を作る場合の基本になります。

要素のないリストを「空リスト」といって [ ] で表します。次の例を見てください。

Prelude> let xs = tail [1]
Prelude> xs
[]
Prelude> :t xs
xs :: [Integer]
Prelude> let ys = tail["foo"]
Prelude> ys
[]
Prelude> :t ys
ys :: [[Char]]

要素が一つしかないリストに taill を適用すると空リストになります。このように、空リストはリストの終端を表すデータでもあります。[ ] はどのようなリストの型にもあてはまります。最初の例は [Integer] に tail を適用したので、[ ] の型は [Integer] です。2 番目の例のように、[[Char]] の空リスト [ ] の型は [[Char]] です。

コンス演算子を続ける場合は結合規則に注意してください。次の例を見てください。

1 : 2 : 3 : [] => (1 : (2 : (3 : []))) => [1, 2, 3]

このように、コンス演算子は四則演算とは違って「右結合」になります。また、コンス演算子の右辺はリストでなければいけません。1 : 2 はエラーになります。ご注意くださいませ。

実際のプログラムでは、head や tail でリストを分解するよりも「パターンマッチング」を使った方が簡単です。リストのパターンマッチングはあとで詳しく説明します。

●関数

Haskell は関数も記号 = で定義します。

名前 引数 = 式

ただし、インタプリタ ghci の対話モードで関数を定義する場合は、変数と同様に let を使います。

let 名前 引数 = 式

let のあとに名前と引数を書き、= のあとに引数を含む式を書きます。プログラムをファイルに書いて読み込む場合、変数と同様に let は不要になります。たとえば、引数を 2 倍する関数 times2 を定義すると次のようになります。

Prelude> let times2 x = x * 2
Prelude> times2 10
20
Prelude> times2 1.234
2.468

演算子 * は整数と実数どちらにも適用できるので、関数 times2 も整数と実数どちらでも計算することができます。ここで 2 のデータ型を Integer に指定すると、実数の計算はできなくなります。

Prelude> let times2' x = x * 2 :: Integer
Prelude> times2' 10
20
Prelude> times2' 1.23
=> エラー

関数型言語の場合、関数も処理系で取り扱うことができる値 (データ) の一つなのでデータ型があります。Haskell の場合、関数のデータ型は "引数のデータ型 -> 返り値のデータ型" で表します。times2' のデータ型は次のようになります。times2 のデータ型はあとで説明します。

times2' :: Integer -> Integer

あらかじめ関数のデータ型を指定しておくと、2 のデータ型を Integer に指定する必要はなくなります。Haskell の場合、関数のデータ型を指定しなくても型推論によりプログラムは動作しますが、きちんとデータ型を書くことが Haskell の流儀のようです。

ファイルにプログラムを書く場合はこれでよいのですが、ghci の対話モードで関数の型を定義する場合はちょっと面倒です。次の例を見てください。

Prelude> let {times2' :: Integer -> Integer; times2' x = x * 2}
Prelude> :t times2'
times2' :: Integer -> Integer
Prelude> times2' 10
20

let で定義するプログラムを { } で囲って、式をセミコロン ( ; ) で区切ります。ただし、この書き方は途中で改行することはできません。複数行に分けて書きたい場合はコマンド :{ と :} を使います。次の例を見てください。

Prelude> :{
Prelude| let
Prelude| times2'' :: Double -> Double;
Prelude| times2'' x = x * 2
Prelude| :}
Prelude> :t times2''
times2'' :: Double -> Double
Prelude> times2'' 10
20.0

この場合でも式はセミコロンで区切ります。

複数の引数を持つ関数はタプルを使って定義することができます。ただし、Haskell の場合は「カリー化関数」を使うのが普通です。カリー化関数についてはあとで詳しく説明します。

簡単な例を示しましょう。

Prelude> let {f :: (Integer, Integer) -> Integer; f (x, y) = 2 * x + 3 * y}
Prelude> :t f
f :: (Integer, Integer) -> Integer
Prelude> f (1, 2)
8

関数 f はタプル (x, y) を受け取ります。ここで関数 f のデータ型を見てください。引数のデータ型がタプルになっていますね。実をいうと、Haskell の関数は引数を一つしか受け取ることができません。つまり、関数呼び出し f (1, 2) は、タプル (1, 2) に関数 f を適用するという意味なのです。

タプルを使えば複数の値を返す関数も簡単に作ることができます。次の例を見てください。

Prelude> :{
Prelude| let
Prelude| foo :: (Integer,Integer) -> (Integer,Integer);
Prelude| foo(x,y) = if x == y then (0, 0) else if x < y then (-1, y - x) else (1, x - y)
Prelude| :}
Prelude> :t foo
foo :: (Integer, Integer) -> (Integer, Integer)
Prelude> foo(10, 20)
(-1,10)
Prelude> foo(20, 20)
(0,0)
Prelude> foo(40, 20)
(1,20)

関数 foo は引数 x と y の差分の絶対値を計算し、符号とその値を返します。if-then-else は else if でつなぐことができます。x == y ならば (0, 0) を返します。x < y ならば (-1, y - x) を返し、x > y ならば (1, x - y) を返します。このように、タプルを使って複数の値を返すことができます。

●局所変数と大域変数

関数の引数は「局所変数 (local variable) 」として扱われます。局所変数は「有効範囲 (scope : スコープ) 」が決まっています。引数の有効範囲は、関数が定義されている式の中だけです。次の例を見てください。

Prelude> let x = 10
Prelude> x
10
Prelude> let y = 20
Prelude> y
20
Prelude> let bar y = x + y
Prelude> :t bar
bar :: Integer -> Integer
Prelude> bar 100
110

局所変数として定義されていない変数は「大域変数 (global variable) 」になります。大域変数はどこからでも値を参照することができます。対話モードで変数を定義すると、それらの変数は大域変数になります。最初に定義した変数 x と y は大域変数です。

関数 bar は、引数が y で式は x + y です。関数を呼び出す場合、引数用に新しいメモリを割り当てて、そこに与えられた値で引数を束縛します。大域変数 y と引数 y は同じ名前ですが、異なる変数になるのです。そして、局所変数が定義されていれば、その値が参照されます。局所変数が定義されていない場合、大域変数の値が参照されます。したがって、式の中の y は引数 y を参照し、bar の引数に x がないので、式の中の x は大域変数 x を参照します。よって、bar 100 は 10 + 100 = 110 になります。これを図に示すと次のようになります。

┌───── Haskell system ─────┐
│                                    │
│      大域変数  y                   │
│      大域変数  x ←──────┐  │
│                                │  │
│    ┌─ 関数 bar  引数 y ─┐  │  │
│    │                  ↑  │  │  │
│    │            ┌──┘  │  │  │
│    │        x + y         │  │  │
│    │        └──────┼─┘  │
│    └───────────┘      │
│                                    │
└──────────────────┘

        図 : 大域変数と局所変数

関数 bar を実行するとき、関数 bar の枠が作成されると考えてください。このとき、引数用に新しいメモリが割り当てられ、新しい局所変数 y が作成されるわけです。関数の実行が終了すると枠が壊されて、作成された局所変数も廃棄されます。関数 bar の場合、引数 y が廃棄されるので、対話モードでは大域変数 y の値を参照することができます。このように、関数の引数は関数定義されている式の中だけ有効なのです。

●型と型クラス

Haskell は「型」だけではなく「型クラス (type class) 」というものがあります。型クラスはあまり馴染みのない言葉ですが、Haskell でプログラムを作るならば、型クラスを避けて通ることはできません。ここで簡単に説明しておきましょう。

先ほど定義した関数 times2 は整数でも実数でも適用することができましたね。それでは、times2 のデータ型はどうなっているのでしょうか。実際にコマンド :t で型を調べてみましょう。

Prelude> :t times2
times2 :: Num a => a -> a

a -> a の a は「型変数」といい、任意のデータ型を表します。=> の左辺は右辺で出現する型変数の制限を表します。Num a の場合、型 a は Num という型に属していることを表します。Num は数全体を表す型で、このような型を「型クラス (type class) 」といい、=> の左辺 Num a のことを「型クラス制約」といいます。型変数は英小文字から、型と型クラスは英大文字から始めます。

型クラスは型の性質を表したものです。Haskell の場合、型の性質はその型に適用できる関数 (または演算子) と考えてください。たとえば、数には整数や実数がありますが、それらに共通の演算 (性質) を定義することができます。加算 (+), 乗算 (*), 減算 (-) は整数でも実数でも同じ演算子で処理できると便利ですね。除算の場合、一般的には整数同士の除算の結果は整数とし、実数同士の除算の結果は実数とするのが普通です。この場合、ML 系の言語や Haskell では異なる演算子で定義しています。

Haskell の場合、このような性質を型クラスを使って表わすことができます。次の図を見てください。

│←───── 型クラス ─────→│←────── 型 ──────→
│
  数 (Num) ─┬─ 整数 (Integral)  ─┬─ 固定長整数 (Int)
  +,*,-     │   div                └─ 多倍長整数 (Integer)
             │
             └─ 実数 (Fractional)─┬─ 単精度浮動小数点数 (Float)
                  /                  └─ 倍精度浮動小数点数 (Double)

上図は数を表す型クラスと型の関係の一部を抜き出したものです。実際には、多くの型クラスや型が定義されていて、その関係はもっと複雑なものになります。

Haskell の場合、数を表す型クラスが Num です。ここで、演算子 +, *,- を定義しています。基本的には型クラスで定義するのは関数 (演算子) の仕様だけで、関数の実装は具体的な型で行います。整数を表す型クラスが Integral で、整数同士の除算を行う演算子 div が定義されています。Fractional も型クラスで、上図では実数と書きましたが、正確には演算子 / が定義されている型クラスです。

Haskell の型には親子関係があり、親の性質を子が引き継ぐことができます。Integral と Fractional の親は Num で、どちらも Num の性質 (+,-,*) を引き継ぎます。また、Int と Integer の親は Integral なので、Num と Integral の性質 (+,-,*,div) を引き継ぎます。同様に、Float と Double の親は Fractional なので、Num と Fractional の性質 (+,-,*,/) を引き継ぎます。この仕組みは「オブジェクト指向言語」の機能である「継承」とよく似ています。

ただし、Haskell では型クラスと型は役割が異なっていて、具体的なデータに対応するものが「型」で、複数の型に対して共通の仕様 (演算) を定義するものが「型クラス」となります。ある型クラスが他の型クラスの仕様を引き継ぐことはできますが、型は型クラスからしか仕様を引き継ぐことができません。型 A が型クラス B の仕様を引き継いでいることを Haskell では「型 A は型クラス B のインスタンス (instance) である」といいます。たとえば、Integer は Num のインスタンスであり、Integral のインスタンスでもあります。型クラス制約が Num a であれば、a は Num のインスタンスであることを表しています。

簡単な例を示しましょう。引数を 2 で割る関数を定義します。

Prerude> let div2 x = x `div` 2
Prerude> :t div2
div2 :: Integral a => a -> a
Prerude> let div2' x = x / 2
Prerude> :t div2'
div2' :: Fractional a => a -> a

関数 div2 は演算子 `div` を使っているので、Haskell は関数の型を Integral a => a -> a と推論しています。この関数は Integral のインスタンスである型 Integer と Int に適用することができます。関数 div2' は演算子 / を使っているので、Haskell は関数の型を Fractional a => a -> a と推論しています。この関数は Double と Float に適用することができます。

それでは実行してみましょう。

Prerude> div2 10
5
Prerude> div2 1.23456
=> エラー
Prerude> div2' 10
5.0
Prerude> div2' 1.23456
0.61728

演算子 +, *, - の場合、型クラス制約が Num になるので整数と実数どちらにも適用することができますが、div と / の場合は型クラス制約が異なるので、div は整数の除算、/ は実数の除算となります。

型クラスは ML (SML/NJ, OCaml) にはない Haskell の特徴のひとつです。型クラスを使うと、関数 (演算子) の「多重定義 (over loading) 」も可能ですが、型クラスの話はちょっと難しいので、詳しいことはあとで勉強することにしましょう。

-- [修正 (2013/02/03)] --------
型と型クラスに説明を追加
「型 B が型クラス A の性質を引き継ぐことを、型 B は型クラス A のインスタンスといいます」

Copyright (C) 2013 Makoto Hiroi
All rights reserved.

[ PrevPage | Haskell | NextPage ]