Haskell入門ハンズオン! #5 - 当日用資料 (2/5)
多相性とリストをあつかう関数
Haskellの関数には型がある。
toLower :: Char -> Char
isLower :: Char -> Bool
isLowerの引数はChar型の値でなければならず、返り値はChar型の値となる。リストの型は`[要素の型]'のように表現される。リストをあつかう関数には、どの「要素の型」に対しても使えるものがある。たとえば「リストの長さ」を求める関数。これは「リストの要素の型」には関係なく、どのような型の値を要素とするリストにも使えるようであってほしい。
長さを求める関数
リストの長さを求める関数を定義する。
% vim samples/length.hs
myLength [] = 0
myLength (_ : xs) = 1 + myLength xs
空リストの長さは0。空でないリストの長さは、1に残りのリストの長さを足したもの。試してみる。
> :load samples/length.hs
> myLength [3, 7, 8, 9]
4
リストの長さを求める関数に型宣言をつけたい。つぎのような意味の型として宣言したい。
なんらかの型のリスト -> Int
「なんでもいい型」はHaskellでは小文字ではじまる名前で表現される。この「なんでもいい型」をあらわすものを型変数と呼ぶ。関数myLengthに型をつけてみよう。
% vim samples/length.hs
myLength :: [a] -> Integer
なんらかの型の値のリストを引数として、Integer型の値をかえす関数として宣言した。
すべての要素に関数を適用する
あたえられた関数を、すべての要素に適用する。
> map (* 2) [3, 5, 8]
[6,10,16]
(* 2)は演算子の部分適用。の左側を引数でうめるような関数になる。つまり「なにかを2倍する関数」だ。関数mapによって( 2)をリストのすべての要素に適用している。関数mapの型をみてみよう。
> :type map
map :: (a -> b) -> [a] -> [b]
型aの値から型bの値を計算する関数と、型aの値のリストを引数としてとり、型bの値のリストを返り値としてかえす。
関数mapとおなじ関数を定義する。
% vim samples/list.hs
myMap :: (a -> b) -> [a] -> [b]
myMap _ [] = []
myMap f (x : xs) = f x : myMap f xs
空リストは空リスト。先頭がxで残りがxsであるようなリストは、先頭がf xであり、残りがリストxsのすべてに関数fを適用したリスト。試してみる。
> :load samples/list.hs
> myMap (* 2) [3, 5, 8, 7]
[6,10,16,14]
条件を満たす値だけ取り出す
条件を満たす値だけ取り出してみる。
> filter even [3, 8, 7, 2, 9]
[8,2]
関数evenは引数が偶数のときにTrueをかえし、奇数のときにFalseをかえす関数。関数filterの型をみてみよう。
> :type filter
filter :: (a -> Bool) -> [a] -> [a]
「a型の値を引数としてとり、Bool型の値をかえす関数」と、a型の値のリストを引数としてとり、a型の値のリストを返り値としてかえす。
関数filterとおなじ関数を定義してみる。
% vim samples/list.hs
myFilter :: (a -> Bool) -> [a] ->[a]
myFilter _ [] = []
myFilter p (x : xs)
| p x = x : myFilter p xs
| otherwise = myFilter p xs
値xが条件pを満たすときだけ、xが結果のリストに追加されている。
リストを結合する
リストを結合するには演算子(++)を使う。
> "Hello" ++ "World"
"HelloWorld"
リストを結合する演算子を自分で定義してみる。
% vim samples/list.hs
(.++) :: [a] -> [a] -> [a]
(x : xs) .++ ys = x : (xs .++ ys)
[] .++ ys = ys
(x : xs)とysとを結合したリストは、xsとysとを結合したリストの先頭にxを追加したもの。空リストとリストysとを結合したリストは、リストysそのもの。
ここまでのまとめ
リストをあつかう関数をみた。
- myLength
- map
- filter
- 演算子(++)
要素の型を問わない関数では、型変数を使って型宣言をする。
代数的データ型
Haskellでは新しい型をつくることができる。新しく作られたデータ型を代数的データ型と呼ぶ。ここでは直交座標系と極座標系の、両方で点を表現できるデータ型と、そのような点のふたつの距離を求める関数をみる。
点をあらわすデータ型
代数的データ型を定義するには予約語dataを使う。
data 型の名前
= 値構築子 型1-1 型1-2
| 値構築子 型2-1 型2-2
点のデータ型を定義してみよう。
% vim samples/point.hs
data Point
= Cartesian Double Double
| Polar Double Double
deriving Show
対話環境で表示できるように「deriving Show」をつけた。試してみよう。
> :load samples/point.hs
> Cartesian 3 8
Cartesian 3.0 8.0
> Polar 5 (pi / 6)
Polar 5.0 0.5235987755982988
2点間の距離をもとめるために、直交座標上のx, yの値を取得する。
% vim samples/point.hs
getXY :: Point -> (Double, Double)
getXY (Cartesian x y) = (x, y)
getXY (Polar d r) = (d * cos r, d * sin r)
試してみる。
> :reload
> getXY (Cartesian 3 5)
(3.0,5.0)
> getXY (Polar 8 (pi / 3))
(4.000000000000001,6.928203230275509)
2点間の距離をもとめる関数を定義する。
% vim samples/point.hs
dist :: Point -> Point -> Double
dist p1 p2 = sqrt ((x1 - x2) ^ 2 + (y1 - y2) ^ 2)
where
(x1, y1) = getXY p1
(x2, y2) = getXY p2
where構文を使うと関数のなかで変数を定義できる。ここでは、それぞれの点のx座標、y座標を取得している。試してみる。
> :reload
> dist (Cartesian 3 5) (Cartesian 7 8)
5.0
> dist (Polar 5 (pi / 6)) (Polar 11 (pi / 6))
6.000000000000001
> dist (Cartesian 3 5) (Polar 11 (pi / 6))
6.545404750680211
値構築演算子
「関数」にたいして、中置記法の「演算子」がある。おなじように「値構築子」にたいして、「値構築演算子」がある。「関数」が小文字ではじまるのにたいして、「値構築子」は大文字ではじまる。おなじように「演算子」は':'以外の記号ではじまるが、「値構築演算子」は':'ではじまる記号列だ。
前置 | 中置 | |
---|---|---|
関数 | foo | +++ |
値構築子 | Foo | :+++ |
値構築演算子を定義してみる。
% vim samples/expression.hs
data Expr
= Integer :+ Integer
| Integer :- Integer
| Integer :* Integer
deriving Show
試してみる。
> :load samples/expression.hs
> 123 :+ 456
123 :+ 456
> 111 :* 33
111 :* 333
型クラス
どの型に対してでも使える関数の型を宣言するのに、型変数を使った。これは、たとえばリストなどの「構造の部分」に注目して、なかみの値には、さわらないことで実現されている多相性だ。ここでは、それぞれの型に対して、専用の関数を定義することによる多相性を学ぶ。
「成熟する」という性質
型クラスは、いくつかの型に共通する「性質」を表現する。例として「成熟する」という性質を型クラスにする。
% vim samples/maturable.hs
class Maturable a where
grow :: a -> a
mature :: a -> Bool
予約語classを使って定義する。型クラス名はMaturableで、ふたつのクラス関数をもつ。
- grow: 成長した「つぎの段階」をかえす
- mature: 成熟したかどうかを調べる
関数growは問題になっている型aについて、成長前(a型の値)を引数として、成長後の値(a型の値)をかえす。関数matureは問題になっている型aについて、チェックする値(a型の値)を引数として、成熟したかどうか(Bool型の値)をかえす。
ブリ
「成熟するもの」として出世魚のブリがある。ブリをあらわすデータ型を定義する。
% vim samples/maturable.hs
data Buri = Tsubasu | Hamachi | Mejiro | Buri
deriving Show
ブリは成長の過程で、つぎのように名前が変わる。
ツバス -> ハマチ -> メジロ -> ブリ
型Buriを型クラスMaturableのインスタンスにする。
% vim samples/maturable.hs
instance Maturable Buri where
grow Tsubasu = Hamachi
grow Hamachi = Mejiro
grow Mejiro = Buri
grow Buri = Buri
mature Buri = True
mature _ = False
関数growは、それぞれ、成長後の値をかえす。関数matureは値BuriのみTrueとなる。試してみよう。
> :load maturable.hs
> grow Hamachi
Mejiro
> mature Mejiro
False
> mature Buri
True
多相関数
型クラスを利用した多相関数を定義する。
% vim samples/maturable.hs
growing :: Maturable a => a -> [a]
growing x
| mature x = [x]
| otherwise = x : growing (grow x)
ある値から成熟するまでの値をリストにする。growing xは、先頭がxで残りがgrowing (grow x)であるリストになる。つまり、xのつぎからはじまるリストの先頭にxを追加している。試してみる。
> :reload
> growing Tsubasu
[Tsubasu,Hamachi,Mejiro,Buri]
コラッツ数列
仕意の数ではじめて、偶数なら2でわり、奇数なら3倍して1をたすという操作を1になるまで、くりかえした数列を考える。これをコラッツ数列と呼ぼう。たとえば26からはじめると、つぎのようになる。
26 13 40 20 10 5 16 8 4 2 1
このコラッツ数列をもとめる処理を、型クラスMaturableを利用して定義する。
% vim samples/maturable.hs
instance Maturable Collatz where
grow (C n)
| even n = C (n `div` 2)
| otherwise = C (3 * n + 1)
mature (C 1) = True
mature _ = False
関数growは、偶数と奇数のそれぞれに対して、つぎの値をかえす。関数matureは1であることを調べている。試してきる。
> :reload
> growing (C 26)
[C 26,C 13,C 40,C 20,C 10,C 5,C 16,C 8,C 4,C 2,C 1]
型クラスのまとめ
型クラスは型のもつ「性質」をあらわす。今回は「成熟する」という性質を定義した。型クラスMaturableを使うことで
- ひとつ前の項から、つぎの項が決まる
- 終了条件が定義されている
といった性質をもつ型をまとめられる。これを使うことで、ある値から終了条件までの(数)列をかえす関数を抽象的に定義できる。