purescript
algebra

Eq型クラス 〜型クラスで見るPureScript〜

※この記事は「型クラスで見るPureSciript」の連載記事です。

等しいか等しくないか

数学でもプログラミングでも、2つの値が等しいか等しくないかを区別できることは、最も基本的な概念です。これを区別できなければ、世の中のほとんどの論理が成り立ちません。そこで、まずは等しいか等しくないかを区別するためのイコール演算を定義しましょう。

その前に、イコール演算が満たすべき条件を思い出しておきましょう。みなさんも中学生の時に習ったと思いますが、忘れている人も多いと思うので復習しておきましょう。

右辺の値と左辺の値が等しいことを表す等号演算子==は次の性質を満たす必要があります。

  1. 反射律:$a==a$
    aa自身と等しい」
  2. 対称律:$a==b\;\Rightarrow\;b==a$
    abが等しければ、baも等しい」
  3. 推移律:$a==b\;\text{and}\;b==c\;\Rightarrow\;a==c$
    abが等しく、且つ、bcが等しければ、acも等しい」

難しいことを言っているように聞こえるかもしれませんが、よく考えると当たり前のことしか言っていません。数学というものは、普段無意識に理解しているような当たり前のことを、小難しい言葉を使って言い直す捻くれ者なのです。

当たり前の性質ではありますが、自分でイコール演算を実装する際には、これらの性質を満たさなくなるバグを作らないように、気をつける必要があります。

*この連載記事では、できる限り、演算子の記号を、通常数学で用いられる記号ではなく、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型クラスについては下記を参考にしました。

また、イコール演算については下記を参考にしました。