先日プログラミングコンテストの過去問を haskell でやった時に、べき乗をなんやかんやするロジックを書いた時に最終結果が12340000
で欲しかったのに12340000.0
になってしまって通りませんでした。恥ずかしい。
恥ずかしいけど、聞くは一時の恥聞かぬは一生の恥の解説とも言うし、知らないことは素直に学んで修めればなかったことにできるんです。できんの?
よくわからんけど、要するに曖昧なまま使ってたInt
みたいなものをきっちりまとめるぜって、そんな話。
かるーく背景
普段は java / DDD で契約管理のシステムを作ってるんだけど、扱う数字なんてたかだか数万の整数くらいなんだよね。ちょっとした円だとか契約の数を数えたりだとか、その程度。
なんと月の請求に日割りがないしね。驚きだね。
DDD の value object とかのおかげで、生のInt
とかを触ることもあんまりないしね。
なのでこの記事を書く直前の例えば java の理解度はだいたいこんな感じ。
「byte
? よくわからんけど怖い」
「long
? 長そ〜」
「BigInteger
? でかそ〜」
「float
? ふわふわ〜」
「double
? 何が倍なの〜?とうおるるるるるるるる」
ってくらいの理解度。これ大マジ。
(double のイタリア語が doppio ってことだけは知ってたんだよォォォ)
なのでそれくらいの人がまとめたんだよってことだけ了承してね!
java 版もよろしく
この記事の確認言語には haskell を使っています。
もともと haskell で確認してたのですが、いくつかの言語を確認した方がより理解が進むかと思って java でも試してみました。
python でも少し確認したりしたのですが、記事は本懐の haskell と仕事で使ってる java にしようかと思います。
というわけで、こっちもよろしく。→ java の int と Integer と BigInteger の違いや float や double を理解する
まずは概要
プログラミングに入る前に、数学の話です。
数学といっても代数とか圏論とかの怖いやつはでてこないです。僕も怖いので。
だいたいが中学校くらいまでの話。
まずはこれを見てください。
ざっくり説明します。
実数と虚数
普段目にする数はだいたい実数
です。
対して虚数
は便利なので発明された数ですが、現実には存在しません。
代表的なのが√-1
もしくはそれをi
と表現したものですね。わかりやすいですね。僕はよくわかりません。
虚数
は二乗して 0 未満の実数になる数で、実数
はそれ以外と定義されます。
ここを細かく考える気はないので、「だいたい実数
」くらいで大丈夫です。
有理数と無理数
実数
の分類は真剣に考えます。
実数
は有理数
と無理数
に大別されますが、有理数
は整数の比で表現できる数です。
対してそれ以外の整数の比で表現できない数を無理数
と言います。
整数と有限小数と循環小数
いずれも有理数
です。
整数
は説明するまでもありませんね。
3
は3/1
と表現できるので有理数
です。
有限小数は0.5
の様な終わりのある小数
です。
1/2
の様に整数
の比で表現できます。
対して0.333...
や0.142857142857142857...
の様に同じ数字の繰り返しが無限に続く小数
を循環小数
と言います。
これも1/3
や1/7
の様に整数
の比で表現できます。
負の整数と正の整数とゼロと自然数
整数
は一番馴染みがあるのであまり問題ないと思いますが、一応。
負の整数
は-1
や-5
のことで、-5/1
の形で表現できます。
0
は0/1
ですね。
正の整数
についても同様です。
また、正の整数
を自然数
とも言います。(0
を含めるかは本記事では問いません)
小数と分数
少数
と分数
についての補足です。
分数
は数の比で表現される数であり、一見有理数
と同じな気がします。
が、有理数
は整数の比なので、分数の方が広い概念です。
例えば1/√2
なんてのもありです。これは整数の比ではないので無理数
です。
(だいたい 0.7 なので二乗するとだいたい0.5
で0
より大きいので虚数
ではないですね。)
また、例えば無限小数
というものがありますが、先ほどの図で言うと、有理数
の循環小数
も無理数
も無限小数
です。
循環しているかしていないかの違いですね。
押さえておきたい英語
以上の要点を押さえつつ、我々プログラマは英単語も知らないと困るので、ざっくり整理しておきます。
(と言っても絵には英語も入ってますが。)
実数
はreal number
で虚数
はimaginary number
です、イメージつきやすいですね。
あまり馴染みはないですが、有理数
はrational number
です。コピペで書いてると稀にぶち当たります。
ratio
が比率
という意味なので変数名で使ったことがある人もいるのではないでしょうか。
また、絵にはないですが小数
はdecimal
で分数
はfraction
です。
プログラミングの世界へ ( haskell )
さっそく haskell でサンプルコードを見たいところですが、人間の世界とコンピュータの世界では大きく違うことがあります。
それは「メモリが有限」ということです。
どこにそれが関係するかと言うと、例えば「すげーでけー数」と「無限小数」です。
固定長整数
例えば haskell のInt
は僕の環境では 64bit 固定の整数です。
コンピュータのメモリには限界があるので、数値を 64 の0|1
の範囲に限定して表現します。
多倍長整数
対して多倍長整数
は扱う数に応じて動的にメモリを確保する数値の表現方法です。
理論上は無限の数を扱うことができます。(もちろんコンピュータのメモリの許す限りですが。)
固定長整数と多倍長整数
みんな大好きオーバーフローはこの固定長整数
が引き起こします。
haskell のInt
を 8bit 固定の整数だと仮定して考えます。
頭の 1bit を正負の符号に、残りを値の表現に使います。
0000|0000
から1
ずつ増加を始め、0111|1111
から1000|0000
になるところでオーバーフローし、
1111|1111
から1|0000|0000
になるところで 9bit 目が範囲外になり0000|0000
として扱われます。
(見やすくするために 4 桁ごとに|
を入れています。)
対してInteger
は多倍長整数
です。
こいつは桁あふれが起きそうになると、動的にメモリを確保するのでオーバーフローしません。
(符号や値の保持については実装方法によるので、上図はイメージです。)
固定長整数
はメモリ効率や性能に優れ、多倍長整数
は精度に優れます。
これらは適材適所です。
浮動小数点
整数
と同じく小数
においても同様の考え方があります。
浮動小数点
とは数値の表現方法の一つで、固定長
の仮数部
と指数部
を持つ表現方法です。
ざっくり仮数部
は値で指数部
は桁を表していると考えれば大丈夫。
例えば二進数の0.00000101
は101 * 2^-8
の様に表されます。
ただこれだと10.1 * 2^-7
とかでも表現できちゃうので、IEEE754
と言う規格で仮数部
は1.x
にすると決まってます。なので1.01 * 2^-6
です。
1.01e-6
なんて書いたりもします。
コード書いていてたまに出るe
入ってるやつはこれだね。怖かったけど克服したぞ。
仮数部
と指数部
によって小数点を打つ位置が変わってくるので浮動小数点
と言うのかな。
一方で対になる単語は固定小数点
で、例えば整数
がこれに含まれます。
haskell で確認
前置きが長くなりました。ここからはガシガシ haskell で確認していきます。
type | 説明 |
---|---|
Int | 64bit 固定長整数 |
Integer | 多倍長整数 |
Float | 単精度浮動小数点 ( 32bit ) |
Double | 倍精度浮動小数点 ( 64bit ) |
以下のコードは repl の実行とその結果です。
また、この絵は「なんとなく数値っぽい型とそこに定義されていた関数を整理した絵」です。
Int
はIntegral
クラスのインスタンスになっているのでInt -> Integral
と線を入れています。
ghci> :info Int
data Int = GHC.Types.I# GHC.Prim.Int# -- Defined in ‘GHC.Types’
instance Eq Int -- Defined in ‘GHC.Classes’
instance Ord Int -- Defined in ‘GHC.Classes’
instance Show Int -- Defined in ‘GHC.Show’
instance Read Int -- Defined in ‘GHC.Read’
instance Enum Int -- Defined in ‘GHC.Enum’
instance Num Int -- Defined in ‘GHC.Num’
instance Real Int -- Defined in ‘GHC.Real’
instance Integral Int -- Defined in ‘GHC.Real’
instance Bounded Int -- Defined in ‘GHC.Enum’
Int
先述した通りInt
は固定長整数
です。
これを見る限り、java のLong.MAX_VALUE
と同じなので 64bit ですね。
ghci> maxBound :: Int
9223372036854775807
例えばInt
の上限値に+1
すると、オーバーフローします。
ghci> ( maxBound :: Int ) + 1
-9223372036854775808
ところでmaxBound
はBounded
に定義されています。
他にBounded
のインスタンスになっているものは例えばBool
やOrdering
があります。
ghci> ( minBound, maxBound ) :: ( Bool, Bool )
(False,True)
ghci> ( minBound, maxBound ) :: ( Ordering, Ordering )
(LT,GT)
Integer
対してInteger
は多倍長整数
です。気前よく巨大な整数
を扱ってみましょう。
ghci> n = 9223372036854775807 :: Integer
ghci> n + 1
9223372036854775808
Int
の上限に加算してもオーバーフローしてません。
もっと思い切りよく足しても全然大丈夫。
ghci> n + n
18446744073709551614
ところでInteger
がBounded
のインスタンスでないのは、多倍長整数
なので上限がないからですね。ここまで理解していれば至極納得。
余談 型注釈
ところでサンプルコードで:: Int
や:: Integer
の様に型注釈をつけていますが、これをつけないとどうなるでしょうか。
ghci> n = 42
ghci> :t n
n :: Num p => p
まだ具体的には断定されていないみたいですね。
haskell は数値リテラル
を必要になるまでは抽象的に扱っておいて、必要に応じて型を決めてくれます。
例えばこのn
の定義後にtoInteger n
なんてあれば haskell はIntegral
だと思って扱おうとするわけです。
冒頭で触れた僕のわからなかったエラーの原因は完全にこれですね。
僕にはf 8 0 []
って書いた時の値はInt
に見えていたのですが、コンパイラはそう見てなかったということなのでしょう。悲しい 未熟ゆえのすれ違い。
ちょっと確認してみましょう、以下のコードはどちらも実行できます。
main = do
let n = 42
print $ toInteger n
-- print $ floor n
main = do
let n = 42
-- print $ toInteger n
print $ floor n
が、これは実行できません。
main = do
let n = 42
print $ toInteger n
print $ floor n
n
はIntegral
なのかRealFrac
なのかどっちだ、ってなってしまうからですね。
ですがこれは実行できます。
main = do
let n = 42
print $ toRational n
print $ floor n
これはRealFrac
の定義がこうなっているからです。
ghci> :info RealFrac
class (Real a, Fractional a) => RealFrac a where
絵を目で辿れば把握できそうですね。整理して良かった。
Int と Integer の変換
絵を見ると、Num
にあるfromInteger :: Integer -> a
を使ってNum
にしてから:: Int
で断定すればたどり着きそうです。
ghci> n = 5 :: Integer
ghci> :t fromInteger n
fromInteger n :: Num a => a
ghci> fromInteger n :: Int
5
で、想像の通り精度の低い方から高い方への変換はできますが、逆は正しくは行えません。
ghci> n = ( 9223372036854775807 :: Integer ) + 1
ghci> fromInteger n :: Int
-9223372036854775808
逆にInt
からInteger
は絵にそのものずばりがあります。
ghci> n = 5 :: Int
ghci> toInteger n
5
余談 加算
絵を見ると、+
はNum
に定義されています。
実は普段の+
はInt
の足し算をしている気がするかもしれないけど、先ほどの余談で確認した通り明示しない限りInt
とは断定されていません。
ghci> x = 1
ghci> y = 2
ghci> n = x + y
ghci> :t n
n :: Num a => a
ここからx
とy
がInt
, Integer
, Float
, Double
どれに断定されても大丈夫です。
ghci> x = 1 :: Int
ghci> y = 2 :: Int
ghci> n = x + y
ghci> :t n
n :: Int
ghci> x = 1 :: Float
ghci> y = 2 :: Float
ghci> n = x + y
ghci> :t n
n :: Float
ただし(+) :: a -> a -> a
なので、x
とy
とn
の型は全て同じである必要はあります。
ずれている場合は型を揃えてから計算します。
ghci> x = 1 :: Integer
ghci> y = 2 :: Int
ghci> n = x + ( toInteger y )
ghci> :t n
n :: Integer
ghci> n = ( fromInteger x ) + y
ghci> :t n
n :: Int
ex1 と ex2 の違いももう大丈夫です。ちゃんとわかります。
ex2 はせっかくfromInteger
でx
をNum
まで抽象化したけど、y
がInt
なのでx
もInt
に断定されている。なのでオーバーフローする。はず。
ghci> x = 9223372036854775807 :: Integer
ghci> y = 1 :: Int
ghci> x + ( toInteger y )
9223372036854775808
ghci> ( fromInteger x ) + y
-9223372036854775808
うん。ちゃんと理解できてる。
余談 除算
絵を見ると(+)
はNum
に定義されていますが(/)
は違いますね。
なので普段行なっている4 / 2
の演算は実はInt
でもInteger
でも行われてないのです。
ghci> a = 4
ghci> b = 2
ghci> a / b
2.0
ghci> a = 4 :: Integer
ghci> b = 2 :: Integer
ghci> a / b
No instance for (Fractional Integer) arising from a use of '/'
Num
に抽象化してから計算しましょう。
ghci> ( fromInteger a ) / ( fromInteger b )
2.0
Int と Integer の違いまとめ
-
Int
は上限があるぞ -
Integer
は上限が(メモリの許す限り)ないぞ -
Int
からInteger
への変換は値が壊れる可能性があるぞ - 型を明記しない限り、haskell は必要に応じて型を決めるぞ
ってことですね。
Float, Double
整数
は押さえましたね。次は小数
です。
Double
の何が倍
なんだって思ってましたが、勉強すれば明瞭ですね。
Float
は 32bit を使って、Double
は 64bit を使って値を表現するということでした。だから倍精度。
コンピュータのメモリが有限である以上無限小数
を完全に表現することは不可能なので、誤差が出る前提で扱わなければなりません。
例えば十進数の0.01
は二進数だと有限で表現することができません。
有限で表現できない以上どこかで諦めなければいけず、それを繰り返せば誤差が大きくなるのはなんとなく感覚で理解できますね。
で、どんな誤差が出るか、です。試してみましょう。
ghci> f = 0.01 :: Float
ghci> sum $ replicate 100 f
0.99999934
ghci> d = 0.01 :: Double
ghci> sum $ replicate 100 d
1.0000000000000007
Double
の方が1.0
に近いですね。
相互変換
Float
とDouble
の変換も、Int
とInteger
と同様に精度の高い方から低い方へ変換すると壊れます。
どうやって変換すれば良いのかな...
絵を見る限りではRational
を経由したら良いのかな...
ghci> f = 0.99999934 :: Float
ghci> d = 1.0000000000000007 :: Double
ghci> fromRational ( toRational f ) :: Double
0.9999993443489075
ghci> fromRational ( toRational d ) :: Float
1.0
絵には出せてなかったけど、調べたらこっちの方が良さそうかな。
ghci> realToFrac f :: Double
0.9999993443489075
ghci> realToFrac d :: Float
1.0
どちらにせよDouble
からFloat
にした場合は欠けてしまっていますね。
またそもそも有限なので、単純に以下の様な値で誤差が出ます。
ghci> ( 10 :: Double ) / ( 3 :: Double )
3.3333333333333335
ghci> 1.00000001 :: Float
1.0
では誤差の出る小数の計算はどうすれば良いのでしょうか。
Rational
冒頭のベン図の様な絵で確認すると、有理数
のことです。
さらっと冒頭で説明したこれのことですね。
実数
は有理数
と無理数
に大別されますが、有理数
は整数の比で表現できる数です。
haskell にはそのものずばりなRational
という型があって、そのまんま分数の様な形で扱っているみたいです。
ghci> 1 :: Rational
1 % 1
ghci> 0.01 :: Rational
1 % 100
Rational
はRatio Integer
のエイリアスになってるみたいです。
ghci> :info Rational
type Rational = GHC.Real.Ratio Integer -- Defined in ‘GHC.Real’
Ratio
は単純に比という意味なので、Ratio Integer
は ↓ を表現しているのかな、今ならわかる気がしますね。
分数
は数の比で表現される数であり、一見有理数
と同じな気がします。
が、有理数
は整数の比なので、分数の方が広い概念です。
Rational
も普通に(+)
が可能です。
ghci> r = 0.01 :: Rational
ghci> r
1 % 100
ghci> r + r
1 % 50
面白いですね、そのまんま分数の足し算みたいです。
これは 100 回加算しても誤差が出ない気がしますね。
ghci> r = 0.01 :: Rational
ghci> sum $ replicate 100 r
1 % 1
すごい。
小数
にしたければ、こんな感じでしょうか。
ghci> fromRational ( sum $ replicate 100 r ) :: Float
1.0
もちろん、Rational
から小数
にする時に誤差が出ることはありえます。
ghci> fromRational $ ( 10 :: Rational ) / ( 3 :: Rational )
3.3333333333333335
ここは気をつけなければいけませんね。
Real, RealFrac, Fractional, Floating, RealFloat
こいつらは一体何者なのでしょうか。
Real
Real
はまぁシンプルに実数でしょう。
RealFrac
小数
はdecimal
で分数
はfraction
です。
有理数
は整数の比で表現できる数です。
対してそれ以外の整数の比で表現できない数を無理数
と言います。
これを思い出すと、RealFrac
は実数の分数
なので**有理数
も無理数
も含むけど整数
は含まない**と言ったところでしょうか...
整数
を含むなら実数
と変わらない気もするし、絵を見る限り整数
は関係してません。
切り捨てや切り上げもここで定義されています。
Fractional
今の僕にとってはここは興味の対象外なので、かなり推測が入ってしまいます。お許しください。
Real
とは関係してない分数
なので、おそらくは虚数
の分数
を考慮しているのではないかと思います。
2/√-1
とかですかね...
Floating, RealFloat
浮動小数点
とは数値の表現方法の一つで、固定長
の仮数部
と指数部
を持つ表現方法です。
これがそのもの floating point number だと思うのですが、Real
を冠してないFloating
が何かは正直まだ理解できてません。
pi
とかはRealFloat
だと思うんだけどな...
ちなみにこの記事まとめるまでは「Float
がRealFloat
なのはわかるけどDouble
もRealFloat
なのかよ。イミワカンネ。」
とか思ってたんですよね...
Float
とDouble
は仕組みは同じで精度の違いだけだからね...学んだよ...
余談 べき乗
実は冒頭で触れた僕のエラーの直接原因は(**)
でした。
2 ** 3
が8
と出力されて欲しかったのに8.0
と出力されてしまうので「よくわからんけど適当にそこかしこに:: Int
って書いてみよ」とかやって変なことになったのです。
なぜ8.0
になるのかは、もう学んだので大丈夫です。
Int
で扱うと巨大な数でオーバーフローしますし、Integer
にしたら小数
が扱えないからですね。
浮動小数点
にして先述のIEEE754
の規格通り1.0e20
みたいに大きい数を扱えば、オーバーフローしないし小数
も大丈夫です。
ghci> 3000000000 ** 2
9.0e18
ghci> 4000000000 ** 2
1.6e19
ghci> 1.5 ** 2
2.25
仮に(**)
の結果がInt
だとすると、こんな感じになってしまうでしょう。
ghci> (***) a b = floor $ a ** b :: Int
ghci> 2 *** 3
8
ghci> 3000000000 *** 2
9000000000000000000
ghci> 4000000000 *** 2
-2446744073709551616
よく考えられてるんですねぇ。 不勉強でした、本当に恥ずかしい。
変換まとめ
変換関数がたくさん出てきたので、絵を少し違う切り口で整理してみました。
青は安全な関数、赤は値が壊れる可能性がある関数です。
(int2Float
みたいなやつはGHC.Float
に定義してありました、要 import です。)
また、整数リテラル
は最初にInteger
として扱われてfromInteger
によってNum
に、
小数リテラル
は最初にRational
として扱われてfromRational
によってFractional
に変換されるらしいです。
それぞれを緑で書き込んでみました。
整数
になる点線を超える左向きの関数と、ベン図の外側に行く様な下向きの関数が危険なのがよくわかります。
まとめ
ながーい記事になったけど、やってみて感じた haskell における数値表現の要点は4つだけだ!
-
Int
は扱える桁に限りがあるぜ -
Integer
は動的にメモリを確保するので(メモリがある限り)上限はないぜ -
Float
とDouble
の違いは精度だけで、小数
は有限のメモリでは表現できないので誤差が前提だぜ -
Rational
はすげーぜ、分数
の形で値を扱うのでRational
同士の計算においては誤差が出ないぜ
いやーそれにしても勉強になった。普段どれだけ適当にやってきたかを痛感した。
そしてこれを理解したらどうするかと言うと、やっぱドメインロジックとは切り離したいので value object を作って隠蔽するわけだ!
きっちり理解したので普段の業務(ドメイン実装)ではやっぱり使わないわけだ!なんというパラドクス!