M.Hiroi's Home Page

お気楽 Haskell プログラミング入門

応用編 : 複素数

Copyright (C) 2021 Makoto Hiroi
All rights reserved.

はじめに

近年、多くのプログラミング言語で「複素数 (complex number)」がサポートされるようになりました。たとえば、C言語では 1999 年に発行された規格 C99 で複素数型が導入されました。Go 言語や Python でも複素数型をサポートしていますし、複素数用の標準ライブラリを用意している言語 (C++, Ruby, Haskell など) も多くあります。

他の言語では、FORTRAN や Common Lisp が昔から複素数型をサポートしています。Common Lisp の場合、基本的な数学関数でも複素数を適用できるのであれば、引数に複素数を渡して計算することができます。Haskell の場合、モジュール Data.Complex をインポートすると、複素数を使用することができます。今回は Haskell の複素数についてまとめてみました。

●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 で判別することができます。

●Haskell の複素数

数学では複素数 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\) になります。また、下記のようにド・モアヴルの公式も導出することができます。

\( z^n = (re^{i\theta})^n = r^n e^{in\theta} = r^n (\cos n\theta + i \sin n\theta) \)

関数 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\) とします。

●複素数の四則演算

複素数の四則演算は次のようになります。

\( \begin{array}{l} (a + bi) + (c + di) = (a + c) + (b + d)i \\ (a + bi) - (c + di) = (a - c) + (b - d)i \\ (a + bi) \times (c + di) = (ac - bd) + (bc + ad)i \\ (a + bi) \div (c + di) = \dfrac{ac + bd + (bc - ad)i}{c^2 + d^2} \end{array} \)

これらの演算は 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

初版 2021 年 11 月 20 日