Haskell には Num という型クラスがあって、足し算とか掛け算の基本的な演算はここで定義されている。標準ライブラリで定義されているインスタンスとしては、 Int, Integer, Rational, Complex a などがある。
一言で言えば Num クラスは環っぽいものに対応するのだが、いろいろとアレな点がある。これを設計した人は、インスタンスとして整数、有理数、浮動小数点数、複素数くらいしか想定していないのではないか。
Haskell 2010 での Num クラスの定義を引用しておく。
class (Eq a, Show a) => Num a where
(+), (-), (*) :: a -> a -> a
negate :: a -> a
abs, signum :: a -> a
fromInteger :: Integer -> a
目次
Num クラスの悪いところ
Eq, Show がスーパークラスになっている
Haskell 2010 の仕様では、上に書いたように Eq と Show が Num のスーパークラスとなっている。しかし、最近の GHC ではこれらは Num のスーパークラスではなくなっているので実用上は問題ない。
abs, signum が邪魔
実数(の部分環)の場合は、これらの意味ははっきりしている。
複素数 (Data.Complex) の場合は、絶対値 \({\lvert z\rvert}\) および複素数をその絶対値で割ったもの \(\frac{z}{\lvert z\rvert}\) になる。やや苦しい。abs, signum のせいで Num (Complex a) の制約が Num a ではなくて RealFloat a となっている。abs, signum さえなければ、 Complex Integer みたいなものが普通に使えたはず。
ちなみに、 abs の返す値は実数 a ではなく複素数 Complex a である。複素数の絶対値と言ったら普通は実数を返して欲しい (magnitude :: Complex a -> a
) と思われるので、虚部が 0 の複素数を返す abs :: Complex a -> Complex a
は存在意義が不明である。
複素数の場合ですら苦しいのに、それ以外の環で abs や signum をどう定義しろというのか。アホか。
もっときめ細かい階層にして欲しい
例えば、ベクトルを表す型を作ったとする。ベクトルどうしの足し算、引き算は、当然 + や – を使って書きたい。しかし、ベクトルどうしの掛け算 * というのは一般には定義されないので、このベクトル型を Num クラスのインスタンスにはできない。そうすると、ベクトルどうしの足し算、引き算を + や – で書くこともできない。
何が言いたいかというと、足し算 + や引き算 – (およびリテラルの 0)は Num クラスから独立した別のクラスに入れて欲しかった。
自然数のことを考えると、足し算と引き算も別のクラスに入れるべきかもしれない。
ただ、 Num クラスを分割するとしても、どの程度きめ細かい階層にするかという問題で意見が分かれそうである。
-- AdditiveSemigroup とか AdditiveMonoid みたいなクラスを作って、 (+) と zero を分離すべき、という考え方もあるかもしれない
class AdditiveGroup a where
(+) :: a -> a -> a
(-) :: a -> a -> a
negate :: a -> a -- 単項マイナス
zero :: a -- `0' と書いたらこれになって欲しい
-- (*) と one を分けて、単位元を持たない環も表せるようにした方がいいかもしれない
class AdditiveGroup a => Ring a where
(*) :: a -> a -> a
one :: a -- `1' と書いたらこれになって欲しい
fromInteger :: Integer -> a -- 整数リテラル
-- 現行の Num クラスと互換
class Ring a => Num a where
abs :: a -> a
signum :: a -> a
オレオレ Num クラスを作る際に障害になること
Num クラスが不満なら、自分でそれっぽい型クラスを作ればいいじゃん!と思われるかもしれない。だが、それにはいろいろ障害がある。
構文との癒着
(+)
, (-)
, (*)
, abs
, signum
は普通の演算子および関数なので、import Prelude hiding しつつ自前のものを同じ名前で定義すれば良い。
問題は、Haskellの一部の文法が Prelude の Num クラスを直に参照していることだ。具体的には、単項マイナスは Prelude の Num クラスの negate
を参照しているし、整数リテラルは同 fromInteger
を参照している。(Num クラスではないが、同様の問題として、浮動小数点数が Floating クラスの fromRational
を参照するという問題がある)
まあ、これは GHC の RebindableSyntax 拡張を使えば、これらの構文で自前で定義した negate
や fromInteger
を参照するようにできる。ただ、使う側のすべてのソースコードに {-# LANGUAGE RebindableSyntax #-}
プラグマを書く必要があるというのはあまり嬉しくない。
インスタンスが地味に多い
自分で代替クラスを作る場合は、既存の数値型のインスタンスを自前で定義する必要がある。
- Prelude
- Int, Integer, Rational (Data.Ratio Integer), Float, Double
- Data.Int
- Int8, Int16, Int32, Int64
- Data.Word
- Word, Word8, Word16, Word32, Word64
- Data.Ratio
- Ratio a
- Data.Complex
- Complex a
- Foreign.C.Types
- CChar, CSChar, CUChar, CShort, CUShort, CInt, CUInt, CLong, CULong, CPtrdiff, CSize, CWchar, CSigAtomic, CLLong, CULLong, CIntPtr, CUIntPtr, CIntMax, CUIntMax, CClock, CTime, CFloat, CDouble
これら全てについて、オレオレ Num クラスのインスタンスを定義する必要がある。まあ、FFI 関連の型は一部省略してもいいかもしれない。
標準ライブラリか、野良のライブラリか
第三者が作った Haskell ライブラリにとっては、 Num クラスこそが数を表すクラスであり、オレオレ Num クラスなど知ったことではない。第三者が作ったライブラリの関数の制約は標準の Num クラスだし、第三者が作ったライブラリで定義される型は標準の Num クラスのインスタンスになっているかもしれないがオレオレ Num クラスのインスタンスにはなっていない。
なんだかんだ言ってこれが一番どうしようもない問題である。
既存の議論と既存の代替品
Haskell の Num クラスがクソだというのは、Haskell をやっていればきっと誰でも行き当たる問題なので、それに関する議論・考察も、まあ、ある。Haskell Wiki にも Mathematical prelude discussion というページがある。
Haskell-cafe での直近の議論を探したところ、2015年9月のものが見つかった: [Haskell-cafe] Thoughts about redesigning “Num” type class
この話題になると必ず話題に上がる代替 Prelude としては、 Numeric Prelude が有名であろう。このライブラリは、そこそこ(Num よりは)きめ細かい型クラスの階層を提供してくれる。
しかし、 Numeric Prelude は Prelude の代替にとどまらず、他にもいろいろな型と型クラスを提供しているので、でかい。あなたが数学関連のライブラリ作者だとして、ちょっと標準の Num クラスが気に入らないからといってわざわざ Numeric Prelude を依存関係に追加してまで使いたいだろうか?
結論
Numeric Prelude という代替品はあるが、実際に他の Haskell ライブラリから使われている雰囲気は薄い。Numeric Prelude じゃないオレオレ Num クラスならなおさらである。結局、標準の Num クラスをどうにかしないとダメなのだ。
ということで、タイムマシンで過去に遡って Num クラスの定義を変えるしかない。誰か頼む〜〜
真面目な話、 Monad クラスに関しては Applicative のサブクラスにしようとか fail を分離しようとかの動きがある(GHC で実現しつつある)のに、 Num についてその手の実現可能性のある話を聞かないのは悲しい。自分で提案しろってか。
クラスの定義を変えてもインスタンスの定義に手を加えずにすむような仕組みがあれば、互換性の問題を気にせずに Num クラスを分割とかできたかもしれないが、そういうのは難しいのだろうか。
ピンバック: Haskell (GHC) の型レベル自然数とリフレクションを試してみる | 雑記帳