※この記事は「型クラスで見るPureSciript」の連載記事です。
等しいか等しくないか
数学でもプログラミングでも、2つの値が__等しいか等しくないか__を区別できることは、最も基本的な概念です。これを区別できなければ、世の中のほとんどの論理が成り立ちません。そこで、まずは__等しいか等しくないか__を区別するための__イコール演算__を定義しましょう。
その前に、__イコール演算__が満たすべき条件を思い出しておきましょう。みなさんも中学生の時に習ったと思いますが、忘れている人も多いと思うので復習しておきましょう。
右辺の値と左辺の値が等しいことを表す__等号演算子__==
は次の性質を満たす必要があります。
- 反射律:$a==a$
「a
はa
自身と等しい」 - 対称律:$a==b;\Rightarrow;b==a$
「a
とb
が等しければ、b
とa
も等しい」 - 推移律:$a==b;\text{and};b==c;\Rightarrow;a==c$
「a
とb
が等しく、且つ、b
とc
が等しければ、a
とc
も等しい」
難しいことを言っているように聞こえるかもしれませんが、よく考えると当たり前のことしか言っていません。数学というものは、普段無意識に理解しているような当たり前のことを、小難しい言葉を使って言い直す捻くれ者なのです。
当たり前の性質ではありますが、自分で__イコール演算を__実装する際には、これらの性質を満たさなくなるバグを作らないように、気をつける必要があります。
*この連載記事では、できる限り、演算子の記号を、通常数学で用いられる記号ではなく、PureScriptのプログラムコード内で使われる記号に一致するように記述することにします。
Eq型クラス
それではEq型クラスについて、見ていきましょう。Eq型クラスはData.Eq
モジュールで次のように定義されています。
class Eq a where
eq :: a -> a -> Boolean
つまり「Eq型クラスには、データ型a
の引数を2つ取り、Boolean型の値を返す関数eq
が定義されている」ということです。この関数eq
が、先ほどから何度も言っている__イコール演算__を定義しています。
言葉で説明してもよく分からなくなるだけなので、具体例を見て見ましょう。
Eq型クラスの例1 座標点を表すデータ型
2次元の座標点を表すデータ型Point
を次のように定義します。
data Point = Point Number Number
テスト時に便利なようにShow型クラスも実装しておきましょう。
instance showPoint :: Show Point where
show (Point x y) = "Point (" <> show x <> ", " <> show y <> ")"
それでは、このPointデータ型に対してEq型クラスを実装して見ましょう。
2つの座標点 $P=(p_x,p_y)$ と $Q=(q_x,q_y)$ が等しいということは、$P$ と $Q$ の $x$座標と $y$座標が、それぞれ等しいということです。つまり $p_x==q_x;\text{and};p_y==q_y$ ということです。
このことを考慮すると、Eq型クラスの実装は次のようになります。
instance eqPoint :: Eq Point where
eq (Point x1 y1) (Point x2 y2) = x1 == x2 && y1 == y2
実装できたらPSCIで軽くテストして見ましょう。
> a = Point 1.0 2.0
> b = Point 2.0 3.0
> c = Point 1.0 2.0
> eq a b
false
> eq a c
true
Eq型クラスの関数
notEq関数
notEq :: forall a. Eq a => a -> a -> Boolean
notEq関数は、その名の通りEq関数の反対です。2つの値が等しい時にfalse
を返却し、等しくない時にtrue
を返却します。
eq関数が定義されていれば、notEq関数の定義は一意に定まるので、Eq型クラスに属するデータ型に対してであれば、わざわざnotEq関数を定義しなくても実行可能です。これが型クラスを使うメリットで、限られた幾つかの性質を満たしさえすれば、関連する関数の定義が派生的に定まってしまいます。
Eq型クラスの例2 長さを表すデータ型
それではnotEq関数を試して見ましょう。先ほどのPointデータ型を使っても良いのですが、ここでは新しいデータ型を定義してみましょう。
data Length = Inch Number | CentiMetre Number
Lengthデータ型は長さを表すデータ型です。長さの単位には色々ありますが、ここではインチとセンチメートルから選択可能にしておきましょう。手始めにShow型クラスを実装するとこんな感じでしょうか。
instance showLength :: Show Length where
show (Inch l) = show l <> " in"
show (CentiMetre l) = show l <> " cm"
次にEq型クラスを実装して見ましょう。Lengthデータ型は2種類の単位を選ぶことができるので、異なる単位同士を比較するには単位換算する必要があります。1インチは25.4ミリメートルらしいので、Eq型クラスの実装は次のようになります。
instance eqLength :: Eq Length where
eq (Inch k) (Inch l) = k == l
eq (CentiMetre k) (CentiMetre l) = k == l
eq (Inch k) (CentiMetre l) = k * 2.54 == l
eq (CentiMetre k) (Inch l) = k == l * 2.54
実装できたらPSCIで軽くテストして見ましょう。
> j = CentiMetre 1.0
> k = CentiMetre 2.54
> l = Inch 1.0
> notEq j l
true
> notEq k l
false
Eq型クラスの演算子
(==)演算子
infix 4 eq as ==
(==)演算子はeq関数の演算子版です。a == b
のように記述するとeq a b
と解釈されて、eq関数が実行されます。
(/=)演算子
infix 4 notEq as /=
(/=)演算子はnotEq関数の演算子版です。数学記号の「$\neq$」をイメージしているようです。これも同じようにa /= b
と記述するとnotEq a b
が実行されます。
Eq型クラスの例3 角度を表すデータ型
それでは、もう1つ新しいデータ型を作って試して見ましょう。
data Degree = Degree Int
instance showDegree :: Show Degree where
show (Degree n) = "Degree " <> show n
Degreeデータ型は角度を表すデータ型です。ここでは簡単のため、角度の最小単位を1°としておきます。このDegreeデータ型にEq型クラスを実装して見ましょう。
ところで、角度というものはクルッと回って一周すると同じところに戻ってきます。つまり、ぴったり360°の差がある角度は等しいということができます。正確には360°の整数倍の差であれば同一視できます。すると、Eq型クラスの実装は次のようになります。
instance eqDegree :: Eq Degree where
eq (Degree n) (Degree m) = 0 == mod (n - m) 360
早速、試して見ましょう。
> Degree 90 == Degree 90
true
> Degree (-90) == Degree 90
false
> Degree (-90) == Degree 270
true
みんな違って、みんな楽しい
これで少しはEq型クラスの使い方が分かったでしょうか。同等の機能は他のプログラミング言語にもあるので、簡単だよって方も多いでしょう。
これから先、様々な型クラスについて見ていくことになります。少しずつ書き上げていくので、初めのうちは簡単な方クラスにお付き合いください。それでは、また次回。
演習問題
重さを表すWeightデータ型を定義し、Eq型クラスを実装してみてください。単位にはグラムとオンスを選択可能とし、$1\text{g}=28.35\text{oz}$として実装してみてください。
参考文献
Eq型クラスについては下記を参考にしました。
- https://pursuit.purescript.org/packages/purescript-prelude/3.1.0/docs/Data.Eq
- https://github.com/purescript/purescript-prelude/blob/v3.1.0/src/Data/Eq.purs
また、イコール演算については下記を参考にしました。