3. 型入門

関数とデータ型の基本を学びます.どんなプログラミング言語も扱うデータを「型(type)」で区別します.Haskellはコンパイル時に型を指定する静的型付け言語です.

3.1. アトミック型

Haskellには予め多くの型が用意されています.そうした型の中で,これ以上分割できないアトミック型(atomic type)のデータ型は以下の6つです.Haskellの型は必ず大文字で始まリます.

  1. Bool - 真偽値型.この型にはTrueFalseの値の2値のみ.

  2. Char - 文字型.'a','b’のようにシングル・クォーテーションで囲む1文字.

  3. Int - 固定長整数型.扱える値に制限がある整数型.固定長ビット数で表される.

  4. Integer - 任意精度整数型.扱える値に制限のない整数型.任意長ビットで表される.

  5. Float - 単精度浮動小数点数型.固定長ビット数で表される浮動小数点数の型.

  6. Double - Floatの2倍の大きさのビット数で表される浮動少数点数の型.

ghciではデータや式の型を:typeまたは:tコマンドで調べることができます.VSCodeでもHaskell用のExtensionをインストールしていれば,式や値の上にマウスを持っていくと型情報が表示されます.

ghci> :type True
Bool
ghci> :t 'c'
'c' :: Char

Trueの型はBool型であることが分かります.文字'c'では'c' :: Charと出力されています.2つのコロン::の後ろが型名です.式の後ろに::を付けて型名を記したものを型アノテーション(type snotation),または型注釈と呼びます.

コンパイラに型を明示的に指示するには型アノテーションを付けます.

ghci> 1 :: Int
1
ghci> 1 :: Float
1.0
ghci> 1 :: Double
1.0

同じ1ですが,Int型を指定した場合にはインタプリタに1と評価され,FloatDouble型を指定した場合には1.0と評価されました.今度は逆に1.0と入力して同じように型を指定してみます.

ghci> 1.0 :: Int

<interactive>:6:1: error:
    • No instance for (Fractional Int) arising from the literal ‘1.0’
    • In the expression: 1.0 :: Int
      In an equation for ‘it’: it = 1.0 :: Int
ghci> 1.0 :: Float
1.0
ghci> 1.0 :: Double
1.0

1.0は整数ではないのでInt型を指定するエラーになります.1は小数として扱えるのと対照的です.整数は浮動小数点数として解釈しても,失われる情報がないので安全ですが,浮動小数点数を整数として解釈すると一部の情報が失われる可能性があるのでエラーとなります.

Haskellは型に厳格なプログラミング言語で,コンパイル時にコードから型を推論します.Haskellコンパイラは私たちがつけた型アノテーションを鵜呑みにせず,プログラムを精査して型に間違いがないかチェックします.文法上の誤りに加え型の整合性までチェックしてくれるので,コンパイルが成功したプログラムは非常に堅牢で実行時エラーの少ない安全なものになります.

型が複雑なプログラムで型推論がうまくいかない場合は,プログラマが型アノテーションをつけて推論を助ける必要があります.Haskellの型推論は優秀なのでコンパイラを補助するために型アノテーションをつける必要性は薄いのですが,プログラマに対しては型アノテーションをつける利点が2つあります.一つは,型アノテーションがあると,コードの動作が推測しやすく可読性が上がります.二つ目は,型アノテーションとコードが整合的でないとコンパイラがエラーを吐いて指摘してくれるので,自分のコーディングの誤りを発見し易くなります.以上の理由から,Haskellプログラマは,他のコードから利用される関数や変数には型アノテーションを付けます.

型アノテーションの記法は式 :: 型です.

3.2. 型制約と型変数

Section 2.3で,整数の割り算4 / 2の計算が自動的に浮動小数点数の除算として解釈され2.0を得ました.これは整数リテラルの型が文脈に応じて様々な型に解釈されるように定義されているからです.文脈に応じて型が変化する性質を多相性,もしくはポリモーフィズム(polymorphism)と呼びます.ポリモーフィズムはオブジェクト指向型言語では多態性とも訳されます.

前節で整数1の型を調べた結果をもう一度見てみましょう.

ghci> :type 1
1 :: Num a => a

矢印=>の右側のa1の型を表します.型名は大文字で始まる約束でした.型名が小文字で始まる場合,それは型変数(type variable)を表します.つまりaは特定の型を表しているのではなく,文脈によっていろいろな型を取り得ることを意味します.ただしaには制約が付けられています.矢印=>の左側が型クラス制約(type class constraint)です.つまり,型aNum型の仲間でなければいけませんよと言っています.Num型はIntFloatDoubleなどの数値を表す型が全て備えている共通の性質を表す型です(正確には型クラスです).

先ほど,1を浮動小数点数の型として指定することができたのは,整数定数がこのように型変数を用いて定義されていたためです.型変数を使って定義されていてもコンパイル時には型が決定されるので,1.0 :: Intのように誤った使い方をすればエラーになります.

下の例のように小数の定数値はNumではなくFractionalです.これは割り算で余りが切り捨てられる整数と,剰余が出ない小数とで値を区別する必要があるためです.小数定数値はNumではなくFractionalという型クラスに属します.

ghci> :type 1.0
1.0 :: Fractional a => a

3.3. IntとInteger型

扱える値の大きさに制限のあるInt型と,制限のないInteger型の違いを見てみましょう.冪乗を使って少し実験をしてみます.以下の結果を試してください.

ghci> 2^62 :: Int
4611686018427387904
ghci> 2^63 :: Int
-9223372036854775808
ghci> 2^64 :: Int
0

2の62乗は計算できていますが,63乗はマイナス,64乗はゼロになっています.これはInt型が64ビットの固定長で表されているからです.64ビットのうち1ビットはプラスマイナス符号のために使われます.したがって数値として使えるのは63ビットです.

10進数3桁で表せる最大値は,\(10^3 -1=999\)です.0から数えるので0から999の1000個の数を表せます.2進数63桁で表せる最大値は\(2^{63} -1\)です.ですので,2^63Int型が扱える数を1超過してしまい,64ビット目の符号ビットが立ってマイナスとなったわけです.実際2^63 - 1は正しく計算できます.

ghci> 2^63 - 1 :: Int
9223372036854775807

このように扱える値が有界なデータ型はBoundedという型クラスに属しています.ghciで:info Intと入力するとInt型の情報が得られます.

ghci> :info Int
type Int :: *
data Int = GHC.Types.I# GHC.Prim.Int#
        -- Defined in ‘GHC.Types’
instance Integral Int -- Defined in ‘GHC.Real’
instance Real Int -- Defined in ‘GHC.Real’
instance Num Int -- Defined in ‘GHC.Num’
instance Bounded Int -- Defined in ‘GHC.Enum’   (1)
instance Enum Int -- Defined in ‘GHC.Enum’
instance Show Int -- Defined in ‘GHC.Show’
instance Eq Int -- Defined in ‘GHC.Classes’
instance Ord Int -- Defined in ‘GHC.Classes’
instance Read Int -- Defined in ‘GHC.Read’
1 Bounded型クラスの仲間であることが分かります.

Bounded型は扱える最大値と最小値をそれぞれmaxBoundminBoundに持っています.

ghci> maxBound :: Int
9223372036854775807
ghci> (2^63 - 1 :: Int) == (maxBound :: Int)  (1)
True
ghci> minBound :: Int
-9223372036854775808                          (2)
1 \(2^{63} - 1\)とmaxBoundの値が等しいか==演算子でテストしTrueを得ています.つまり左辺と右辺は等しい値です.
2 最小値は符号ビットまで使えるので,正の値より負の数の方が扱える値の絶対値が1大きくなります.

Integer型は任意精度なので有界ではありません.ghciで型を指定しないとインタプリタは大きさに合わせて多相的に計算します.

ghci> 2^100 :: Int
0
ghci> 2^100 :: Integer
1267650600228229401496703205376
ghci> 2^100
1267650600228229401496703205376

Integer型は任意の大きさの値を計算できますが,その分メモリもCPUも消費するので,通常の計算ではInt型を使います.

3.4. リスト型

ここまで単独で型を成すアトミック型のデータ型を見てきました.こんどは複数のデータで構成される複合型(composite type)の仲間を見ていきます.Haskellでもっとも基本的な複合型はリスト型(List)タプル型(Tuple)です.リストは内部に同じ型のデータを複数保持できるデータ型で,タプルは異なる型のデータを複数保持できるデータ型です.まずはリストから見ていきます.

リストは内部に同じ型を並べて格納します.内部に格納する値をコンマ区切りで[1, 2, 3, 4]のように[]括弧で囲むことでリテラル表記でリスト型データを作成できます.

ghci> [10, 20, 30]
[10,20,30]

上のリストの型を調べると以下のようにNum型の型制約が付いた[a]型であることが分かります.リストは内部に任意の型を格納できるので,リストの一般的な型は型変数を用いて[a]で表されます.

ghci> :type [10, 20, 30]
[10, 20, 30] :: Num a => [a]

3.5. 変数定義

型を調べるために毎回リストをタイプするのは面倒なので,リストを変数に格納しましょう.変数名は小文字で始めなければなりません.「変数名 = 値」で定義します.以下ではmyList変数を作成し,型を調べています.

ghci> myList = [10, 20, 30]
ghci> :type myList
myList :: Num a => [a]

型アノテーションを付ければInt型のリストを作成できます.

ghci> intList = [10, 20, 30] :: [Int]   (1)
ghci> :type intList
intList :: [Int]
1 右辺の式の後ろに型アノテーションを追加.

上の例では右辺に型アノテーションを付けましたが,左辺の変数に型アノテーションを付けても同じ結果になります.右辺は整数リテラルのリストなので多相的ですが,左辺が[Int]型なら,コンパイラは右辺の型も[Int]であると推論するからです.

ghci> intList2 :: [Int] = [10, 20, 30]  (1)
ghci> :type intList2
intList2 :: [Int]
1 左辺に型アノテーションを追加.

上の例では新たにintList2変数を作成しました.実は,変数は一度定義したら変更できません.つまり「変数」というより「定数」です.通常のプログラミング言語では変数には値を再代入できますが,Haskellは再代入できないので上の例で変数名を変更しました.

関数型プログラミングでは一般に「変数に値を代入する」という表現は使わず「値を名前に束縛(bind)する」といいます.上の例の場合,[10, 20, 30]intList2に束縛しました.

Haskellは純粋性を重んじるプログラミング言語です.純粋性を重んじるというのは「副作用(side effects)を避ける」ということです.後の章で副作用と純粋性について詳しく学びますが,変数の値の変更は副作用一つです.本書の後半では副作用をうまく処理する方法を学びます.そうすれば純粋性を損なわずにプログラムの効率性を高めるために値を変更するプログラムも書くことが可能になります.

3.6. ghciでの変数の再定義

ファイルにプログラムを書く場合は変数は変更できませんが,ghciでは変数を再定義する方法が提供されています,letキーワードを用いてlet 名前 = 値で変数を定義します.以下ではnumを再定義しています.

ghci> let num = 4               (1)
ghci> :type num
num :: Num a => a
ghci> let num = 10 :: Int       (2)
ghci> :type num
num :: Int
1 numの初回定義
2 numを再定義

ghciは一つ前に評価した結果値をitという変数に格納しています.式を評価したあと,itを使うとその型を調べるのにもう一度入力する手間が省けます.

ghci> ['a', 'b', 'c', 'd']
"abcd"
ghci> :type it
it :: [Char]

[Char]Char型の要素を持つリストの型を表します.

3.7. 文字列とリスト

文字列はダブルクォーテーションで囲むことでリテラル表記できます.

ghci> "Hello world!"
"Hello world!"
ghci> :type it
it :: String

文字列はString型のデータです.String型は実際には[Char]型の別名(エイリアス)です.つまり,"Hello world!"Char型のリストです.

ghci> ['H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!']
"Hello world!"
ghci> :type it
it :: [Char]

上の例でCharのリストを評価すると文字列が得られました.しかし型を調べるとStringではなく[Char]になっています.型の詳しい情報は:infoコマンドで調べることができます.String型を調べてみましょう.

ghci> :info String
type String :: *
type String = [Char]
        -- Defined in ‘GHC.Base’

type String = [Char]GHC.Baseモジュールで定義されていることが分かります.typeキーワードは既存の型に型シノニム(type synonym),つまり別名を付けます.書式はtype 型の別名 = 既存の型名です.

Listは関数型プログラミング言語で最もよく使われるデータ構造で,Listを操作する関数が豊富に用意されています.List操作に慣れ親しむことが関数型プログラミングの第一歩となります.文字列もListなので,慣れ親しんだList処理がそのまま文字列操作に利用できます.リスト処理はChapter 4で学びます.

3.8. タプル型

複合型でもう1つよく使うのがタプル型(Tuple)です.リストが同じ型のデータしか格納できないのに対し,タプルは異なる型のデータを複数格納できます.タプルもリテラル表記で作成できます.格納する要素をコンマで区切り,括弧で囲みます.

ghci> ("Japan", 4820344, 2024)
("Japan",4820344,2024)
ghci> :type it
it :: (Num b, Num c) => (String, b, c)

2番目と3番目の要素に整数リテラルを使ったため,多相的な型になりNum型の制約がついていますが,タプルの型はリテラル表記と対応しています.型制約を無視すると(String, b, c)の部分がタプルの型を表しています.

以下では,整数リテラルが多相的に解釈されないように型アノテーションを付けています.

ghci> ("Japan", 4820344, 2024) :: (String, Int, Int)
("Japan",4820344,2024)
ghci> :type it
it :: (String, Int, Int)

型アノテーションは任意の式の後ろに付けられるので以下のように各要素に型アノテーションを付けて型を指定することもできます,定数リテラルも評価されると値を返すので「式」であったことを思い出しましょう.

ghci> ("Japan", 4820344 :: Int, 2024 :: Int)
("Japan",4820344,2024)
ghci> :type it
it :: (String, Int, Int)

3.9. 関数の型

関数にも型があります.「整数を引数にとって文字列を返す関数」の型は,Int -> [Char],もしくは,Int -> Stringです.つまり,引数の型 -> 返り値の型です.関数の型アノテーションは定義式の後ろではなく前の行に書きます.書き方は「関数名 :: 型アノテーション」です.ghciで複数行を入力するには,:{ :}で囲みます.

ghci> :{
ghci| yourChoice :: Int -> String                   (1)
ghci| yourChoice n = "Your choice: " ++ show n      (2)
ghci| :}
1 yourChoiceの型は「Int型の引数を取りStringを返す関数」.
2 右辺は,文字列"Your choice: "と,show関数を使って文字列型に変換された整数を,リストの接続演算子++で繋いでいます.show関数についてはSection 3.12で説明します.

文字列はCharのリストなので,右辺ではリストを繋げる++演算子を使って文字列を連結させています.リスト処理はChapter 4で詳しく学びます.yourChoice関数の型を見てみましょう.

ghci> :type yourChoice
yourChoice :: Int -> String

実行結果は以下のようになります.

ghci> yourChoice 4
"Your choice: 4"

次に,引数が複数ある場合の関数の型を見てみます.以下は3つのInt型を引数にとり,その合計を文字列に埋め込んで返す関数の定義と実行結果です.

ghci> :{
ghci| addThreeNums :: Int -> Int -> Int -> String           (1)
ghci| addThreeNums x y z = "Total = " ++ show (x + y + z)   (2)
ghci| :}
ghci> addThreeNums 1 5 9
"Total = 15"
1 3つの引数を取りStringを返す関数の型.
2 関数は左結合でshow関数は引数を1つしか取らないので,x + y + zには括弧が必要.

引数が1つのyourChoice関数の型はInt -> Stringだったので,引数が3つのaddThreeNumsの型は引数を括弧でまとめて(Int, Int, Int) -> Stringになりそうな気がしますが,これは間違いです.3つの引数を括弧で囲んでしまうと「3つの要素を持つタプル」を引数に取る関数の型になります.

3.10. カリー化

型アノテーション内の->は右結合の演算子です.したがって,前節で定義したaddThreeNums関数の型Int -> Int -> Int -> StringInt -> (Int -> (Int -> String))と同じです.この型アノテーションにそれぞれ引数1から引数3までと返り値を書き込むと以下を得ます.

コード 1. addThreeNumsの型アノテーションの分解図
    Int  ->  Int  ->  Int  -> String
=   Int  -> (Int  -> (Int  -> String))
=   引数1 -> (引数2 -> (引数3 -> 返り値 ))
                       ^^^^^^^^^^^^^
                           関数3

コード 1の2行目右端の括弧内はInt -> StringIntを引数の取ってStringを返す関数の型です.この関数をコード 1の3行目に記したように関数3と呼ぶことにしましょう.すると,関数3の外側の括弧は,引数2をとって関数3を返す関数の型であることが分かります.すなわち下図の一番下の行を得ます.

コード 2. addThreeNumsの型アノテーションの分解図その2
    Int  ->  Int  ->  Int  -> String
=   Int  -> (Int  -> (Int  -> String))
=   引数1 -> (引数2 -> (引数3 -> 返り値 ))

=   引数1 -> (引数2 ->      関数3     )
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
                      関数2

つまり,addThreeNums関数は1つ目の引数を取って,関数2を返すことが分かりました.

以上を踏まえてもう一度addThreeNums x y zの関数呼び出しを見てみましょう.addThreeNumsは,まずxを引数にとって関数2を返します.今度は関数2に2番目の引数yが渡され,再び関数が返ります.これが関数3です.最後に関数3に3番目の引数zが渡され,addThreeNums関数の最終的な計算結果であるStringが返ることになります.

Haskellは3つの引数に対し一度に関数を適用するのではなく,以上の手順と同じように,左の引数から順に関数適用を行い,全ての引数が揃うまで「次の引数を取る関数」を返します.プログラミングの手法で,複数引数をとる関数を1つの引数を取る関数に分解して定義することをカリー化(currying)と呼びます.Haskellではどの関数も常にカリー化が行われます.[1]

関数は左結合であったことを思い出しましょう.つまり,addThreeNums x y z(((addThreeNums x) y) z)と同じ意味です.これはまさにカリー化を意味しています.一番内側の(addThreeNums x)が関数を返し,その関数に引数yが渡され,再び関数が返り,右端の引数zが渡されます.

試しにaddThreeNumsに引数を1つだけ与えて,その結果をaddTwoMoreと名付けます.addTwoMoreは上の関数2に対応します.

addTwoMore = addThreeNums 1

型を調べるとaddTwoMoreの型は2つの引数を取りStringを返す関数になります.

ghci> :type addTwoMore
addTwoMore :: Int -> Int -> String

残りの引数に23を与えると,最初に与えた1と合わせて6を得ます.

ghci> addTwoMore 2 3
"Total = 6"

もちろん,addTwoMoreにもう一つ引数を与えて,最後の引数を待ち受けるaddOneMore関数を作ることもできます.addOneMoreは上の関数3に対応します.

addOneMore = addTwoMore 2

addOneMore関数の型と,最後の引数に3を与えた実行結果は以下の通りです.

ghci> :type addOneMore
addOneMore :: Int -> String
ghci> addOneMore 3
"Total = 6"

3.11. 型クラス(type class)

これまで,多相的な関数の型クラス制約で,何度か「型クラス」が登場しました.ここで型クラスとは何かについて学んでおきましょう.

プログラミングにおけるクラス(class)は同じ性質を持つ仲間を表す概念です.インスタンス(instance)はそうしたクラスの1つの具現化だと思ってください.たとえると,「クラス」はクッキーの型枠のようなものです.同じ型枠から作られたクッキーは似たもの同士ですが,それぞれのクッキーは実際には微妙に異なり,焼き上がりの色も違います.こうした型枠からできた実際のクッキー1つ1つが「インスタンス」です.

型クラスは「同じ性質」を持つ「型」を集めた型枠のようなものです.具体的な「型」がインスタンスです.クッキーの型枠と比較してみましょう.

  • 「クッキーの型枠」は,同じ形状のクッキーを定めた規格です.型枠から「個々のクッキー」を作ります.

  • 「型クラス」は,同じ性質を持つ型を定めた規格です.型クラスから「個々の型インスタンス」を作ります.

イメージが掴めたでしょうか.次に「同じ性質を持つ型」について明らかにしましょう.数値データは四則演算が可能なのに対し,文字列にはそのような演算は定義されていません.一方,文字列は連結処理はできますが,数値データはそのような処理に対応していません.つまり,型によって対応する演算や関数が決まります.型クラスは「同じ関数が適用できる型」を集めたものです.

これまで何度も登場したNum型クラスの定義を見てみましょう.

class  Num a  where
    (+), (-), (*)       :: a -> a -> a
    negate              :: a -> a
    abs                 :: a -> a
    signum              :: a -> a
    fromInteger         :: Integer -> a

Num型クラスの仲間は上の7つの関数を持っています.Num型クラスは,クラスに属するインスタンスが提供すべき関数を定義しています.

型クラスは型枠だけなので上のNumのように関数の中身は空っぽです.型クラスは,クラスに属する型に対する「関数の呼び出し方」を規定しているだけです.つまり型クラスは型に適用できる関数のインターフェイス(interface)を定義しているだけです.各型は,インスタンス化するためにインターフェイスの実装を提供しなければなりません.実際には,多相的に定義できる関数は型クラス内で実装が提供されるので,その関数を利用(継承)することでインスタンスの実装は省けます.

Int型の例を見てみましょう.Intは以下のようにNum型クラスのインターフェイスを実装しています.まだコードの意味は分からなくて構いません.しかし,上の7つの関数が定義されているのが見て取れるはずです.

instance Num Int where
    I# x + I# y = I# (x +# y)
    I# x - I# y = I# (x -# y)
    negate (I# x) = I# (negateInt# x)
    I# x * I# y = I# (x *# y)
    abs n  = if n `geInt` 0 then n else negate n

    signum n | n `ltInt` 0 = negate 1
             | n `eqInt` 0 = 0
             | otherwise   = 1

    fromInteger i = I# (integerToInt# i)

3.12. Show型クラスとshow関数

データを画面に表示させたい時には,データを文字列に変換する必要があります.データの文字列表記を提供するshow関数を持つ型を集めたのがShow型クラスです.言い換えると,Show型クラスのインスタンスは,show関数を使ってデータを文字列に変換できます.

以下はShow型クラスの定義の抜粋です.

class Show a where
    show :: a -> String

アトミックデータ型は全てShow型クラスのインスタンスです.また,アトミックデータ型を組み合わせた複合型も全てShow型クラスに属します.Section 3.9yourChoice関数でshow関数を用いたように,データを文字列に変換したい場面で利用できます.ちなみに,show関数は小文字で始まることに注意しましょう.

ghci上では日本語をshowすると文字化けします.その際は,Haskell-jpの記事を参考にUnicodeに対応したushowを使用してください.

3.13. 本章のまとめ

  • アトミック型と複合型の代表的な型を学びました.複合型にはリスト,タプル,文字列がありました.

  • Int型とInteger型の違いを学びました.

  • 各データ型の型アノテーションの書き方を学びました.

  • 「関数の型」を学びました.

  • 変数定義とghci上での変数の再定義方法を学びました.Haskellでは変数は変更できないことも学びました.

  • Haskellの関数はすべてカリー化されることを学びました.

  • 型クラスとは何かを学びました.また,Show型クラスが提供するshow関数について学びました.

3.14. 練習問題

  1. fromIntegral関数の型はfromIntegral :: (Integral a, Num b) ⇒ a -> bです.この関数は引数に与えられたa型の値をb型の値に変換して返します.引数と返り値の型は多相的に定義されているので,型アノテーションを付けることで,数値データを所望の型に変換できます.fromIntegral関数を整数10に適用し,Double型に変換してください.

  2. Int型の最大値が10進数何桁になるか計算して求めてください.ヒント: ある値\(x\)が10進数で\(k\)桁なら,\(10^k > x \geq 10^{k-1}\)が成立するので,\(k > \log_{10} x \geq \log_{10} k-1\)です.logBase関数の第2引数はDouble型である必要があることに注意しましょう.

  3. logBase関数を用いて,底が2の対数を計算する関数log2 :: Double -> Doubleを定義してください.


1. 余談ですがカリー化のカリーはHaskell Curryという著名な数学者の名を取って付けられました.そして皆さんが勉強しているHaskellもこの数学者の名前に由来します.