プログラミング言語における複素数の実装 格付けチェック

前に書いたこれ

浮動小数点数による複素数の演算に関する注意点

の観点で、各種プログラミング言語がどの程度健闘しているか、確かめてみた。

目次

チェック項目

チェック項目1, 2:回避できるオーバーフローを回避しているか

  • チェック項目1(絶対値):abs(1e300 + 1e300 i) が inf ではなく 1.4142135623730952e300 になるか
  • チェック項目2(除算/逆数):1 / (1e300 + 1e300 i) が 0 + 0 i ではなく 5e-301 – 5e-301 i になるか

これらができていないライブラリは正直言って存在価値がないと思う。

絶対値の実装を、標準 C にあるような hypot 関数に丸投げしている場合は、項目1は自動でクリアする。

チェック項目3〜8:無限大や NaN の扱いはどうなっているか

  • チェック項目3(絶対値):inf + nan i の絶対値が inf になるか
  • 掛け算:
    • チェック項目4:非 0 有限値と無限大をかけた時に無限大になるか
    • チェック項目5:無限大と無限大をかけた時に無限大になるか
  • 割り算:
    • チェック項目6:有限値を無限大で割った時に 0 になるか
    • チェック項目7:非 0 有限値を 0 で割った時に無限大になるか
    • チェック項目8:無限大を有限値で割った時に無限大になるか

素朴に実装すると、これらの項目全てで NaN が帰ってくる可能性がある。ただし、絶対値に関しては、標準 C にあるような hypot 関数に丸投げした場合に inf + nan i の絶対値が inf になる(項目3をクリアする)。

「複素数の無限大なんか知るか!」というのも立場としてアリだとは思うので、これらの項目を満たしていない(NaN が帰ってきた)からと言って存在価値がないとまでは言わない。

その他比較項目

複素数型のインターフェースについては、各言語の特色が出る。

  • 組み込み
    • 何も import しなくても複素数型を使える。
    • 組み込みと言うからには +, * などの演算子は当然のように使えることが期待される。
    • リテラルの文法の有無は、言語によって異なる。
  • ライブラリ実装
    • 何かを import する必要がある。
    • 演算子オーバーロードのできる言語であれば +, * などの演算子を使える。
    • 標準ライブラリか、サードパーティーが提供するライブラリか?

テストプログラム

ここに置いた。

各言語で、

  1. abs(1e300 + 1e300 i)
  2. 1 / (1e300 + 1e300 i)
  3. abs(inf + nan i)
  4. (1 + 1 i) * (inf + nan i)
  5. (inf + nan i) * (inf + nan i)
  6. 1 / (inf + nan i)
  7. 1 / (0 + 0 i)
  8. (inf + nan i) / (1 + 2 i)

を計算し、出力する。

結果早見表

項目1 項目2 項目3 項目4 項目5 項目6 項目7 項目8
C言語
C++
D cdouble × × × × ×
D std.complex × × × × ×
Go × ×
Rust num::complex × × × × × ×
Haskell Data.Complex × × × × × ×
OCaml × × × × × ×
Python × × × 例外 ×
Ruby × × × 例外 ×

「○」と書いた項目は、期待される答えが出た項目である。

項目 1, 2 にバツがついたものは、複素数ライブラリとしての存在価値がない。読者の中に慈悲深い人がいたらプルリクを投げてあげると良いと思う。

項目 3〜6, 8 は、無限大の取り扱いに関するものである。計算結果を見るときに、実部と虚部のどちらか一方でも無限大であれば無限大が帰ってきたとみなす。実部と虚部の両方が NaN の場合は、 NaN が帰ってきたとみなす。

C言語 (_Complex)

C言語は実用的な言語なので、上に挙げた項目は全てクリアする。

C言語における複素数型は組み込み型だが、リテラル表記はない。

$ gcc -v
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 8.0.0 (clang-800.0.42.1)
Target: x86_64-apple-darwin16.4.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
$ gcc -Wall test.c
$ ./a.out
C _Complex
abs(1e300 + 1e300 i)          = 1.414213562373095e+300
1 / (1e300 + 1e300 i)         = 4.999999999999999e-301 + -4.999999999999999e-301 i
abs(inf + nan i)              = inf
(1 + 1 i) * (inf + nan i)     = inf + inf i
(inf + nan i) * (inf + nan i) = inf + nan i
1 / (inf + nan i)             = 0 + 0 i
1 / (0 + 0 i)                 = inf + nan i
(inf + nan i) / (1 + 2 i)     = inf + -inf i

C++ (std::complex)

ライブラリ実装。(演算子オーバーロードあり)

$ g++ -v
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 8.0.0 (clang-800.0.42.1)
Target: x86_64-apple-darwin16.4.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
$ g++ -Wall test.cpp
$ ./a.out
C++ std::complex
abs(1e300 + 1e300 i)          = 1.414213562373095e+300
1 / (1e300 + 1e300 i)         = 4.999999999999999e-301 + -4.999999999999999e-301 i
abs(inf + nan i)              = inf
(1 + 1 i) * (inf + nan i)     = inf + inf i
(inf + nan i) * (inf + nan i) = inf + nan i
1 / (inf + nan i)             = 0 + 0 i
1 / (0 + 0 i)                 = inf + nan i
(inf + nan i) / (1 + 2 i)     = inf + -inf i

C言語の _Complex と同じ結果になった(というか GCC や clang の std::complex<double> は組み込みの複素数型を使うように特殊化されていたような気がする)。

TODO: 規格でどうなっているか調べる。

D言語 (cdouble)

組み込み型である(ただし deprecated)。リテラル表記は、サフィックス i である。

$ dmd --version
DMD64 D Compiler v2.073.2-devel
Copyright (c) 1999-2016 by Digital Mars written by Walter Bright
$ dmd -run test1.d
D cdouble
abs(1e300 + 1e300 i)          = 1.41421e+300
1 / (1e300 + 1e300 i)         = 5e-301-5e-301i
abs(inf + nan i)              = inf
(1 + 1 i) * (inf + nan i)     = nannani
(inf + nan i) * (inf + nan i) = nannani
1 / (inf + nan i)             = nannani
1 / (0 + 0 i)                 = nannani
(inf + nan i) / (1 + 2 i)     = nannani

成分の片方が NaN でも、もう片方が inf であれば abs 関数は inf を返す。これは、 hypot 関数と同様である。

乗算や除算に関しては、無限大の特別な取り扱いはしていないようだ。

D言語 (std.complex)

ライブラリ実装。

std.complex

$ dmd -run test2.d
D std.complex
abs(1e300 + 1e300 i)          = 1.41421e+300
1 / (1e300 + 1e300 i)         = 5e-301-5e-301i
abs(inf + nan i)              = inf
(1 + 1 i) * (inf + nan i)     = nan+nani
(inf + nan i) * (inf + nan i) = nan+nani
1 / (inf + nan i)             = nan+nani
1 / (0 + 0 i)                 = nan+nani
(inf + nan i) / (1 + 2 i)     = nan+nani

組み込み型のものと同じ結果となった。

Go (complex64 / complex128)

組み込み型。リテラル表記は、サフィックス i である。

実部と虚部から複素数を構築するには、組み込みの complex 関数を使う。

複素数に関する各種関数は math/cmplx パッケージにある。

$ go version
go version go1.8 darwin/amd64
$ go run test.go
Go
abs(1e300 + 1e300 i)          = 1.4142135623730952e+300
1 / (1e300 + 1e300 i)         = (5e-301-5e-301i)
abs(inf + nan i)              = +Inf
(1 + 1 i) * (inf + nan i)     = (NaN+NaNi)
(inf + nan i) * (inf + nan i) = (NaN+NaNi)
1 / (inf + nan i)             = (0+0i)
1 / (0 + 0 i)                 = (+Inf+Infi)
(inf + nan i) / (1 + 2 i)     = (+Inf+Infi)

結果を見た限り、複素数の乗算については無限大の特別な取り扱いをしていないが、除算に関しては(C と似た)特別な取り扱いをしている。

Rust (num::complex)

ライブラリ実装。標準ライブラリではない。

test.rs を src/main.rs にリネームし、適当に Cargo.toml をでっち上げて、 num を使えるようにすること。

$ rustc --version
rustc 1.16.0
$ cargo run
   Compiling test v0.1.0 (file:///Users/***)
    Finished debug [unoptimized + debuginfo] target(s) in 0.93 secs
     Running `target/debug/test`
Rust num::complex
abs(1e300 + 1e300 i)          = 1.4142135623730952e300
1 / (1e300 + 1e300 i)         = 0e0 + 0e0 i
abs(inf + nan i)              = inf
(1 + 1 i) * (inf + nan i)     = NaN + NaN i
(inf + nan i) * (inf + nan i) = NaN + NaN i
1 / (inf + nan i)             = NaN + NaN i
1 / (0 + 0 i)                 = NaN + NaN i
(inf + nan i) / (1 + 2 i)     = NaN + NaN i

複素数をそのまま println!("{}", z) で出力するとアレなことになる。何がどうアレかは、実際に自分で試して確かめてもらいたい。

Haskell (Data.Complex)

ライブラリ実装。

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 8.0.2
$ runghc test.hs
Haskell Data.Complex
abs(1e300 + 1e300 i)          = 1.4142135623730952e300
1 / (1e300 + 1e300 i)         = 4.999999999999999e-301 :+ (-4.999999999999999e-301)
abs (inf + nan i)             = NaN
(1 + 1 i) * (inf + nan i)     = NaN :+ NaN
(inf + nan i) * (inf + nan i) = NaN :+ NaN
1 / (inf + nan i)             = NaN :+ NaN
1 / (0 + 0 i)                 = NaN :+ NaN
(inf + nan i) / (1 + 2 i)     = NaN :+ NaN

回避可能なオーバーフローは回避している(項目 1,2)が、それ以外に関しては極めて素朴な実装である。

OCaml

ライブラリ実装。演算子オーバーロードがない。

Module Complex

$ ocaml -version
The OCaml toplevel, version 4.02.2
$ ocaml test.ml
OCaml Complex
abs(1e300 + 1e300 i)          = 1.41421356237e+300
1 / (1e300 + 1e300 i)         = 5e-301 + -5e-301 i
hypot(inf, nan)               = inf
abs(inf + nan i)              = nan
(1 + 1 i) * (inf + nan i)     = nan + nan i
(inf + nan i) * (inf + nan i) = nan + nan i
1 / (inf + nan i)             = nan + nan i
1 / (0 + 0 i)                 = nan + nan i
(inf + nan i) / (1 + 2 i)     = nan + nan i

Haskell と同様の結果になった。hypot 関数と Complex.norm 関数の結果が異なるのが興味深い(前者が導入されたのが割と最近だからか?)。

Python

組み込み。リテラル表記は、サフィックス j である。

ゼロ除算は例外を投げるので、チェック項目7は含めていない。

$ python3 --version
Python 3.6.0
$ python3 test.py
Python
abs(1e300 + 1e300 i)          = 1.4142135623730952e+300
1 / (1e300 + 1e300 i)         = (5e-301-5e-301j)
abs(inf + nan i)              = inf
(1 + 1 i) * (inf + nan i)     = (nan+nanj)
(inf + nan i) * (inf + nan i) = (nan+nanj)
1 / (inf + nan i)             = (nan+nanj)
(inf + nan i) / (1 + 2 i)     = (nan+nanj)

乗算や除算に関して、無限大の特別な取り扱いはしていないと思われる。

Ruby

組み込み。リテラル表記は、サフィックス i である。

ゼロ除算は例外を投げるので、チェック項目7は含めていない。

$ ruby2.4 --version
ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-darwin16]
$ ruby2.4 test.rb
Ruby
abs(1e300 + 1e300 i)          = 1.4142135623730952e+300
1 / (1e300 + 1e300 i)         = 5.0e-301-5.0e-301i
abs(inf + nan i)              = Infinity
(1 + 1 i) * (inf + nan i)     = NaN+NaN*i
(inf + nan i) * (inf + nan i) = NaN+NaN*i
1 / (inf + nan i)             = NaN+NaN*i
(inf + nan i) / (1 + 2 i)     = NaN+NaN*i

乗算や除算に関して、無限大の特別な取り扱いはしていないと思われる。