近年、多くのプログラミング言語で「複素数 (complex number)」がサポートされるようになりました。たとえば、C言語では 1999 年に発行された規格 C99 で複素数型が導入されました。Go 言語や Python でも複素数型をサポートしていますし、複素数用の標準ライブラリを用意している言語 (C++, Ruby, Haskell など) も多くあります。
他の言語では、FORTRAN や Common Lisp が昔から複素数型をサポートしています。Common Lisp の場合、基本的な数学関数でも複素数を適用できるのであれば、引数に複素数を渡して計算することができます。Haskell の場合、モジュール Data.Complex をインポートすると、複素数を使用することができます。今回は Haskell の複素数についてまとめてみました。
Haskell の数はプリミティブな型で大きく分けると、整数 (Int, Integer) と実数 (Float, Double) の 2 種類があります。Haskell の Integer は多倍長整数なので、Integer を使えばメモリの許す限り任意の精度で扱うことができます。モジュール Data..Ratio をインポートすると有理数 (分数) を使うことができます。特に、Rational は分子と分母の型が Integer なので、メモリの許す限り任意の精度で扱うことができます。
実数は浮動小数点数 (floating point number) として表現されます。浮動小数点数には IEEE 754 という標準仕様があり、近代的なプログラミング言語のほとんどは、IEEE 754 に準拠した浮動小数点数をサポートしています。浮動小数点数はすべての小数を正確に表現することはできません。このため、実数は近似的 (不正確) な値になります。
IEEE 754 には通常の数値以外にも、負のゼロ (-0.0)、正負の無限大 (∞, -∞)、NaN (Not a Number, 非数) といった値が定義されています。これらの値は Haskell でも取り扱うことができます。ghci の場合、負のゼロは -0.0、正負の無限大は Infinity と -Infinity、NaN は NaN と表示されます。
一般に、無限大は値のオーバーフロー、ゼロ除算 (数値 / 0.0)、数学関数の計算結果 (たとえば log(0.0)) などで発生します。なお、浮動小数点数のゼロ除算でエラー (例外) を送出する処理系 (たとえば Python など) もあります。
簡単な実行例を示します。
ghci> 1e308 1.0e308 ghci> 1e309 Infinity ghci> 1e308 * 2 Infinity ghci> -1e308 -1.0e308 ghci> -1e309 -Infinity ghci> -1e308 * 2 -Infinity ghci> 1 / 0 Infinity ghci> -1 / 0 -Infinity ghci> log(0) -Infinity
Haskell の場合、無限大の判定は関数 isInfinite を使います。
ghci> isInfinite (1 / 0) True ghci> isInfinite (-1 / 0) True ghci> isInfinite 0 False ghci> isInfinite 1.2345 False
Infinity は他の数値と比較したり演算することもできますが、結果が NaN になることもあります。
ghci> a = 1 / 0 ghci> a Infinity ghci> b = -1 / 0 ghci> b -Infinity ghci> a > b True ghci> a < b False ghci> a == b False ghci> a /= b True ghci> a > 0 True ghci> a < 0 False ghci> b < 0 True ghci> b > 0 False ghci> a + 100 Infinity ghci> a - 100 Infinity ghci> a * 100 Infinity ghci> a / 100 Infinity ghci> a + a Infinity ghci> a * a Infinity ghci> a - a NaN ghci> a / a NaN ghci> a + b NaN ghci> a * 0 NaN
負のゼロ (-0.0) は、計算結果が負の極めて小さな値でアンダーフローになったとき発生します。また、正の値を負の無限大で除算する、負の値を正の無限大で除算する、負の値と 0.0 を乗算しても -0.0 が得られます。
ghci> -1e-323 -1.0e-323 ghci> -1e-324 -0.0 ghci> -1e-323 / 2 -5.0e-324 ghci> -1e-323 / 4 -0.0 ghci> -1.0 / (1 / 0) -0.0 ghci> 1.0 / (-1 / 0) -0.0 ghci> -1.0 * 0.0 -0.0
標準 (IEEE 754) では、演算子 (== など) による 0.0 と -0.0 の比較は等しいと判定されます。Haskell の場合、-0.0 は関数 isNegativeZero で判別することができます。また、任意の正の数を -0.0 で除算すると -Infinity になるので、それを使って判別することもできます。
ghci> a = 0.0 ghci> b = -0.0 ghci> a 0.0 ghci> b -0.0 ghci> a == b True ghci> isNegativeZero a False ghci> isNegativeZero b True ghci> 1 / a Infinity ghci> 1 / b -Infinity
-0.0 は数学関数 (たとえば atan2) や複素数の演算処理などで使われます。
ghci> atan2 0 (-1) 3.141592653589793 ghci> atan2 (-0) (-1) -3.141592653589793 ghci> sqrt(0) 0.0 ghci> sqrt(-0) -0.0
NaN は数ではないことを表す特別な値 (非数) です。一般的には 0.0 / 0.0 といった不正な演算を行うと、その結果は NaN になります。たとえば、負数の平方根は虚数になりますが、関数 sqrt に負数を与えると浮動小数点数では表現できないので NaN になります。
ghci> 0 / 0 NaN ghci> sqrt(-1) NaN ghci> a = (1 / 0) ghci> a Infinity ghci> a / a NaN ghci> isNaN (a/a) True ghci> isNaN (-0) False
Haskell の場合、NaN は関数 isNaN で判別することができます。
数学では複素数 z を \(x + yi\) と表記します。x を実部、y を虚部、i を虚数単位といいます。虚数単位は 2 乗すると -1 になる数です。実部と虚部の 2 つの数値を格納するデータ構造を用意すれば、プログラミング言語でも複素数を表すことができます。Haskell で複素数を使用するときはモジュール Data.Complex をインポートしてください。複素数の型は Complex a になります。
data Complex a = !a :+ !a
Haskell は複素数 \(x + yi\) を x :+ y と記述します。演算子 :+ は複素数を生成します。複素数 z の実部は関数 realPart で、虚部は関数 imagPart で取得することができます。複素数の虚部の符号を反転することを「複素共役」といいます。複素共役は関数 conjugate で求めることができます。
簡単な例を示しましょう。
ghci> :m + Data.Complex ghci> 1 :+ 2 1 :+ 2 ghci> :t 1 :+ 2 1 :+ 2 :: Num a => Complex a ghci> 1.0 :+ 2.0 1.0 :+ 2.0 ghci> :t 1.0 :+ 2.0 1.0 :+ 2.0 :: Fractional a => Complex a ghci> a = 1.234 :+ 5.678 ghci> a 1.234 :+ 5.678 ghci> realPart a 1.234 ghci> imagPart a 5.678 ghci> conjugate a 1.234 :+ (-5.678)
複素数は極形式 \(z = r (\cos \theta + i \sin \theta)\) で表すことができます。このとき、r を絶対値、\(\theta\) を偏角といいます。絶対値は関数 magnitude で求めることができます。偏角は複素平面において正の実軸とベクトル (x, y) との角度を表します。偏角を求めるには関数 phase を使います。phase の返り値 \(\theta\) は \(-\pi \leq \theta \leq \pi\) (\(\pi\) : 円周率) です。
極形式 \(z = r (\cos \theta + i \sin \theta)\) で複素数を生成するときは関数 mkPolar を使います。
mkPolar :: Floating a => a -> a -> Complex a
第 1 引数が絶対値 r、第 2 引数が偏角 \(\theta\) を表します。複素数 z はオイラーの公式 \(e^{i\theta} = \cos \theta + i \sin \theta\) を使って \(z = r e^{i\theta}\) と書くこともできます。ここで、\(\theta\) に \(\pi\) を代入するとオイラーの等式 \(e^{i\pi} + 1 = 0\) になります。また、下記のようにド・モアヴルの公式も導出することができます。
関数 cis x は絶対値が 1 で偏角が x の複素数を生成します。cis は cos + i sin の略です。関数 polar は複素数の絶対値と偏角を求めます。
cis :: Floating a => a -> Complex a polar :: RealFloat a => Complex a -> (a, a)
簡単な例を示しましょう。
ghci> a = 1.0 :+ 1.0 ghci> a 1.0 :+ 1.0 ghci> magnitude a 1.4142135623730951 ghci> phase a 0.7853981633974483 ghci> mkPolar 1.4142135623730951 0.7853981633974483 1.0000000000000002 :+ 1.0 ghci> magnitude (cis 0.7853981633974483) 1.0 ghci> polar a (1.4142135623730951,0.7853981633974483) ghci> phase ((-1.0) :+ 0.0) 3.141592653589793 ghci> phase ((-1.0) :+ 1.0) 2.356194490192345 ghci> phase (0.0 :+ 1.0) 1.5707963267948966 ghci> phase (1.0 :+ 1.0) 0.7853981633974483 ghci> phase (1.0 :+ 0.0) 0.0 ghci> phase (1.0 :+ (-1.0)) -0.7853981633974483 ghci> phase (0.0 :+ (-1.0)) -1.5707963267948966 ghci> phase ((-1.0) :+ (-1.0)) -2.356194490192345 ghci> phase ((-1.0) :+ (-0.0)) -3.141592653589793
Haskell の場合、-1.0+0.0i の偏角 \(\theta\) は \(\pi\) になり、-1.0-0.0i の偏角は \(-\pi\) になります。ゼロと負のゼロを区別しないプログラミング言語では、偏角 \(\theta\) の範囲を \(-\pi \lt \theta \leq \pi\) に制限して、-1.0 + 0.0i (== -1.0 - 0.0i) の偏角を \(\pi\) とします。
複素数の四則演算は次のようになります。
これらの演算は Haskell の演算子 +, -, * , / で行うことができます。複素数の場合、大小の比較演算は使えませんが、等値の判定は演算子 == や /= で行うことができます。簡単な例を示しましょう。
ghci> a = 1.0 :+ 2.0 ghci> a 1.0 :+ 2.0 ghci> b = 3.0 :+ 4.0 ghci> b 3.0 :+ 4.0 ghci> a + b 4.0 :+ 6.0 ghci> a - b (-2.0) :+ (-2.0) ghci> a * b (-5.0) :+ 10.0 ghci> a / b 0.44 :+ 8.0e-2 ghci> a == a True ghci> a == b False ghci> a /= a False ghci> a /= b True
実部と虚部の値は Infinity や NaN になることもあります。
ghci> c = 1e300 :+ 1e300 ghci> c 1.0e300 :+ 1.0e300 ghci> c * c NaN :+ Infinity