※この記事は「型クラスで見るPureSciript」の連載記事です。
ピンからキリまで
前回は、同じデータ型の値に順序を定義するOrd型クラスを学びました。ところで、コンピュータの世界は、全てが有限の世界です。もちろん、数学でも有限集合という考え方があります。
有限の世界で順序関係を定義すると、当然ながら、その中で最大のものと最小のものが定まります。今回は、データ型の最大値と最小値を定義する Bounded型クラスについて見ていきましょう。
最大値と最小値
ところで、最大値と最小値とは、改めてどういう意味の言葉なのでしょう?「そんなのわざわざ説明するまでもないよ」と思う方も多いでしょう。では、あえて説明してくださいと言われて上手く説明できるでしょうか?
ついうっかり、「最大値というのは一番大きい値のことだよ」と言った後で、「アレ?今、自分、同じことを言ったぞ?」と説明になっていないことに気づく方も多いことでしょう。当たり前の事を人に説明するのは意外と難しいです。
最大値と最小値という言葉を説明するには、前回導入した順序関係を使って説明するのが適切です。最大値について説明すると次のようになります。
- 最大値
「データ型 $X$ の全ての値 $x$ に対して、$x<=\text{top}$ が成り立つような値 $\text{top}$ が存在するとき、値 $\text{top}$ をデータ型 $X$ の最大値と呼ぶ。」
最小値はこの逆です。
- 最小値
「データ型 $X$ の全ての値 $x$ に対して、$\text{bottom}<=x$ が成り立つような値 $\text{bottom}$ が存在するとき、値 $\text{bottom}$ をデータ型 $X$ の最小値と呼ぶ。」
あぁ、数学ってまわりくどい……
Bouded型クラス
それでは、Bounded型クラスの定義を見てみましょう。といっても、Bounded型クラスの定義は、これまで見てきた型クラスの中では一番簡単だと思います。
class (Ord a) <= Bounded a where
top :: a
bottom :: a
最大値を表すtop
と最小値を表すbottom
という2つの値が定義されているだけです。特に悩むこともないと思うので早速実装して見ましょう。
Bounded型クラスの例1 四季を表すデータ型
Bounded型クラスの例を示すために、まずは新しいデータ型を定義しましょう。
data Season
= Spring
| Summer
| Autumn
| Winter
instance showSeason :: Show Season where
show s = case s of
Spring -> "Spring"
Summer -> "Summer"
Autumn -> "Autumn"
Winter -> "Winter"
Seasonデータ型は、四季を表すデータ型です。Seasonデータ型は、いわゆる列挙型の構造をしています。他の言語では、列挙型は整数の表現を持つことが多いのですが、PureScriptでは、このように各値に対応する引数なしのコンストラクタを列挙して表現することが多いです。
新しいデータ型を作ったので、まずは、Bounded型クラスの前提となる、Eq型クラスとOrd型クラスを実装しましょう。
instance eqSeason :: Eq Season where
eq s t = case s, t of
Spring, Spring -> true
Summer, Summer -> true
Autumn, Autumn -> true
Winter, Winter -> true
_ , _ -> false
instance ordSeson :: Ord Season where
compare s t = case s, t of
Spring, Spring -> EQ
Spring, Summer -> LT
Spring, Autumn -> LT
Spring, Winter -> LT
Summer, Spring -> GT
Summer, Summer -> EQ
Summer, Autumn -> LT
Summer, Winter -> LT
Autumn, Spring -> GT
Autumn, Summer -> GT
Autumn, Autumn -> EQ
Autumn, Winter -> LT
Winter, Spring -> GT
Winter, Summer -> GT
Winter, Autumn -> GT
Winter, Winter -> EQ
ちょっと面倒臭いですね。特に、Ord型クラスが4×4のパターンを書き下しているので間違えそうです。後の回で、これをもっと簡潔に書く方法を紹介するつもりなので、今は我慢願います。
さて、ここまで書いてしまえば、Bounded型クラスの実装は一意に定まってしまいます。Ord型クラスの定義を見ればわかるように、「春」が最初の季節(一番小さい)で、「冬」が最後の季節(一番大きい)なので、Bounded型クラスの実装は次のようになります。
instance boundedSeason :: Bounded Season where
top = Winter
bottom = Spring
簡単ですね。試すまでもないとは思いますが、一応、お作法として実行しておきましょう。今回は型推論が働かないので、実行するときは型を指定して記述します。
> (top::Season)
Winter
> (bottom::Season)
Spring
退屈ですね。
Bounded型クラスの例2 角度を表すデータ型
Bounded型クラスだけでは、特に紹介するような便利な関数はありません。今回は退屈な回になりそうです。例を1つ示しただけでは余りにも退屈すぎるので、もう1つ例を考えてみましょう。
前回、Ord型クラスを導入したDegreeデータ型についてもBounded型クラスを実装できそうです。まずは、前回定義したOrd型クラスを思い出してみましょう。
Degreeデータ型の順序関係は、0°から180°までの値を数値順に並べて定義されていました。符号の違いと360°の差異は、順序関係においては同順と扱われるので、0°から180°までの範囲を考えれば十分です。
すると、すぐに分かると思いますが、Degreeデータ型の全ての値Degree x
に対して、Degree x <= Degree 180
とDegree 0 <= Degree x
が成り立ちます。つまり、この2つが最大値と最小値です。
instance boundedDegree :: Bounded Degree where
top = Degree 180
bottom = Degree 0
無事、Bounded型クラスを実装することができました。
ところで、前回、Degreeデータ型に対してはx == y
とcompare x y == EQ
となる条件が異なる__不完全な__Ord型クラスの実装をしたことを思い出してください。
一般に__不完全な__Ord型クラスの実装をすると最大値と最小値が一意に定まる保証がなくなります。Bounded型クラスには最大値と最小値を1つずつしか定義できないので、(値の異なる)複数の最大値や最小値がある場合は、その中から恣意的に1つを選ぶことになり、最大値や最小値を利用するロジックが正しく動くか、少し心配になってしまいます。
__不完全な__Ord型クラスの実装した時点で、既存のライブラリの一部が正しく動作しないことを許容していると思うので今更な気もしますが、歪みが拡大していくことは、できれば避けたいものです。
幸いにも、Degreeデータ型は、0°と180°の2点では同値条件と同順条件が一致するので、このような心配をすることなく自然にBounded型クラスを実装することができました。
少し道を外れてみると、裏路地で今まで知らなかった店に出会えるように、少しルールを外れてみると、ルールの理由を理解できて勉強になりますね。
ぼんやりと欠伸でもしながら
この節の内容は筆者による見解を含みます。
ところで、前回は長さを表すLengthデータ型にも順序関係を導入しましたが、Lengthデータ型にはBounded型クラスを実装できないのでしょうか?
無理やり実装しようとすれば、できないことはないのですが、あまり自然な行為ではありません。なぜなら、Lengthデータ型がラップしているNumberデータ型にBouded型クラスが実装されていないからです。何故でしょうか?
確かに、Numberデータ型は有限サイズのデータ型なので、表現できる値には上限と下限があります。ところが、浮動小数点型は有効桁数の上限もあるので、表現できる値の精度には限界があります。PureScript(JavaScript)のNumber型は、内部的にはC言語のdouble型で表現されるので有効桁数は52ビットしかありません。なので、それよりも細かい精度で計算することはできません。それは、イコールになるはずの演算が完全には一致しない可能性があることも意味しています。
最大値・最小値が定義されているということは、ピッタリその値になる場合が存在するということでもあります。ところが、double型では常に丸め誤差がつきまとうので、例えdouble型の最大値を表すビット列であっても、ピッタリの値を期待することができません。
私見ですが、おそらくは、このような事情を考慮して、下手にNumber型の最大値・最小値を定義してプログラマの勘違いを誘発するよりも、初めから大した使い途もない定数値を定義しておかない方が良いという方針なのでしょう。この辺りは、ビット列志向なC言語とかとは違う考え方なのでしょう。
勿論、大きな値の数値計算をする場合は、double型の表現できる最大値を超えないように気にする必要がありますが、特段の理由がなければ。Number型をラップしたデータ型にBounded型クラスを実装することは控えた方が良さそうです。
最大や最大がいつもあるとは限らない?!
Ord型クラスとBounded型クラスが別々に定義されているということは、順序関係があっても最大値や最小値を定義できない場合があるということです。上で述べたように、Numberデータ型もBounded方クラスが実装されない例の1つではありますが、なんだか腑に落ちない方もいるでしょう。
最後に、もう1つだけ、Bounded方クラスを実装できない例を見てみましょう。
data Astrs = Astr | Astrs Astrs Astrs
infixl 6 Astrs as ++
instance showAstrs :: Show Astrs where
show Astr = "*"
show (Astrs a b) = show a <> show b
Astrsデータ型は1つ以上のアスタリスク「*」を表すデータ型です。例えば、3つのアスタリスクを表現したければ次のように記述します。
> Astr ++ Astr ++ Astr
***
このAstrsデータ型にEq型クラスとOrd型クラスを実装したいのですが、その前に便利な関数を2つ定義しておきましょう。
countAstr :: Astrs -> Int
countAstr Astr = 1
countAstr (Astrs a b) = countAstr a + countAstr b
genAstrs :: Int -> Astrs
genAstrs n | n < 1 = Astr
genAstrs n = genAster' n Astr
where
genAster' 1 acc = acc
genAster' n acc = genAster' (n - 1) (acc ++ Astr)
関数countAstr
はAstrsデータ型のアスタリスクの数を数える関数です。一方、関数genAstrs
はアスタリスクの個数を指定してAstrsデータ型の値を生成する関数です。
> countAstr $ Astr ++ Astr ++ Astr
3
> genAstrs 3
***
これらの関数、特にcountAstr
を使って、Astrsデータ型にEq型クラスとOrd型クラスを実装してみましょう。
instance eqAstrs :: Eq Astrs where
eq a b = (countAstr a) == (countAstr b)
instance ordAstrs :: Ord Astrs where
compare a b = compare (countAstr a) (countAstr b)
Astrsデータ型の大小関係はアスタリスクの数で比べます。例えば、次のようになります。
> Astr ++ Astr < Astr ++ Astr ++ Astr
true
すると、すぐにAstr
がAstrsデータ型の最小値であることがわかるでしょう。何故なら、Astrsデータ型の全ての値a
に対してAstr <= a
が成り立つからです。
さて、Astrsデータ型に最大値はあるでしょうか?
答えは、「Astrsデータ型には最大値はない」が正解です。
何故なら、Astrsデータ型はメモリの許す限り大きな個数のアスタリスクを表現できるからです。確かに、コンピュータの世界は有限なのですが、概念的にはAstrsデータ型は無限個のアスタリスクを表現できるはずです。(実際には無限個のアスタリスクを表現する前にメモリ不足による異常終了を迎えるはずですが……)
Bounded型クラスは、最大値と最小値の両方が定義できてはじめて実装できるので、Astrsデータ型のように、片方しか定義できない場合には実装することができません。
このように、Ordデータ型が定義されているからといって、必ずしもBoundedデータ型を定義できるとは限らないのです。
阿吽の呼吸
さて、今回の話では、Bounded型クラスについて紹介しましたが、プログラミング大好きな皆さんにとっては、少し退屈だったのではないでしょうか。特に便利そうな関数や演算子があるわけでもなく、何のための型クラスなのやらさっぱりという感想を持たれるのではないでしょうか。
Bounded型クラスが活躍するのは、この章の最後の回EoundedEnum型クラスまで待つことになります。次回紹介するEnum型クラスの実装をすると、Bounded型クラスが縁の下で良い仕事をしてくれて、BoundedEnum型クラスの実装が自然に定まってしまう様子を見ることができます。(見えないところで活躍してたりするので、ありがたみが分かり難いですが。)
その他にも、自分でロジックを組む際にも、最大値と最小値が分かっていると思考を整理しやすかったりします。普段は存在を意識しないのに、無くなると急に不便を感じるものってありますよね。そんな風に、Bounded型クラスは、目立たないけど、いると助かる子だと思います。
それでは、また次回
演習問題
ドリンクサイズを表すデータ型DrinkSize
にBounded型クラスを実装してみてください。
data DrinkSize = Short | Regular | Tall
参考文献
Bounded型クラスについては下記を参考にしました。