Haskellで自然数から整数を構成する
こんにちは、やきつか(@sy_t_)です。
最近はとても暑いですね。僕は研究室の冷房強くしすぎてよくお腹を壊してるんですけど。
目次
はじめに
さて、今回は、純粋関数型言語であるHaskellを使って自然数を実装して、その自然数から整数を構成する、ということをやります。いきなりコードを貼り付けるのもアレなんで、自然数と整数の簡単な説明を通してから実装していきます。「それぐらいわかるわボケ」って人はその単元は飛ばしてください。
自然数ってなんぞや (ペアノの公理)
自然数ってなんでしょうかね。人に説明する時は「1,2,3,...って続く数字のことだよ」とか「0以上の全ての整数だよ」とかって言うんですかね。僕もそう言うと思います、めんどくさいし。
しかし、数学は厳密な学問なので、しっかりした定義が欲しくなります。そこで、ペアノさんが、次のような性質(ペアノの公理)を満たすものを自然数と呼ぶことにしようと決めました。自然数全体の集合をℕと表すことにします。
]
sucは後者関数と呼ばれる単射な写像で、"1を足す"ことに相当します。
ℕを外延的記法で書けば、という感じになります。
ここで、次のように各要素に名前をつけていきます。
このようにすると、我々がよくみるになりますね!
Haskellで自然数を実装する
自然数の定義はある程度理解できた(とする)ので、Haskellで自然数を実装してみましょう!
Haskellで自然数を定義するには、次のように書けばいいです。
data N = Zero | Suc N deriving (Eq, Ord, Show)
この文は、=の左辺と右辺に分けて読みます。dataは代数的データ型とよばれる、列挙型・構造体・共用体のようなものを生成するキーワードです、Cのstructのような使い方ですね。
=は代入だと思う人もいそうですが、「=の左辺っていうのは=の右辺に書いてあるもののことやで」の意味です。
右辺にあるZeroはコンストラクタとか構築子と呼ばれるやつで、列挙型の要素に相当します。
次にある|の記号は、「もしくは」と読みます。Suc NのSucは構築子なのですが、Nはフィールドとよばれるもので、なんらかの値(ここではN自身)にSucを適用したものです。
全体としては、「代数的データ型Nは、Zeroもしくは自身にSucを適用したもののことを言うんだな、うんうん」ぐらいの感じです。
代数的データ型に関しては、Haskell データ型・型クラス・型族 | Netsphere Laboratoriesあたりを読むといいと思います。
つまり、NというのはZeroもしくはSuc ZeroもしくはSuc (Suc Zero)もしくは...と無限の値を持つ代数的データ型です。
これは、Zeroを0、Sucを後者関数だとすると自然数そのものだと考えることができます。
「derivingってなんや?Eq,Ord,Show?」となっている人がいると思います、これは「代数的データ型Nの値は値同士で同値(Eq)であることを確かめられるし、比較(Ord)することもできるし、画面上に表示(Show)することができるんやで!!」というぐらいの意味です。詳しくは型クラス - ウォークスルー Haskellあたりを読んで下さい。(言い訳: Haskellの文法を解説する目的ではないので...)
自然数に二項演算を用意する
さて、Haskellで自然数を実装することができたので、足し算とか掛け算のような二項演算を作りましょう。
足し算は次のように定義します。
(+.) :: N -> N -> N Zero +. n = n n +. Zero = n n +. Suc m = Suc (n +. m)
これは、ペアノさんによる再帰的な加法の定義をそのままHaskellで書き下ろしただけです、これでちゃんと動きます。詳しくは加法 - Wikipediaを参考に。
同じ感じで、掛け算も実装していきます!
(*.) :: N -> N -> N Zero *. _ = Zero _ *. Zero = Zero n *. Suc m = n +. (n *. m )
+とか*に.(ドット)が付いてるのはHaskellの標準関数として+と*が定義されてしまっているからです、許して。
自然数が正しく動作していることを確認
全体のソースコードとしてはこんな感じ。
--いい感じに自然数を定義する data N = Zero | Suc N deriving (Eq, Ord, Show) --足し算を定義 Zero +. n = n n +. Zero = n n +. Suc m = Suc (n +. m) --掛け算を定義 Zero *. _ = Zero _ *. Zero = Zero n *. Suc m = n +. (n *. m )
ghciの対話形式で実行します。
- 同値性・順序性の確認
- 足し算の確認
Suc (Suc (Suc Zero))は3, Suc (Suc Zero)は2にあたるので、
Suc (Suc (Suc (Suc (Suc Zero))))が5=3+2という結果に一致していますね。
- 掛け算の確認
3*3=9になっていることを確認してください。(疲れた)
整数ってなんぞや
Haskellで自然数は実装できたので、一旦話を戻して、整数を数学的にどうやって作るのか説明したいと思います。
まず、自然数全体の2つの直積
を考えます。このを次のような同値関係
で割ってあげます。これが整数です。
の要素としては、
や
、
などが考えられると思います。これらの要素としての集合を、商写像
を用いて、
と表します。
さらに、を
、
を
と置くと、僕たちのよく知る整数
を垣間見ることができます!(何故このようにできるかは足し算や掛け算の実装でわかると思います。)
Haskellで整数を実装する
整数については理解できた(はず)なので、Haskellで実装していきましょう。といっても、そのまま書くだけなんですけど。
整数の元は2つの自然数全体の直積でした、これをHaskellで表現するには次のように書きます。
data Z = Z N N
「えっ、これだけ?」と思うかもしれません、これだけです。
「代数的データ型ZはNとNの2つの値を持つんだよ~」ぐらいのテンションです。(なげやり)
流石にこれだけでは、整数とは言えないので同値関係を定めておきます。これは、次のようにかけばOKです。
instance Eq Z where (Z n m) == (Z n' m') = n +. m' == m +. n'
「2つの自然数n,mを持つ整数と、2つの自然数n',m'を持つ整数が等しいとは、n+m'=m+n'が成立することである。」と読みます。
このように、ZをEq型クラスのインスタンスとして実装することで、いとも簡単に同値関係で割ることができます。やっぱりHaskellってすげぇよ。
整数を表示する時に、一意的に表示できるようにZをShow型クラスのインスタンスにします。
instance Show Z where show (Z n m) = show $ f (Z n m) where f :: Z -> Int f (Z Zero Zero) = 0 f (Z Zero (Suc m')) = (-1) + f (Z Zero m') f (Z (Suc n') Zero) = 1 + f (Z n' Zero) f (Z (Suc n') (Suc m')) | n' >= m' = 1 + f (Z n' (Suc m')) | n' < m' = (-1) + f (Z (Suc n') m')
これによって、例えばZ (Suc Zero) (Zero)もZ (Suc (Suc Zero)) (Suc Zero)も両方とも1と表示されます。(負の数ももちろん表示できる。)
整数に二項演算を用意する
整数において、足し算+は次のように定義されます。
また、掛け算は次のように定義されます。
なぜこのように定義すればうまくいくかは、実装のあと確認します。
Haskellで、上記の定義をそのまま書いてやります。
(Z n m) +.. (Z n' m') = Z (n +. n') (m +. m') (Z n m) *.. (Z n' m') = Z ( (n *. n') +. (m *. m') ) ( (n *. m') +. (n' *. m) )
+や*に.(ドット)が2つ付いてるのは、標準関数と上で自然数に対する演算で定義しているからです。(Num型クラスのインスタンスとして実装すればよかったのですが、+と*以外別にいらなかったので、このようにしました。)
整数が正しく動作していることを確認
ソースコード全体はこんな感じになりました。
--いい感じに自然数を定義して、演算も準備しておく data N = Zero | Suc N deriving (Eq, Ord, Show) Zero +. n = n n +. Zero = n n +. Suc m = Suc (n +. m) Zero *. _ = Zero _ *. Zero = Zero n *. Suc m = n +. (n *. m ) --整数はN×Nを適当な同値関係で割って構成する data Z = Z N N instance Eq Z where (Z n m) == (Z n' m') = n +. m' == m +. n' instance Show Z where show (Z n m) = show $ f (Z n m) where f :: Z -> Int f (Z Zero Zero) = 0 f (Z Zero (Suc m')) = (-1) + f (Z Zero m') f (Z (Suc n') Zero) = 1 + f (Z n' Zero) f (Z (Suc n') (Suc m')) | n' >= m' = 1 + f (Z n' (Suc m')) | n' < m' = (-1) + f (Z (Suc n') m') (Z n m) +.. (Z n' m') = Z (n +. n') (m +. m') (Z n m) *.. (Z n' m') = Z ( (n *. n') +. (m *. m') ) ( (n *. m') +. (n' *. m) )
それでは、確認していきます。
- 同値性の確認
- 表示の一意性
- 足し算
2-3=-1や2+3=5がうまく計算できていますね。
- 掛け算
2*3=6が計算できています。すごい。
あっ(-1)*(-1)ってどうなるんだろう
ちゃんと1になってくれましたね、めでたしめでたし。