※この記事は「型クラスで見るPureSciript」の連載記事です。
始発駅から終着駅まで
前回は Enum型クラスについて学びました。そして、前々回は Bounded型クラスについて学びました。今回は、これら2つの型クラスを合体させた BoundedEnum型クラスについて学びます。
Eq型クラスから始まった第1章も、今回で一旦ゴールです。それでは、張り切っていきましょう。
BoundedEnum型クラス
まずは、いつものように、BoundedEnum型クラスの定義を見るところから始めましょう。Bounded型クラスの定義は下記のようになっています。
class (Bounded a, Enum a) <= BoundedEnum a where
cardinality :: Cardinality a
toEnum :: Int -> Maybe a
fromEnum :: a -> Int
BoundedEnum型クラスは、Bounded型クラスとEnum型クラスの2つを親に持つ型クラスです。そして、1つのメンバフィールドと2つのメンバ関数を持っています。簡単に各メンバについて説明しておきましょう。
cardinality
cadinality
は値の個数を表すメンバです。個数を表すだけならIntでも十分に思えますが、実際はCardinalityデータ型で包まれています。その理由は、おそらく、呼び出し時にどの型のインスタンスを呼び出すかを特定するためだと思われます。Cardinalityデータ型は下記のようにnewtype宣言されています。
newtype Cardinality a = Cardinality Int
toEnum
toEnum
関数は、整数値からBoudedEnum型クラスの値を生成する関数です。全ての整数に対応する値が存在するとは限らないので、結果はMaybeデータ型で包まれています。
fromEnum
fromEnum
関数はtoEnum
とは逆で、BoundedEnum型クラスの値から、対応する整数値に変換する関数です。
BoudedEnum型クラスの例1
さて、早速BoundedEnum型クラスの例を見てみましょう。これまでなんども登場してきたSeasonデータ型に対してBoudedEnum型クラスを実装すると下記のようになります。
instance boundedEnumSeason :: BoundedEnum Season where
cardinality = Cardinality 4
toEnum n = case n of
1 -> Just Spring
2 -> Just Summer
3 -> Just Autumn
4 -> Just Winter
otherwise -> Nothing
fromEnum s = case s of
Spring -> 1
Summer -> 2
Autumn -> 3
Winter -> 4
特に悩むこともないと思います。いつものように、PSCIで正しく実装できたことを確かめましょう。ところで今回は、fromEnum
以外では型推論が働きません。1つだけ型を書かないのも気持ち悪いので、今回は全て型を明記して実行してみます。
> (cardinality :: Cardinality Season)
(Cardinality 4)
> (toEnum :: Int -> (Maybe Season)) <$> [0,1,2,3,4]
[Nothing,(Just Spring),(Just Summer),(Just Autumn),(Just Winter)]
> (fromEnum :: Season -> Int) <$> [Spring,Summer,Autumn,Winter]
[1,2,3,4]
purescript-enums パッケージの Ver.3.2.1 までは、Cardinalityデータ型に Show型クラスのインスタンスが定義されていませんでした。 サンプルを試す場合は Ver.4.0.0 以降を使用してください。
BoudedEnum型クラスの例2
ところで、Bonded型クラスの回の最後に、意味深な事を書いていたことを思い出してください。それは、Bounded型クラスとEnum型クラスが実装されると、BoundedEnum型クラスの定義が(一意ではありませんが)自然に定まるという話です。
次の例では、Bounded型クラスとEnum型クラスの連携プレーを見てみることにしましょう。
まずは、方位を表すDirectionデータ型を次のように定義してみます。
data Direction
= North
| East
| South
| West
instance showDirection :: Show Direction where
show s = case s of
North -> "North"
East -> "East"
South -> "South"
West -> "West"
これに対して、Eqデータ型とOrdデータ型を実装すると次のようになります。
instance eqDirection :: Eq Direction where
eq s t = case s, t of
North, North -> true
East , East -> true
South, South -> true
West , West -> true
_ , _ -> false
instance ordDirection :: Ord Direction where
compare s t = case s, t of
North, North -> EQ
North, East -> LT
North, South -> LT
North, West -> LT
East , North -> GT
East , East -> EQ
East , South -> LT
East , West -> LT
South, North -> GT
South, East -> GT
South, South -> EQ
South, West -> LT
West , North -> GT
West , East -> GT
West , South -> GT
West , West -> EQ
「なんだ、Seasonデータ型と同じじゃないか。面倒臭い。」と思う方もいることでしょう。わざとです。次に、Bounded型クラスとEnum型クラスも実装します。
instance boundedDirection :: Bounded Direction where
top = West
bottom = North
instance enumDirection :: Enum Direction where
succ s = case s of
North -> Just East
East -> Just South
South -> Just West
West -> Nothing
pred s = case s of
North -> Nothing
East -> Just North
South -> Just East
West -> Just South
やはりSeason型クラスと同じですね。書いてる本人が言うのも何ですが、ここまで来ると嫌がらせにしか思えません。わざとです。今は耐えてください。
最後に、BoundedEnum型クラスを実装します。あまりにもSeason型クラスと同じなので、勢いで同じことを書いてしまいそうですが、ちょっと待ってください。簡単な方法があります。実は、Bounded型クラスを実装するには次のように書けば十分なのです。
instance boundedEnumDirection :: BoundedEnum Direction where
cardinality = defaultCardinality
toEnum = defaultToEnum
fromEnum = defaultFromEnum
ここで出てきたdefault○○○
という関数は、Data.Enumモジュールにあらかじめ用意してある関数で、Bounded型クラスとEnum型クラスが実装されていれば、BoundedEnum型クラスの定義を自動的に定めてしまいます。
これだけだとよくわからないので、正しく実装できていることを確かめて見ましょう。
> (cardinality :: Cardinality Direction)
(Cardinality 4)
> (toEnum :: Int -> (Maybe Direction)) <$> [0,1,2,3,4]
[(Just North),(Just East),(Just South),(Just West),Nothing]
> (fromEnum :: Direction -> Int) <$> [North,East,South,West]
[0,1,2,3]
ここで、注意深い皆さんは気づかれたことでしょう。Seasonデータ型の時は“1始まり”だったのに対し、Directionデータ型は“0始まり”になっています。先ほど括弧書きの中で「一意ではない」と言ったのはこの事でした。
default○○○
関数は、複数ある実装方法のうち、整数変換した値が“0始まりの連番”になるような実装を提供する関数です。“1始まり”や“跳び番”がある場合などは、独自に実装する必要があるということです。
それと、もう1つ言及するべき事があります。それは、上記の方法はパフォーマンスが悪いということです。上記で使用した3つのdefault○○○
関数は、bottom
から順に目的の値が見つかるまでトレースする作りになっています。なので、cardinalitiyが$n$の場合のパフォーマンスは$\mathcal{O}(n)$になります。用途によっては性能問題になる可能性を考慮しなくてはなりません。
BoudedEnum型クラスの例3
さて、Directionデータ型の例は多少役立つものではありましたが、開始番号が0固定である点とパフォーマンスが悪いという点で、制限がありました。
さらに、それ以上に面倒臭いのは、BoundedEnum型クラスを実装する前に、Eq型クラスやOrd型クラスなどの前提となる型クラスを実装しなければならないと言う点です。寛容な皆さんでも、二度も同じ構造のデータ型を作らされて、多少苛立ったかもしれません。
しかし、これまでの例はまだマシです。Ord型クラスの実装は、4×4の16パターンを書くだけで済んでいるのですから。例えば、次のような例を見ると根気強い皆さんでも一気にやる気が損なわれるのではないでしょうか。
data Planet
= Mercury
| Venus
| Earth
| Mars
| Jupiter
| Saturn
| Uranus
| Neptune
instance showPlanet :: Show Planet where
show Mercury = "Mercury"
show Venus = "Venus"
show Earth = "Earth"
show Mars = "Mars"
show Jupiter = "Jupiter"
show Saturn = "Saturn"
show Uranus = "Uranus"
show Neptune = "Neptune"
Planetデータ型は、太陽系の惑星を表すデータ型です。太陽系には8つの惑星が存在するので、値の個数は8個あります。今度は、このPlanetデータ型に対してBoundedEnumデータ型を実装してみましょう。
「面倒臭いので嫌です。」という声が聞こえてきそうです。少なくとも私は嫌です。Ord型クラスの実装に8×8の64パターンを書くなんて、、、
ところが、実は上手い解決法があるのです。逆転の発想です。それは、先にBoundedEnum型クラスを実装してしまうという方法です。
前提となる型クラスを先に実装しなくて良いのかという疑問が湧いてきそうですが、大丈夫です。プログラミング途中では一時的にコンパイルできない状態を経由しますが、最終的にコンパイルをする時点までに全てが揃っていれば問題ありません。一気に書いてしまいましょう。
まず、BoundedEnum型クラスを実装します。これだけは真面目に実装します。あとで楽するためです。
instance boundedEnumPlanet :: BoundedEnum Planet where
cardinality = Cardinality 8
toEnum i = case i of
1 -> Just Mercury
2 -> Just Venus
3 -> Just Earth
4 -> Just Mars
5 -> Just Jupiter
6 -> Just Saturn
7 -> Just Uranus
8 -> Just Neptune
_ -> Nothing
fromEnum p = case p of
Mercury -> 1
Venus -> 2
Earth -> 3
Mars -> 4
Jupiter -> 5
Saturn -> 6
Uranus -> 7
Neptune -> 8
特に困ることはないと思います。これだけだと前提となる型クラスが実装されていないのでコンパイルできません。しかし、先にBoundedEnum型クラスを実装したおかげで、残りの型クラスは、非常に楽に実装できるのです。下記を見てください。
instance eqPlanet :: Eq Planet where
eq a b = (fromEnum a) == (fromEnum b)
instance ordPlanet :: Ord Planet where
compare a b = compare (fromEnum a) (fromEnum b)
instance boundedPlanet :: Bounded Planet where
top = Neptune
bottom = Mercury
instance enumPlanet :: Enum Planet where
succ = defaultSucc toEnum fromEnum
pred = defaultPred toEnum fromEnum
たったこれだけです。今までの面倒臭さが嘘のように少ない行数で実装できてしまいました。
Eq型クラスとOrd型クラスについては一目瞭然ですね。整数に変換できればIntデータ型のインスタンスを流用できるので、いとも簡単に実装できてしまいます。ミスも減ります。
Bounded型クラスは、今まで通りフィールド値を定義するだけなので説明はいらないでしょう。Enum型クラスの定義に使われている2つのdefault○○○
関数は、やはりあらかじめ用意されている関数で、整数に変換した時の値が連番になっていればEnum型クラスの実装を自動的に定めてしまう関数です。
これだけで正しく動作することを確かめておきましょう。
> Jupiter == Jupiter
true
> Mercury == Mars
false
> Venus < Earth
true
> Mars < Earth
false
> (top :: Planet)
Neptune
> (bottom :: Planet)
Mercury
> (cardinality :: Cardinality Planet)
(Cardinality 8)
> (fromEnum :: Planet -> Int) <$> [Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune]
[1,2,3,4,5,6,7,8]
> (toEnum :: Int -> (Maybe Planet)) <$> [1,2,3,4,5,6,7,8]
[(Just Mercury),(Just Venus),(Just Earth),(Just Mars),(Just Jupiter),(Just Saturn),(Just Uranus),(Just Neptune)]
どうですか? コードがすっきりしただけでなく、第1章の総復習までもできてしまいました。
PureScriptで列挙型の性質を持つ型を作成する場合は、初めからこの方法で実装するのが良さそうですね。
見上げる景色と、見下ろす景色
さて、第1章はこれでおしまいですが、どうだったでしょうか? Eq型クラスから順に積み重ねてきたはずが、まさかの大逆転で、BoundedEnum型クラスを先に定義してしまうのが楽だった! なんていう興味深いゴールに辿り着いてしまいました。
第1章を書き上げるのに思った以上の時間がかかってしまいましたが、いよいよ次章からが本格的な代数学の話題になってきます。やっと書きたかった章に手をつけられます。
それでは、次回をお楽しみに。
演習問題
- (例1)の方法を使って、前回と前々回で作成した DrinkSizeデータ型に BoundedEnum型クラスを実装してください。
- 十二星座を表すZodiacデータ型を作成し、(例3)の方法を使ってBoundedEnum型クラスを実装してください。
参考資料
BoundedEnum型クラスについては下記のページを参考にしました。