この記事の要点
- Swift エコシステム向けの新しいオープンソースパッケージ Swift Numerics が発表されました。数値計算の基礎となる機能を、細粒度のモジュール群として 1 つの Swift パッケージにまとめて提供します。
- 最初の 2 つのモジュールとして、
Real(SE-0246 の基本数学関数を提供)とComplex(複素数とその演算を提供)が同梱されています。 - これらは標準ライブラリではなくパッケージとして提供されます。Swift のリリースサイクルに縛られずに開発・公開でき、API を安定化させる前に試験的に公開できるためです。一部のモジュールは将来的に標準ライブラリへの取り込みを提案する可能性があります。
何が発表されたのか
Swift Numerics は、Swift で数値計算を行うための土台を提供するオープンソースパッケージです。標準ライブラリの既存 API では埋まっていなかった重要な穴を埋め、Swift で新たな分野のプログラミングを可能にすることを目的としています。
発表時点では、計算数学ですぐに役立つ 2 つのモジュールが用意されました。
Real: SE-0246 で提案された「基本数学関数」の API をモジュールとして提供します。Complex: 複素数型とその算術演算を提供します。
Real モジュール
SE-0246 は、サイン(sin)や対数(log)といった操作をジェネリックな文脈で使えるようにする「基本数学関数」の API を提案し、受理されました。しかし当時はコンパイラの制約により、ソース安定性を保ったまま標準ライブラリに追加することができませんでした。Real モジュールはこの API を独立したモジュールとして提供することで、その改善された API をプロジェクトですぐに使えるようにします。
このモジュールは 3 つのプロトコルを定義します。
ElementaryFunctions: 最も一般的なプロトコルで、指数関数(exp、expMinusOne)、対数関数(log、log(onePlus:))、三角関数(cos、sin、tan)とその逆関数、双曲線関数とその逆関数、べき乗・累乗根関数(pow、sqrt、root)を利用できます。RealFunctions:ElementaryFunctionsを継承し、実数より一般的な体の上では定義・実装が難しい操作を追加します。atan2(y:x:)、hypot(中間のオーバーフロー・アンダーフローなしにsqrt(x*x + y*y)を計算)、誤差関数erf・erfc、exp2・exp10、log2・log10、ガンマ関数gamma・logGamma・signGammaなどです。Real: 最もよく使うことになるプロトコルで、基本数学関数一式を備えた浮動小数点型を表します。ジェネリックなコードを書くのに便利で、多くの数値関数を実装するのに必要な基本がそろっています。
たとえば、ジェネリックなシグモイド関数は次のように書けます。
import Numerics
func sigmoid<T: Real>(_ x: T) -> T {
1 / (1 + .exp(-x))
}
このコードは Float、Double、(ターゲットが対応していれば)Float80 で動作します。将来 Float16 や Float128 のような新しい浮動小数点型が Swift に追加されれば、それらでも動作するようになります。
Complex モジュール
Complex モジュールは Real を土台にして、Swift に複素数型を提供します。複素数は、フーリエ変換をはじめとする信号処理など、多くのアルゴリズムを自然に記述できる場であり、多くの言語や標準ライブラリが提供する重要な基本要素です。
Complex 型は、Real に適合する RealType をジェネリックパラメータに取ります。
public struct Complex<RealType> where RealType: Real {
...
}
複素数は実部と虚部の 2 つの成分を持ち、虚数単位 i を使って数学では a + bi と書きます。Swift でも同様に書けます。
let z: Complex<Double> = 2 + 3 * .i
print(z.real) // 2.0
print(z.imaginary) // 3.0
実部と虚部を指定して構築することもできます。表示は Fortran スタイルで、a + bi は (a, b) と出力されます。
let w = Complex<Double>(1, -2) // (1.0, -2.0)
print(z) // (2.0, 3.0)
Complex 型は Numeric プロトコルに適合し、通常の演算子で加減乗除を行えます。割り算も通常の / 演算子を使うため、ほかの数値型と同じように扱えます。
func scaleAndRotate<T>(_ z: Complex<T>) -> Complex<T> {
z * Complex(0, 2)
}
let result = scaleAndRotate(Complex(1.0, 1.0)) // (-2.0, 2.0)
無限大や NaN の扱いについては、C や C++ の複素数ライブラリが異なるゼロ・無限大・NaN を細かく区別しようとするのに対し、Swift Numerics はこの区別をしません。実部と虚部がともにゼロなら 0、実部か虚部のいずれかが非有限ならすべて単一の「無限遠点」に集約されます。情報がわずかに失われますが、この区別を活用するプログラムはごくわずかである一方、区別を維持しようとするとすべてのプログラムの性能が損なわれるためです。
この設計により、Swift Numerics の複素数演算は C より高速です。値が適切にスケールされている一般的なケースで乗算は約 30% 速く、除算では条件次第でさらに大きな差が出ます。除算演算がコンパイラから見える形で実装されているため、多数の値を 1 つの除数で割るような場面で最適化の余地が大きくなります。除数が適切にスケールされている場合は、次のように reciprocal プロパティを使って乗算に置き換えることで、さらに高速化できます。
// 除数が適切にスケールされていれば、除算ではなく逆数の乗算を使う。
if let recip = divisor.reciprocal {
return data.map { $0 * recip }
}
// そうでなければ除算にフォールバックする。
return data.map { $0 / divisor }
さらに Swift Numerics は、扱いの難しいケースでも C や Python より正確な結果を返します。
なぜパッケージなのか
著者は、すべてを標準ライブラリに入れるべきではないという考えから、この機能をパッケージとして開発しています。SwiftNIO がネットワーク分野で果たしているのと同じように、数値計算を中心としたモジュールの共通の置き場所を提供することが狙いです。パッケージにすることで次の利点もあります。
- Swift のリリースに縛られないスケジュールで、これらのモジュールを開発・公開できます。
- API を安定化させたと宣言する前に、試験的にモジュールを公開できます。
一部のモジュールはいずれ標準ライブラリへ取り込まれる可能性がありますが、すべてのプロジェクトにデフォルトで含まれるべきではないモジュールには、別の住処が必要だという位置づけです。
今後の見通し
発表時点では、今後数か月のうちに次のような機能を追加していく計画が示されていました。いずれも将来の構想であり、実現を約束するものではありません。
- 多次元の homogeneous なデータを表現できる
ShapedArrayプロトコルと、それを支える型。 - 浮動小数点型の近似的等価判定(SE-0259 の続きとして)。
- 64 ビットより大きい固定幅整数型。
Float16のサポート。
また、Complex を ElementaryFunctions に適合させ、複素数でも一連の超越関数を使えるようにする作業も進められていました。