3. 型入門
関数とデータ型の基本を学びます.どんなプログラミング言語も扱うデータを「型(type)」で区別します.Haskellはコンパイル時に型を指定する静的型付け言語です.
3.1. アトミック型
Haskellには予め多くの型が用意されています.そうした型の中で,これ以上分割できないアトミック型(atomic type)のデータ型は以下の6つです.Haskellの型は必ず大文字で始まリます.
-
Bool - 真偽値型.この型には
True
とFalse
の値の2値のみ. -
Char - 文字型.'a','b’のようにシングル・クォーテーションで囲む1文字.
-
Int - 固定長整数型.扱える値に制限がある整数型.固定長ビット数で表される.
-
Integer - 任意精度整数型.扱える値に制限のない整数型.任意長ビットで表される.
-
Float - 単精度浮動小数点数型.固定長ビット数で表される浮動小数点数の型.
-
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
と評価され,Float
やDouble
型を指定した場合には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
矢印=>
の右側のa
が1
の型を表します.型名は大文字で始まる約束でした.型名が小文字で始まる場合,それは型変数(type variable)を表します.つまりa
は特定の型を表しているのではなく,文脈によっていろいろな型を取り得ることを意味します.ただしa
には制約が付けられています.矢印=>
の左側が型クラス制約(type class constraint)です.つまり,型a
はNum
型の仲間でなければいけませんよと言っています.Num
型はInt
やFloat
,Double
などの数値を表す型が全て備えている共通の性質を表す型です(正確には型クラスです).
先ほど,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^63
はInt
型が扱える数を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
型は扱える最大値と最小値をそれぞれmaxBound
とminBound
に持っています.
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 -> String
はInt -> (Int -> (Int -> String))
と同じです.この型アノテーションにそれぞれ引数1から引数3までと返り値を書き込むと以下を得ます.
Int -> Int -> Int -> String = Int -> (Int -> (Int -> String)) = 引数1 -> (引数2 -> (引数3 -> 返り値 )) ^^^^^^^^^^^^^ 関数3
コード 1の2行目右端の括弧内はInt -> String
はInt
を引数の取ってString
を返す関数の型です.この関数をコード 1の3行目に記したように関数3
と呼ぶことにしましょう.すると,関数3
の外側の括弧は,引数2
をとって関数3
を返す関数の型であることが分かります.すなわち下図の一番下の行を得ます.
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
残りの引数に2
と3
を与えると,最初に与えた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.9のyourChoice
関数でshow
関数を用いたように,データを文字列に変換したい場面で利用できます.ちなみに,show
関数は小文字で始まることに注意しましょう.
ghci上では日本語をshow
すると文字化けします.その際は,Haskell-jpの記事を参考にUnicodeに対応したushow
を使用してください.
3.13. 本章のまとめ
-
アトミック型と複合型の代表的な型を学びました.複合型にはリスト,タプル,文字列がありました.
-
Int
型とInteger
型の違いを学びました. -
各データ型の型アノテーションの書き方を学びました.
-
「関数の型」を学びました.
-
変数定義とghci上での変数の再定義方法を学びました.Haskellでは変数は変更できないことも学びました.
-
Haskellの関数はすべてカリー化されることを学びました.
-
型クラスとは何かを学びました.また,Show型クラスが提供する
show
関数について学びました.
3.14. 練習問題
-
fromIntegral
関数の型はfromIntegral :: (Integral a, Num b) ⇒ a -> b
です.この関数は引数に与えられたa
型の値をb
型の値に変換して返します.引数と返り値の型は多相的に定義されているので,型アノテーションを付けることで,数値データを所望の型に変換できます.fromIntegral
関数を整数10
に適用し,Double
型に変換してください. -
Int
型の最大値が10進数何桁になるか計算して求めてください.ヒント: ある値\(x\)が10進数で\(k\)桁なら,\(10^k > x \geq 10^{k-1}\)が成立するので,\(k > \log_{10} x \geq \log_{10} k-1\)です.logBase
関数の第2引数はDouble
型である必要があることに注意しましょう. -
logBase
関数を用いて,底が2の対数を計算する関数log2 :: Double -> Double
を定義してください.