Generic Math(s) Functions
01 何が問題だったのか
Swift の BinaryFloatingPoint(およびそれが refine する各プロトコル)は数値計算のための強力な抽象を備えていますが、exp、log、sin のような超越関数は含まれていません。これらは C の数学ライブラリに由来する関数として、プラットフォームごとのオーバーレイで具体型に対するオーバーロードされたフリー関数として提供されているに過ぎません。
この設計には2つの問題があります。
プラットフォームごとの import が必要
まず、これらの関数を使うために import すべきものがプラットフォームによって異なり、おなじみの #if によるおまじないが必要になります。
#if canImport(Darwin)
import Darwin
#elseif canImport(GlibC)
import GlibC
#endif
すべてのプラットフォームで利用できるはずの機能に対してこのような記述が必要になるのは本来望ましくありません。
ジェネリックに使えない
次に、これらの関数は個々の具体的な BinaryFloatingPoint 型に対してオーバーロードされているだけで、ジェネリックな文脈で直接呼び出す方法がありません。たとえばシグモイド関数をジェネリックに書こうとしても、
func sigmoid<T>(_ x: T) -> T where T: FloatingPoint {
return 1/(1 + exp(-x))
}
exp は FloatingPoint プロトコルのメンバではないため、これはコンパイルできません。回避策として一度 Double に落とす手があります。
func sigmoid<T>(_ x: T) -> T where T: FloatingPoint {
return 1/(1 + T(exp(-Double(x))))
}
しかしこれは冗長であり、T が Double より精度が低ければ無駄な計算が増え、高ければ精度が落ちます。将来的に標準ライブラリが Complex 型を持つようになった場合にも、同じ抽象で扱える仕組みがありません。
ジェネリックな数値コードを素直に書けるよう、これらの数学関数をプロトコルで抽象化する必要がありました。
02 どのように解決されるのか
2つの新しいプロトコル ElementaryFunctions と Real を導入し、浮動小数点型(および将来の Complex、SIMD のスカラ型)に対してジェネリックに数学関数を呼び出せるようにします。あわせて、ジェネリックなフリー関数版(exp(x) など)も提供され、プラットフォームごとの import の違いを気にせず使えるようになります。
なお、このProposalは受理されたものの、型チェッカの性能や shadowing ルールに絡む source-breaking な問題により Swift 本体への実装は保留されています。同等の機能は Swift Numerics パッケージから利用できます。
ElementaryFunctions プロトコル
初等関数(sqrt、三角関数、双曲線関数、指数・対数、pow、root)を static メソッドとして持つプロトコルです。標準ライブラリの各 FloatingPoint 型がこれに適合し、将来の Complex 型も適合できるよう実数/複素数どちらでも意味を持つ関数に絞られています。SIMD 型自体は適合しませんが、スカラ型が適合していれば SIMD に対しても同じ操作が定義されます。
public protocol ElementaryFunctions {
static func sqrt(_ x: Self) -> Self
static func cos(_ x: Self) -> Self
static func sin(_ x: Self) -> Self
static func tan(_ x: Self) -> Self
static func acos(_ x: Self) -> Self
static func asin(_ x: Self) -> Self
static func atan(_ x: Self) -> Self
static func cosh(_ x: Self) -> Self
static func sinh(_ x: Self) -> Self
static func tanh(_ x: Self) -> Self
static func acosh(_ x: Self) -> Self
static func asinh(_ x: Self) -> Self
static func atanh(_ x: Self) -> Self
static func exp(_ x: Self) -> Self
static func exp2(_ x: Self) -> Self
static func exp10(_ x: Self) -> Self
static func expm1(_ x: Self) -> Self
static func log(_ x: Self) -> Self
static func log2(_ x: Self) -> Self
static func log10(_ x: Self) -> Self
static func log1p(_ x: Self) -> Self
static func pow(_ x: Self, _ y: Self) -> Self
static func pow(_ x: Self, _ n: Int) -> Self
static func root(_ x: Self, _ n: Int) -> Self
}
静的メソッドとして次のように呼び出せます。
Float.exp(1)
// 2.7182817
pow は2種類あり、1つは IEEE 754 の powr(x が負なら NaN)、もう1つは指数を Int に固定した pown 相当です。exp10 や root は多くの C 数学ライブラリに存在しないので、必要ならそれぞれ pow(10, x) や pow を使った実装にフォールバックします。
Real プロトコル
多くのユーザが実際にジェネリック制約として使うのはこちらです。FloatingPoint と ElementaryFunctions の両方を refine し、実数に特有の関数(atan2、誤差関数、hypot、ガンマ関数など)を追加します。
public protocol Real: FloatingPoint, ElementaryFunctions {
static func atan2(y: Self, x: Self) -> Self
static func erf(_ x: Self) -> Self
static func erfc(_ x: Self) -> Self
static func hypot(_ x: Self, _ y: Self) -> Self
static func gamma(_ x: Self) -> Self
static func logGamma(_ x: Self) -> Self
static func signGamma(_ x: Self) -> FloatingPointSign
}
これらは複素数では実装が難しいか、そもそも意味をなさないため Real 側に置かれます。atan2 は引数順序の取り違えによるバグが起きやすいため、atan2(y:x:) と引数ラベルが追加されています。logGamma は既存の lgamma((T, Int) のタプルを返す)の代わりに値だけを返し、符号は必要に応じて signGamma で取得する分離形になっています。
ジェネリックなフリー関数
プロトコルメソッドに加えて、グローバルなフリー関数版も提供されます。プラットフォームごとの #if を書かずに、そして T をジェネリックにしたまま使えます。
import Math
func sigmoid<T>(_ x: T) -> T where T: Real {
return 1/(1 + exp(-x))
}
exp(1.0) // 2.7182817
ceil、floor、round、trunc、fma、remainder、fmod などは ElementaryFunctions には含まれず、FloatingPoint の API に委譲するジェネリックフリー関数として用意されます。たとえば ceil は次のように定義されます。
public func ceil<T>(_ x: T) -> T where T: FloatingPoint {
return x.rounded(.up)
}
これらは既存のプラットフォームモジュール上の定義を置き換えるものです。C の動的丸めモードに依存する nearbyint や rint は Swift のモデルになじまないため deprecated 扱いになります。
プラットフォーム側の整理
既存のプラットフォームオーバーレイ(Darwin / GlibC)が提供していた同名関数は、新しいフリー関数に置き換わる形で obsolete されます。ラベルが変わる atan2(y:x:) や名前が変わる logGamma のように移行が必要なものは、obsolete ではなく deprecated としてしばらく並存させ、ユーザに移行の猶予を与えます。また、<math.h> のサフィックス付き関数(sinf など)の import は、もともと Swift 側で使わせる意図がなかったため取り除かれます。
Future Directions
IEEE 754 が推奨する cospi、sinpi、tanpi、acospi、asinpi、atanpi、exp2m1、exp10m1、log2p1、log10p1、compound といった関数は、処理系実装が広く入手できないため今回は見送られています。将来 Swift 側で直接実装する形で追加される可能性があります。任意の底を取る log(_ base: T, _ x: T) も要望はありますが、正確な結果を得るには C 数学ライブラリにない高精度プリミティブが必要なため、今のところは保留されています。