コメントフォーム(承認式)

ホーム
プロフィール
製作物
前の記事
C++でグラフ型を作成
次の記事
棒人間アクションゲームの制作1
投稿日時:2019/12/17 00:18、最終更新日時:2020/01/15 18:14

C++により数式を直感的に記述する方法についての考察

タグ:
C++
数学
考察
  • pocket
  • はてなブックマーク

 この記事はQiita Advent Calendar 2019における数値計算 Advent Calendar 2019の17日目の記事である。

 数値解析ではない気がするが、数値計算かと言われればそうであると言えると思う。
 初めての参加なのでお手柔らかにお願いしたい。

はじめに

 C++において、例えば

int main() {

	std::complex<double> a(1, 3);
	int b = 3;
	std::complex<double> c = a * b;

	return 0;
}

bは暗黙的にキャストしないためコンパイルエラーとなる。これを動作するようにするには

int main() {

	std::complex<double> a(1, 3);
	int b = 3;
	std::complex<double> c = a * double(b);

	return 0;
}

のようにbを明示的にキャストをしなければならない。
 このようなキャストの記述は冗長であり、除去されるべき表現である。このような問題をほとんど全ての型について解決してC++で数式を直感的に記述するための方法を考える。ただし、この問題は完全に解決をすることは困難であることに留意する必要があり、現状として理論の整合性を証明できていないことにも留意してもらいたい。何か思いつけば順次修正・追記していく予定である。

 そもそも冗長表現が生じないようなプログラムをすればいいというツッコミ等はなしの方向でいく

追記(2019/12/29)

 キャストが冗長であると述べたが、

int main() {

	std::complex<float> a(1, 3);
	int b = 3;
	// a * b はdouble型の精度で結果を得る
	std::complex<double> c = a * double(b);

	return 0;
}

のような場合のように意味をもつキャストについては冗長と見るべきではない(このコード自体はコンパイルエラーが出力されるため注意)。
 そこで、言い回しを少し変え、暗黙のキャストにより演算が可能な対象における明示的キャスト等の冗長表現を除去する手法を考えるということとする。このとき、std::is_convertibleを用いれば用意に実装できる気もするが、クラスにおけるキャスト演算子のオーバーロードの記述が困難であることからこれは用いない。

安直な解決方法

 安直な解決方法として、そもそもオーバーロードが定義されていないことにより生じる問題であるため、オーバーロードを定義することで解決可能である。

std::complex<double> operator*(const std::complex<double>& c, int v) { /* 略 */ }

 しかし、intの部分をshortfloatにしてみたり、std::complex<double>の部分をstd::complex<std::complex<double>>のようにして多重複素数を扱うことを考えたとき、直接定義における定義するべき演算の組み合わせは無限大に存在し、現実的な解決方法とはいえない。

 そのため、例えば複素数におけるスカラー乗法では

template <class T1, class T2, class = std::enable_if_t</* スカラー乗法が作用するためのT1とT2の制約条件の記述 */>>
auto operator*(const std::complex<T1>& c, const T2& v) { /* 略 */ }

といったようにテンプレートパラメータに制約を与えることで、型を抽象化して統括的に扱う

 よくまだ理解はしていないためこの場に明言することはできないが、C++20におけるconceptsと相性が良さそうである。

数学型の構造

数学型の定義

 ここで扱う型というのは、数値計算で用いる型であるが、実際問題として型というのは数値計算以外のものも多く存在する。そこで、数値計算で用いる型について、その定義を与えるべきである。
 天下り的であるが、その定義は以下のように与える。

定義

 加法もしくは乗法の定義される型\(T\)が加法および乗法の特性を示す構造体(addition_traitsmultiplication_traits)がそれぞれ\(T\)について定義されるならば、\(T\)を数学型と定める。また、加法や乗法の逆演算が定義されるならば、\(T\)の吸収元を除いた任意の元が可逆元でなければならない。

 このとき、addition_traitsmultiplication_traitsというのは以下のように定義されるものである。

namespace iml {

	// 加法の特性
	template <class>
	struct addition_traits;
	// 符号無し整数
#define IMATHLIB_ADDITION_TRAITS(TYPE)\
	template <>\
	struct addition_traits<TYPE> {\
		/*単位元*/\
		static constexpr TYPE identity_element() { return 0; }\
		/*結合律*/\
		static constexpr bool associative_value = true;\
		/*消約律*/\
		static constexpr bool cancellative_value = false;\
		/*可換律*/\
		static constexpr bool commutative_value = true;\
	};
	IMATHLIB_ADDITION_TRAITS(uint8_t);
	IMATHLIB_ADDITION_TRAITS(uint16_t);
	IMATHLIB_ADDITION_TRAITS(uint32_t);
	IMATHLIB_ADDITION_TRAITS(uint64_t);
#undef IMATHLIB_ADDITION_TRAITS
	// 符号あり数値
#define IMATHLIB_ADDITION_TRAITS(TYPE)\
	template <>\
	struct addition_traits<TYPE> {\
		/*単位元*/\
		static constexpr TYPE identity_element() { return 0; }\
		/*結合律*/\
		static constexpr bool associative_value = true;\
		/*消約律*/\
		static constexpr bool cancellative_value = true;\
		/*可換律*/\
		static constexpr bool commutative_value = true;\
	};
	IMATHLIB_ADDITION_TRAITS(int8_t);
	IMATHLIB_ADDITION_TRAITS(int16_t);
	IMATHLIB_ADDITION_TRAITS(int32_t);
	IMATHLIB_ADDITION_TRAITS(int64_t);
	IMATHLIB_ADDITION_TRAITS(float32_t);
	IMATHLIB_ADDITION_TRAITS(float64_t);
#undef IMATHLIB_ADDITION_TRAITS

	// 乗法の特性
	template <class>
	struct multiplication_traits;
	// 整数
#define IMATHLIB_MULTIPLICATIVE_TRAITS(TYPE)\
	template <>\
	struct multiplication_traits<TYPE> {\
		/*単位元*/\
		static constexpr TYPE identity_element() { return 1; }\
		/*(唯一の)吸収元*/\
		static constexpr TYPE absorbing_element() { return 0; }\
		/*結合律*/\
		static constexpr bool associative_value = true;\
		/*消約律*/\
		static constexpr bool cancellative_value = false;\
		/*可換律*/\
		static constexpr bool commutative_value = true;\
		/*分配律*/\
		static constexpr bool distributive_value = true;\
	};
	IMATHLIB_MULTIPLICATIVE_TRAITS(uint8_t);
	IMATHLIB_MULTIPLICATIVE_TRAITS(uint16_t);
	IMATHLIB_MULTIPLICATIVE_TRAITS(uint32_t);
	IMATHLIB_MULTIPLICATIVE_TRAITS(uint64_t);
	IMATHLIB_MULTIPLICATIVE_TRAITS(int8_t);
	IMATHLIB_MULTIPLICATIVE_TRAITS(int16_t);
	IMATHLIB_MULTIPLICATIVE_TRAITS(int32_t);
	IMATHLIB_MULTIPLICATIVE_TRAITS(int64_t);
#undef IMATHLIB_MULTIPLICATIVE_TRAITS
	// 浮動小数点
#define IMATHLIB_MULTIPLICATIVE_TRAITS(TYPE)\
	template<>\
	struct multiplication_traits<TYPE> {\
		/*単位元*/\
		static constexpr TYPE identity_element() { return 1; }\
		/*(唯一の)吸収元*/\
		static constexpr TYPE absorbing_element() { return 0; }\
		/*結合律*/\
		static constexpr bool associative_value = true;\
		/*消約律*/\
		static constexpr bool cancellative_value = true;\
		/*可換律*/\
		static constexpr bool commutative_value = true;\
		/*分配律*/\
		static constexpr bool distributive_value = true;\
	};
	IMATHLIB_MULTIPLICATIVE_TRAITS(float32_t);
	IMATHLIB_MULTIPLICATIVE_TRAITS(float64_t);
#undef IMATHLIB_MULTIPLICATIVE_TRAITS
}

 この定義において、ある数学型の吸収元を除いた任意の元が可逆元なら逆演算を定義しなければならないとしていないのは、逆演算を定義するべきでない場面が存在するためである。例えば、剰余類環\({\mathbb Z}/n{\mathbb Z}\)は\(n\)が素数であるとき有限体となるため除法は定義されるが,剰余類環に期待されるのは環としての振る舞いであるため除法は定義するべきでない。体としての振る舞いを期待するのであるならば有限体\({\mathbb F}_{p}\)として数学型を個別に与えるべきである。

C++のテンプレートの表現

 C++では、例えば複素数型であればcomplex<T>のようにテンプレート引数Tをもつ。このとき、Tはテンプレート引数をもつ型の可能性もあれば、そうでない可能性もある。テンプレート引数Tの必要性については、ガウス整数として扱うためにTintとしたり、有効な精度によってはTfloatdoubleを任意に選択したりするためである。
 そこで、これらの記述を与えるために以下の定義を与える。

定義

 数学型\(M\)が実体をもつことができるとき、\(M\)は数学型として完備であると定める。\(M\)が完備であるならば、一般に\(M\)は完備な数学型\(K_{1}\)上の数学型\(K_{2}\)、数学型\(K_{2}\)上の数学型\(K_{3}\)、\(\cdots\)、数学型\(K_{n-1}\)上の数学型\(K_{n}\)という構成をし、これを

\[M=(K_{1}\rightarrowtail K_{2}\rightarrowtail\cdots\rightarrowtail K_{n-1}\rightarrowtail K_{n})\]

のように鎖であらわす。このとき、\(M\)は長さ\(n\)の鎖をもつ数学型であるという。なお、完備でない数学型、例えば\(K_{2}\rightarrowtail K_{3}\)のような記述はできないとする。また、このとき\(K_{1}\)は長さ1の鎖をもつ数学型である。\(K_{1}\)が長さ\(m\)の鎖をもつならば\(M\)は長さ\(n+m-1\)の鎖をもつ数学型となる。
 \(k\in{\mathbb N}(k\leq n)\)で\(M_{k}=(K_{1}\rightarrowtail K_{2}\rightarrowtail\cdots\rightarrowtail K_{k})\)と与えられる数学型\(M_{k}\)は\(M\)から生成可能な\(M\)を含まない完備な数学型のうち\(n-k\)番目に長い鎖をもつ。このとき、\(M_{1},M_{2},\cdots,M_{n-1}\)を\(M\)の基底といい、\(M_{k}\overset{n-k}{\rightarrowtail}M\)とあらわす。なお、\(M\)が基底をもたないとき、すなわち\(M_{n}=M\)のときは整合性をもたせるために\(M_{n}\overset{0}{\rightarrowtail}M\)とあらわすとする。

 例えばcomplex<float>であればfloat\(\rightarrowtail\)complexのようにあらわすこととなるが、特に実数型等の区別が必要ないならば実数型\({\mathbb R}\)上の複素数型\({\mathbb C}\)として\({\mathbb R}\rightarrowtail{\mathbb C}\)のようにあらわすとする。このとき、\({\mathbb R}\)と\({\mathbb R}\rightarrowtail{\mathbb C}\)は完備な数学型であり、\({\mathbb C}\)は自明な完備でない数学型である。

 あくまでもある特定の数学型というのは演算の振舞い方を決める枠組みとなるだけである。例えば、複素数型というのは実部と虚部の2つの値から構築され、複素数型に関する積は

\[\begin{cases}((a,b),(c,d))\mapsto(ac-bd,ad+bc) \\((a,b),c)\mapsto(ac,bc) \\(c,(a,b))\mapsto(ca,cb)\end{cases}\]

といった具体的な演算以外の振舞いをしない。このような演算の振舞いが数学型ごとに異なるということである。

演算の分類

 ここから具体的に理論を考えていくが、何を考えればいいかの方針を立てる。

演算パターンの分類

 数学型\(T\)上の複素数型\(T\rightarrowtail{\mathbb C}\)に関する積であれば、適当な数学型\(T_{2}\)と\(T_{2}\rightarrowtail{\mathbb C}\)で\(a,b\in T\)と\(c,d\in T_{2}\)を用いて

\[\begin{cases}((a,b),(c,d))\mapsto(ac-bd,ad+bc) \\((a,b),c)\mapsto(ac,bc) \\(c,(a,b))\mapsto(ca,cb)\end{cases}\]

の3パターンが存在する。これは、所謂複素数同士の演算と左右それぞれから作用するスカラー演算である。
 他の数学型の場合でも、例えばベクトル型を考えれば、ベクトル型同士の積やスカラー乗法、行列との積といった複素数型とほとんど同様の演算が存在する。ここで、複素数型の演算について立ち戻れば、2つの数学型\(M_{1},M_{2}\)の積が複素数型に関する積をするとき、複素数同士の積であれば\(M_{1}\)と\(M_{2}\)はどちらも複素数型であり、スカラー乗法をするのであれば少なくとも\(M_{1}\)と\(M_{2}\)のどちらか一方は複素数型である。
 そこで、このような演算を抽象化して説明するために以下の定義を与える。

定義

 数学型\(M\)が数学型\(K\)で束縛されているとは、適当な数学型\(T\)により\(M=(T\rightarrowtail K)\)とあらわされることである。

 この定義を用いることで演算の組み合わせは

”束縛された数学型”と”束縛された数学型”同士の演算

”束縛された数学型”と”束縛されてない数学型”同士の演算

”束縛されてない数学型”と”束縛された数学型”同士の演算

”束縛されてない数学型”と”束縛されてない数学型”同士の演算

の4パターンが存在し、例で挙げた複素数型およびベクトル型における演算は一番下のパターンを除き、一対一対応に対応することがわかる。実際、一番下のパターンはどの数学型における演算か判別不可であるため無効パターンである。
 これより、数学型における演算は

”束縛された数学型”と”束縛された数学型”同士の演算

”束縛された数学型”と”束縛されてない数学型”同士の演算

”束縛されてない数学型”と”束縛された数学型”同士の演算

の3パターンにより構成されるといえる。
 これより、これを用いた演算パターンについて、以下の定義を与える。

定義

 2つの束縛された数学型により与えられる二項演算を標準演算、1つの束縛された数学型により与えられる二項演算をスカラー演算と定める。特に、スカラー演算に関して左から束縛されてない数学型(左スカラー型と定める)が作用するものを左スカラー演算、右から束縛されてない数学型(右スカラー型と定める)が作用するものを右スカラー演算と定める。左スカラー型と右スカラー型の具別が必要ないのであればスカラー型と呼ぶものとする。

 例えば、数学型\(M\)同士の演算というのは自明な標準演算である。和や積における指数法則に関してはあくまでも標準演算の別表現であるため、一般にスカラー演算として扱わない。

演算の適用可能性・演算可能性

 2つの数学型\(M_{1},M_{2}\)が与えられたとき、標準演算とスカラー演算のいづれかに適用される。そして、演算の適用の流れとしては”\(M_{1}\)と\(M_{2}\)の二項演算で標準演算とスカラー演算のどちらが適用される可能性があるかの判定”、”演算が可能であるかの判定”の流れとなる。
 演算可能性については、数学型の基底における加算可能性といった演算可能性を判定するメタ関数や、最初に与えたaddition_traitsmultiplication_traitsを用いることでただちに達成することができる。例えば、complex<T1>complex<T2>が加法における標準演算をするのであれば、T1T2が加算可能であればいい。そして、T1T2が加算可能であるには○○である必要がある・・・といったように再帰的に記述される。
 そのため、基本的な考察対象となるのは演算の適用可能性となる。標準演算とスカラー演算のどちらを適用するかを考えたとき、標準演算とスカラー演算の定義に立ち戻れば、何も適用条件を与えない際のスカラー演算の状態は標準演算の状態を含む。実際、

template <class T1, class T2>
auto operator+(const complex<T1>& lhs, const complex<T2>& rhs) { /* 略 */ }
template <class T1, class T2>
auto operator*(const complex<T1>& lhs, const T2& rhs) { /* 略 */ }
template <class T1, class T2>
auto operator*(const T1& lhs, const complex<T2>& rhs) { /* 略 */ }

のようなコードを記述して積に関する標準演算を実行するコードを記述すると、コンパイルエラーが生じることがわかる。つまり、標準演算の適用可能性の適用条件を与えて、その否定をスカラー演算の適用可能性の適用条件として考えることは困難である。そのため、スカラー演算の適用可能性の適用条件を与え、その否定を標準演算の適用可能性の適用条件として考える。実際、私たちが普段扱う数学型というのはスカラー演算を備えているが、数学型全体から見ればスカラー演算を備えている数学型の方が稀である。その点を踏まえてもスカラー演算の適用可能性の適用条件の否定を標準演算の適用可能性の適用条件とするのは適当であるといえる。

 基本的に適用可能性については経験則に基づき議論をしていくため、それについて留意してもらいたい。理論として与えるのはヒューリスティックなものとなる。

数学型の基底による適用可能性

 ベクトルとしての演算の振舞いをする数学型を\(V\)とすれば、複素ベクトル型\(V_{c}\)は

\[V_{\mathbb C}=({\mathbb R}\rightarrowtail{\mathbb C}\rightarrowtail V)\]

とあらわすことができる。このとき、\(V_{\mathbb C}\)に関する積は、\(V_{\mathbb C}\)は基底である\({\mathbb R}\)と\({\mathbb R}\rightarrowtail{\mathbb C}\)とでは明らかにスカラー演算をする。これが一般に成り立つと考えると、数学型は基底との演算に関してスカラー演算をするといえる。
 数学型\(M\)が

\[M=(K_{1}\rightarrowtail K_{2}\rightarrowtail\cdots\rightarrowtail K_{n-1}\rightarrowtail K_{n})\]

のように表現されるとして、\(K_{n}=K_{n-1}\)であるとする。このとき、

\[M'=(K_{1}\rightarrowtail K_{2}\rightarrowtail\cdots\rightarrowtail K_{n-1})\]

を定義して\(M\)と\(M'\)の演算を考えるとき、\(M\)と\(M'\)は同じ数学型により束縛されているため標準演算になると考えられるが、\(M'\)は\(M\)の基底であるためスカラー演算にも成り得ると考えられる。実際、多元数を多重化したときどちらの演算を適用して考えても自然である。そこで、多重複素数

\[\begin{cases}{\mathbb C}_{0}={\mathbb R} \\{\mathbb C}_{k+1}={\mathbb C}_{k}\rightarrowtail{\mathbb C}\end{cases}\]

の構造のように\(M\)と\(M'\)の関係(\(M'\rightarrowtail M\))を当てはめるとすることで、\(M\)と\(M'\)の演算はスカラー演算となるといえる。そのため、どのような数学型で束縛されていても数学型\(M\)とその基底に関する演算はスカラー演算をするとする。実際、このようにした方がプログラム上での記述も楽である。

上位もしくは下位の数学型による適用可能性

 ”数学型の基底による適用可能性”によるスカラー演算の必要条件のみでは、例えば、実ベクトル型と複素数型の積といったスカラー乗法がスカラー演算として適用されなくなる。そのため、数学型の構造についてさらに考える必要がある。
 例えば、複素数型\({\mathbb R}\rightarrowtail{\mathbb C}\)と実ベクトル型\({\mathbb R}\rightarrowtail V\)間の乗法では\({\mathbb R}\subset({\mathbb R}\rightarrowtail{\mathbb C})\)が成り立つため実ベクトル型は複素ベクトル型とすることができ、スカラー演算が成り立つ。同様にして、複素数型\({\mathbb R}\rightarrowtail{\mathbb C}\)と四元数ベクトル型\({\mathbb R}\rightarrowtail{\mathbb H}\rightarrowtail V\)間の乗法では四元数型が複素数型を拡張した代数的構造と考えれることで、複素数型は四元数型とすることができ、スカラー演算が成り立つ。整数型\({\mathbb Z}\)と実ベクトル型\({\mathbb R}\rightarrowtail V\)でも同様である。
 これらのことより、数学型間における包含関係というものが”数学型の基底による適用可能性”以外の演算が生じる必要条件であると考えられる。そこで、以下の定義を与える。

定義

 数学型\(M\)の部分集合\(N\subseteq M\)が\(M\)の少なくとも1つの演算について準同型な数学型となるとき、\(N\)を\(M\)の部分数学型と定める。また、この二項関係を半順序として\(M\)を\(N\)の上位の数学型と定め,\(N\hookrightarrow M\)とあらわす。同様にして、\(N\)を\(M\)の下位の数学型と定める.

 このとき、\(\hookrightarrow\)というのは包含写像といった意をももつ。

 この定義を用いることで、スカラー演算におけるスカラー型が”数学型の基底による適用可能性”の場合と比較して、スカラー型として作用する基底が上位および下位の数学型にまで拡張することができる。しかし、これでは標準演算とスカラー演算が衝突する場合がある。それは、2つの数学型\(M,N\)における二項演算で、\(N\)が\(M\)の基底であるかつ\(N\hookrightarrow M\)、もしくは\(M\)が\(N\)の基底であるかつ\(M\hookrightarrow N\)を満たす場合であり、複素数型\({\mathbb R}\rightarrowtail{\mathbb C}\)同士の二項演算といった多元数における二項演算がこれに当てはまる。
 実際、数学型\(K\)上の多元数型\(H\)で\(K\rightarrowtail H\)と\(K\rightarrowtail H\)における演算は標準演算となるべきであるが、\(K\hookrightarrow(K\rightarrowtail H)\)であり、スカラー演算にもなってしまうため、このままでは標準演算とスカラー演算とで矛盾が生じる。\(K\hookrightarrow(K\rightarrowtail H)\)のような関係について考えると、所謂\(K\rightarrowtail H\)は\(K\)を拡大したものであることがわかる。詳しくは後述(公理と独立性にて解説)するため天下り的になるが、以下の定義を用いることである程度解決をすることができる。

定義

 同じ長さの鎖をもつ2つの数学型\(M,N\)について、\(N\hookrightarrow M\)かつそれぞれから生成される同じ長さ\(k\)の鎖をもつ数学型\(M_{k},N_{k}\)とあらわすとして、\(N_{k}\hookrightarrow M_{k}\)といった関係が全ての基底で成り立つとき、\(M\)を\(N\)の真に上位の数学型と定め、\(N\overset{\bullet}{\hookrightarrow}M\)とあらわす。同様にして、\(N\)を\(M\)の真に下位の数学型と定める。

 このとき、\(N\not\overset{\bullet}{\hookrightarrow}M\)かつ\(M\not\overset{\bullet}{\hookrightarrow}N\)を満たすことがスカラー演算を適用するための必要条件とすることで、前述した矛盾は大凡解決することができる。実際、任意の数学型\(M\)で\(M\)同士の二項演算は標準演算に分類されるべきであるが、\(M\overset{\bullet}{\hookrightarrow}M\)という自明な関係をもつため、この制約は自然といえる。
 他にも、例えば複素数型\({\mathbb R}\rightarrowtail{\mathbb C}\)と四元数型\({\mathbb R}\rightarrowtail{\mathbb H}\)に関して

\[({\mathbb R}\rightarrowtail{\mathbb C})\overset{\bullet}{\hookrightarrow}({\mathbb R}\rightarrowtail{\mathbb H})\]

を満たし、正しく標準演算に分類されることがわかる。しかし、\({\mathbb R}\rightarrowtail{\mathbb H}\)を\({\mathbb Z}\rightarrowtail{\mathbb H}\)としたときはこれを満たさなくなる。このとき、\({\mathbb R}\rightarrowtail{\mathbb C}\)と基底を交換することで

\[({\mathbb Z}\rightarrowtail{\mathbb C})\overset{\bullet}{\hookrightarrow}({\mathbb R}\rightarrowtail{\mathbb H})\]

となり、真に上位もしくは真に下位の関係が成り立つ。
 これが一般にも成り立つとすれば、数学型\(K_{1}\)上の数学型\(M_{1}\)と数学型\(K_{2}\)上の数学型\(M_{2}\)における演算では

\[\begin{cases}(K_{1}\rightarrowtail M_{1})\overset{\bullet}{\hookrightarrow}(K_{2}\rightarrowtail M_{2}) \\(K_{2}\rightarrowtail M_{2})\overset{\bullet}{\hookrightarrow}(K_{1}\rightarrowtail M_{1}) \\(K_{2}\rightarrowtail M_{1})\overset{\bullet}{\hookrightarrow}(K_{1}\rightarrowtail M_{2}) \\(K_{1}\rightarrowtail M_{2})\overset{\bullet}{\hookrightarrow}(K_{2}\rightarrowtail M_{1})\end{cases}\]

のいずれかを満たすときは標準演算、すなわちどれも満たさないときはスカラー演算となる。また、このような広義化した(弱くした)真に上位および真に下位の関係についての定義を与える。

定義

 数学型\(M,N\)が\(N\overset{\bullet}{\hookrightarrow}M\)を満たすか、数学型\(K_{1},K_{2}\)と数学型\(M',N'\)により

\[M=(K_{1}\rightarrowtail M'),\ \ \ N=(K_{2}\rightarrowtail N')\]

とあらわされ、

\[\begin{cases}(K_{1}\rightarrowtail M')\overset{\bullet}{\hookrightarrow}(K_{2}\rightarrowtail N') \\(K_{2}\rightarrowtail M')\overset{\bullet}{\hookrightarrow}(K_{1}\rightarrowtail N')\end{cases}\]

のいずれかを満たすならば、\(M\)を\(N\)の弱い真に上位の数学型と定め、\(N\underset{weak}{\overset{\bullet}{\hookrightarrow}}M\)とあらわす。同様にして、\(N\)を\(M\)の弱い真に下位の数学型と定める。

 また、同様にして上位の数学型についても弱い定義を与えることができる。

定義

 数学型\(M,N\)が\(N\hookrightarrow M\)を満たすか、数学型\(K_{1},K_{2}\)と数学型\(M',N'\)により

\[M=(K_{1}\rightarrowtail M'),\ \ \ N=(K_{2}\rightarrowtail N')\]

とあらわされ、

\[\begin{cases}(K_{1}\rightarrowtail M')\hookrightarrow(K_{2}\rightarrowtail N') \\(K_{2}\rightarrowtail M')\hookrightarrow(K_{1}\rightarrowtail N')\end{cases}\]

のいずれかを満たすならば、\(M\)を\(N\)の弱い上位の数学型と定め、\(N\underset{weak}{\hookrightarrow}M\)とあらわす。同様にして、\(N\)を\(M\)の弱い下位の数学型と定める。

 この定義を用いることで、スカラー演算におけるスカラー型が”数学型の基底による適用可能性”の場合と比較して、スカラー型として作用する基底が弱い上位および下位の数学型にまで拡張することができる。これにより、整数四元数ベクトル型\({\mathbb Z}\rightarrowtail{\mathbb H}\rightarrowtail {\mathbb V}\)と複素数型\({\mathbb R}\rightarrowtail{\mathbb C}\)間における積はスカラー演算として分類させることができる。

標準演算による適用可能性

 ”上位もしくは下位の数学型による適用可能性”というのは”数学型の基底による適用可能性”により与えられるスカラー型を弱い上位の数学型と弱い下位の数学型まで拡張するということをしたが、ここでは別のアプローチによる拡張をする。
 例えば、ブロック化されたベクトルと行列の積について、\(a_{1,1},a_{1,2},a_{2,1},a_{2,2},b_{1},b_{2},c_{1},c_{2},d_{1},d_{2}\in{\mathbb R}\)を用いれば

\[\begin{pmatrix}a_{1,1} & a_{1,2} \\a_{2,1} & a_{2,2}\end{pmatrix}\begin{pmatrix}\begin{pmatrix}b_{1} \\b_{2}\end{pmatrix}\\\begin{pmatrix}c_{1} \\c_{2}\end{pmatrix}\\\begin{pmatrix}d_{1} \\d_{2}\end{pmatrix}\end{pmatrix}=\begin{pmatrix}\begin{pmatrix}a_{1,1} & a_{1,2} \\a_{2,1} & a_{2,2}\end{pmatrix}\begin{pmatrix}b_{1} \\b_{2}\end{pmatrix}\\\begin{pmatrix}a_{1,1} & a_{1,2} \\a_{2,1} & a_{2,2}\end{pmatrix}\begin{pmatrix}c_{1} \\c_{2}\end{pmatrix}\\\begin{pmatrix}a_{1,1} & a_{1,2} \\a_{2,1} & a_{2,2}\end{pmatrix}\begin{pmatrix}d_{1} \\d_{2}\end{pmatrix}\end{pmatrix}\]

のようなスカラー演算が考えられる。これの自然な拡張を考えれば、

\[\begin{pmatrix}a_{1,1} & a_{1,2} \\a_{2,1} & a_{2,2}\end{pmatrix}\begin{pmatrix}\begin{pmatrix}b_{1} \\b_{2}\end{pmatrix}\\\begin{pmatrix}c_{1} \\c_{2}\end{pmatrix}\end{pmatrix}=\begin{pmatrix}a_{1,1}\begin{pmatrix}b_{1} \\b_{2}\end{pmatrix}+a_{1,2}\begin{pmatrix}c_{1} \\c_{2}\end{pmatrix}\\a_{2,1}\begin{pmatrix}b_{1} \\b_{2}\end{pmatrix}+a_{2,2}\begin{pmatrix}c_{1} \\c_{2}\end{pmatrix}\end{pmatrix}\]

ではなく

\[\begin{pmatrix}a_{1,1} & a_{1,2} \\a_{2,1} & a_{2,2}\end{pmatrix}\begin{pmatrix}\begin{pmatrix}b_{1} \\b_{2}\end{pmatrix}\\\begin{pmatrix}c_{1} \\c_{2}\end{pmatrix}\end{pmatrix}=\begin{pmatrix}\begin{pmatrix}a_{1,1} & a_{1,2} \\a_{2,1} & a_{2,2}\end{pmatrix}\begin{pmatrix}b_{1} \\b_{2}\end{pmatrix}\\\begin{pmatrix}a_{1,1} & a_{1,2} \\a_{2,1} & a_{2,2}\end{pmatrix}\begin{pmatrix}c_{1} \\c_{2}\end{pmatrix}\end{pmatrix}\]

となることが自然である。さらに\(a_{3,1},a_{3,2}\in{\mathbb R}\)で

\[\begin{pmatrix}a_{1,1} & a_{1,2} \\a_{2,1} & a_{2,2} \\a_{3,1} & a_{3,2}\end{pmatrix}\begin{pmatrix}\begin{pmatrix}b_{1} \\b_{2}\end{pmatrix}\\\begin{pmatrix}c_{1} \\c_{2}\end{pmatrix}\end{pmatrix}=\begin{pmatrix}\begin{pmatrix}a_{1,1} & a_{1,2} \\a_{2,1} & a_{2,2} \\a_{3,1} & a_{3,2}\end{pmatrix}\begin{pmatrix}b_{1} \\b_{2}\end{pmatrix}\\\begin{pmatrix}a_{1,1} & a_{1,2} \\a_{2,1} & a_{2,2} \\a_{3,1} & a_{3,2}\end{pmatrix}\begin{pmatrix}c_{1} \\c_{2}\end{pmatrix}\end{pmatrix}\]

となることが自然である。
 以上のことを踏まえると、2つの数学型\(M,N\)で\(N\)をスカラー型とするスカラー演算の十分条件の1つとして、\(K\overset{1}{\rightarrowtail}M\)を満たす\(K\)と標準演算をすると考えられる。このとき、十分条件を\(M\)の基底とするということも考えられるが、本質的にスカラー演算というのは\(N\)と\(K\)による演算である。そのため、\(K\)の基底に関する条件をも含めるのはあまりにも広義であり、適当ではないと考えられる。

適用可能性のまとめ

 以上のことより、スカラー演算の適用可能性の適用条件は以下のように与えられる。

定義

 数学型\(M,N\)において、\(N\underset{weak}{\overset{\bullet}{\hookrightarrow}} M\)および\(M\underset{weak}{\overset{\bullet}{\hookrightarrow}}N\)でないとき、以下のいずれかを満たすならば、\(M\)と\(N\)における二項演算は\(N\)をスカラー型とするスカラー演算に分類される。

\(N'\underset{weak}{\hookrightarrow}N\)を満たす\(N'\)が存在して、それは\(M\)の基底である。

\(N\underset{weak}{\hookrightarrow}N'\)を満たす\(N'\)が存在して、それは\(M\)の基底である。

\(K\overset{1}{\rightarrowtail}M\)を満たす\(K\)と\(N\)が標準演算をする。

 このとき、この条件を満たさない演算は標準演算に分類される。

演算の適用可能性の独立性と数学型の公理

 これまでの議論により、標準演算およびスカラー演算の適用可能性の適用条件を与えてきたが、それぞれの適用条件は独立でなければならない。なぜならば、独立でなければスカラー演算なのに標準演算とも成り得るような状況が生じる可能性があるためである。
 標準演算の適用可能性の適用条件の定義より、スカラー演算の適用可能性の適用条件の否定と定義されているため、標準演算とスカラー演算に関しては独立である。次に、左スカラー演算と右スカラー演算の独立性を示す必要があるが、現状それを解決できていない(プログラムに関しては現状として正常に動作しているため、早急な証明や理論の修正が必要としていないことからやる気がそこまで起きないというのが正しい)

 場を濁す感じではあるが、以下では数学型における公理を扱う。

数学型の基底に関する公理

 数学型の基底について立ち戻れば、その厳密な定義を与えることができていないことが窺える。例えば、自然数型\({\mathbb N}\)のそれぞれの桁が自然数で構成されることから自然数型\({\mathbb N}\)上で構築される、すなわち

\[\cdots\rightarrowtail{\mathbb N}\rightarrowtail\cdots\rightarrowtail{\mathbb N}\rightarrowtail{\mathbb N}\]

のような無限の親子関係と考えることが可能であるが、このような表現は本意ではない。そこで、上位の数学型の定義を用いると、基底における上位下位の二項関係はそれらを基底として同じ数学型で束縛したとき、上下関係は保存されるべきである。そこで、以下の公理を与える。

公理

 数学型\(K\)に対して\(K\hookrightarrow K'\)を満たす数学型\(K'\)が存在するとき、それぞれを数学型\(M\)で束縛したとき\(K,K'\)に対する上位下位の二項関係は保存され、それは逆も成り立つ。

\[K\hookrightarrow K'\ \Leftrightarrow\ (K\rightarrowtail M)\hookrightarrow (K'\rightarrowtail M)\]

 この公理を与えることにより、前述した自然数型のパラドックスが解消される。また、この公理を満たすということが数学型が基底をもつこと、すなわちプログラム上で数学型に対してテンプレート引数をもつことの必要十分条件となる
 実際には、以下のようなもう少し強くした公理を用いる。

公理

 数学型\(K\)に対して\(K\hookrightarrow K'\)を満たす数学型\(K'\)が存在するとき、それぞれを適当な数学型\(T\)で\((T\rightarrowtail N)\hookrightarrow(T\rightarrowtail M)\)を満たす数学型\(M,N\)で\(K\rightarrowtail N,K'\rightarrowtail M\)と束縛したとき\(K,K'\)に対する上位下位の二項関係は保存され、それは逆も成り立つ。

\[K\hookrightarrow K',(T\rightarrowtail N)\hookrightarrow(T\rightarrowtail M)\ \Leftrightarrow\ (K\rightarrowtail N)\hookrightarrow (K'\rightarrowtail M)\]

 これは、完備な数学型では上位下位の二項関係による半順序が定義されるのに対して、完備でない数学型でも上位下位の二項関係に似た半順序が存在することを示す。例えば、複素数型\({\mathbb C}\)と四元数型\({\mathbb H}\)は数学型として完備ではないが、適当な数学型\(T\)を与えたときに\((T\rightarrowtail{\mathbb C})\hookrightarrow(T\rightarrowtail{\mathbb H})\)を満たす。
 ここで、真に上位の数学型の定義に立ち戻れば、上記の公理を用いることで以下のように簡潔に書き換えることができる。

定義'

 2つの数学型\(M,N\)について、\(N\hookrightarrow M\)かつ同じ長さの鎖をもつならば、\(M\)を\(N\)の真に上位の数学型と定める。

 最初に与えた真に上位の数学型の定義というのは、公理なしに記述した場合のものである

 基本的な理論はここまで。

 基本的に定義をそのまま記述するだけである。プログラム的には定義と公理の違いというのは、プログラムで直接記述するのが定義、記述をしないのが公理という立ち位置となる。

数学型の構築

 C++における数学型の表現はほとんどの場合クラスを用いて表現される。ここで、Typeという数学型を考えれば、基底をもつ場合ともたない場合で以下のような表現をすることができる。

// Typeが基底をもたないときの定義
class Type {
	// 略
};
// Typeが基底をもつときの定義
template <class T>
class Type {
	// 略
public:
	// 基底の数学型
	using basis_type = T;
	// 基底の再束縛
	template <class Other>
	struct rebind {
		using other = Type<Other>;
	};

	// 略
};

 このとき、基底の再束縛rebindを与えているのは弱い上位の数学型および弱い真に上位の数学型の定義が基底の交換により定義されているからであるTypeに対してaddition_traitsおよびmultiplication_traitsの部分特殊化を与えることで基本的な記述が完成する。
 このとき、Typeというのは数学型として完備ではなく、テンプレート引数に実体を持つ数学型が指定されたときに完備となる。つまり、任意の数学型TによりType<T>とあらわされる数学型は完備ではない。
 また、数学型であることを判定するメタ関数は

namespace iml {

	// 数学型の判定(addition_traitsとmultiplication_traitsのインスタンスが生成可能)
	template <class T>
	struct is_math_type_impl {
	private:
		template <class U> static auto tester(U*) -> decltype(U(), std::true_type());
		template <class U> static std::false_type tester(...);
	public:
		static constexpr bool value = decltype(tester<addition_traits<T>>(nullptr))::value && decltype(tester<multiplication_traits<T>>(nullptr))::value;
	};
	template <class T>
	struct is_math_type : std::bool_constant<is_math_type_impl<std::remove_cv_t<T>>::value> {};
	template <class T>
	inline constexpr bool is_math_type_v = is_math_type<T>::value;
}

のように実装をすることができ、基底をもつ数学型であることおよび基底の取得に関しては

namespace iml {

	// 基底をもつ数学型である(basis_typeが内部で定義されている)ことの判定
	template <class, class = void>
	struct is_math_type_has_basis_impl : std::false_type {};
	template <class T>
	struct is_math_type_has_basis_impl<T, std::void_t<typename T::basis_type>> : is_math_type<T> {};
	template <class T>
	struct is_math_type_has_basis : is_math_type_has_basis_impl<std::remove_cv_t<T>> {};
	template <class T>
	inline constexpr bool is_math_type_has_basis_v = is_math_type_has_basis<T>::value;

	// Tの基底の取得(存在しないときはvoid)
	template <class T, bool = is_math_type_has_basis_v<T>>
	struct basis_as_math_type_impl {
		using type = typename T::basis_type;
	};
	template <class T>
	struct basis_as_math_type_impl<T, false> {
		using type = void;
	};
	template <class T>
	struct basis_as_math_type : basis_as_math_type_impl<std::remove_cv_t<T>> {};
	template <class T>
	using basis_as_math_type_t = typename basis_as_math_type<T>::type;
}

のように実装をすることができる。
 数学型によっては、例えば固定長ベクトル型等は基底の数学型以外に次元を示す固定パラメータをもつため、それを示すためにテンプレートパラメータを2つもつ。このように、数学型によってはテンプレートパラメータを複数もつため、基底をもつ数学型ならば一番最初のテンプレートパラメータが基底を示す数学型であると規定する。これ自体には理論的な意味はなく、単に数学型の記述の統一化をするだけである

各種定義の実装

 まずは完備な数学型\(T_{1},T_{2}\)で\(T_{1}\)が\(T_{2}\)の基底であることを判定する型特性関数を定義する。これは\(T_{2}\)に対してis_math_type_has_basis_vを適用しながら再帰的に\(T_{2}\)の全ての基底を走査することで実装することができる。

namespace iml {

	// T1がT2の基底であることの判定
	template <class T1, class T2, bool = std::is_same_v<T1, T2>, bool = is_math_type_has_basis_v<T2>>
	struct is_basis_as_math_type_impl : std::true_type {};
	template <class T1, class T2>
	struct is_basis_as_math_type_impl<T1, T2, false, false> : std::false_type {};
	template <class T1, class T2>
	struct is_basis_as_math_type_impl<T1, T2, false, true>
		: is_basis_as_math_type_impl<T1, typename T2::basis_type> {};
	template <class T1, class T2>
	struct is_basis_as_math_type : std::bool_constant<is_basis_as_math_type_impl<std::remove_cv_t<T1>, std::remove_cv_t<T2>>::value && !std::is_same_v<std::remove_cv_t<T1>, std::remove_cv_t<T2>>> {};
	template <class T1, class T2>
	inline constexpr bool is_basis_as_math_type_v = is_basis_as_math_type<T1, T2>::value;
}

 次に、\(T_{2}\)が\(T_{1}\)の上位の数学型であることを判定するメタ関数を実装する。それらは上位の数学型となる全てのパターンを予めプログラム上で直接コードを記述する必要があり、例えば

	// T2がT1の上位の数学型かの判定
	template <class T1, class T2>
	struct is_high_rank_math_type : std::false_type {};
	template <class T1, class T2>
	inline constexpr bool is_high_rank_math_type_v = is_high_rank_math_type<T1, T2>::value;
	template <> struct is_high_rank_math_type<int8_t, int8_t> : std::true_type {};
	template <> struct is_high_rank_math_type<int8_t, int16_t> : std::true_type {};
	template <> struct is_high_rank_math_type<int8_t, int32_t> : std::true_type {};
	template <> struct is_high_rank_math_type<int8_t, int64_t> : std::true_type {};
	template <> struct is_high_rank_math_type<int16_t, int16_t> : std::true_type {};
	// 以下略

のように記述することとなる。ライブラリ側での実装については問題ないが、ユーザが外部からライブラリにとっては未知の数学型、例えば任意精度整数型を用いるときは、任意精度整数型に対する全てのis_high_rank_math_typeの組み合わせを記述する必要があり、それら全てを記述することは困難である。
 そこで、数学型は完備・完備でないにかかわらず上位の数学型について半順序が保証、例えば多元数型であれば

\[{\mathbb C}\leq{\mathbb H}\leq{\mathbb O}\]

となるため、マクロにより

// 多元数の半順序
#define IMATHLIB_ORDER_OF_HYPERCOMPLEX_TEMPLATE		(class, T1), (class, T2), (class, T3)
#define IMATHLIB_ORDER_OF_HYPERCOMPLEX				(iml::complex, T1), (iml::quaternion, T2), (iml::octonion, T3)

のように半順序を与え、マクロの展開により全てのis_high_rank_math_typeの組み合わせを記述する。具体的なマクロの実装法についてはそれなりに量があり、本題から逸れるため、ここでは扱わない
 次に、\(T_{2}\)が\(T_{1}\)の真に上位の数学型であることを判定するメタ関数を実装する。これは定義通りに記述することで以下のように実装される。

namespace iml {

	// T2がT1の真に上位の数学型であることの判定
	template <class T1, class T2, bool = is_high_rank_math_type_v<T1, T2>, bool = is_math_type_has_basis_v<T1>, bool = is_math_type_has_basis_v<T2>>
	struct is_principal_high_rank_math_type_impl : std::bool_constant<is_high_rank_math_type_v<T1, T2>> {};
	template <class T1, class T2, bool F1, bool F2>
	struct is_principal_high_rank_math_type_impl<T1, T2, false, F1, F2> : std::false_type {};
	template <class T1, class T2>
	struct is_principal_high_rank_math_type_impl<T1, T2, true, true, false> : std::false_type {};
	template <class T1, class T2>
	struct is_principal_high_rank_math_type_impl<T1, T2, true, false, true> : std::false_type {};
	template <class T1, class T2>
	struct is_principal_high_rank_math_type_impl<T1, T2, true, true, true> : is_principal_high_rank_math_type_impl<typename T1::basis_type, typename T2::basis_type> {};
	template <class T1, class T2>
	struct is_principal_high_rank_math_type : is_principal_high_rank_math_type_impl<std::remove_cv_t<T1>, std::remove_cv_t<T2>> {};
	template <class T1, class T2>
	inline constexpr bool is_principal_high_rank_math_type_v = is_principal_high_rank_math_type<T1, T2>::value;
}

 なお、このコードは公理を仮定していない場合のものである。
 is_high_rank_math_typeis_principal_high_rank_math_typeを用いることにより、弱い上位の数学型および弱い真に上位の数学型であることを判定するメタ関数を与えることができる。

namespace iml {

	// T2がT1の弱い上位の数学型であることの判定
	template <class T1, class T2, bool = (is_math_type_has_basis_v<T1> && is_math_type_has_basis_v<T2>)>
	struct is_weak_high_rank_math_type_impl : is_high_rank_math_type<typename T1::template rebind<typename T2::basis_type>::other, typename T2::template rebind<typename T1::basis_type>::other> {};
	template <class T1, class T2>
	struct is_weak_high_rank_math_type_impl<T1, T2, false> : std::false_type {};
	template <class T1, class T2>
	struct is_weak_high_rank_math_type : std::conditional_t<is_high_rank_math_type_v<T1, T2>, std::true_type, is_weak_high_rank_math_type_impl<std::remove_cv_t<T1>, std::remove_cv_t<T2>>> {};
	template <class T1, class T2>
	inline constexpr bool is_weak_high_rank_math_type_v = is_weak_high_rank_math_type<T1, T2>::value;


	// T2がT1の弱い真に上位の数学型であることの判定
	template <class T1, class T2, bool = (is_math_type_has_basis_v<T1> && is_math_type_has_basis_v<T2>)>
	struct is_weak_principal_high_rank_math_type_impl : is_principal_high_rank_math_type<typename T1::template rebind<typename T2::basis_type>::other, typename T2::template rebind<typename T1::basis_type>::other> {};
	template <class T1, class T2>
	struct is_weak_principal_high_rank_math_type_impl<T1, T2, false> : std::false_type {};
	template <class T1, class T2>
	struct is_weak_principal_high_rank_math_type : std::conditional_t<is_principal_high_rank_math_type_v<T1, T2>, std::true_type, is_weak_principal_high_rank_math_type_impl<std::remove_cv_t<T1>, std::remove_cv_t<T2>>> {};
	template <class T1, class T2>
	inline constexpr bool is_weak_principal_high_rank_math_type_v = is_weak_principal_high_rank_math_type<T1, T2>::value;
}

 最後に、標準演算およびスカラー演算の適用可能性の適用条件に関するメタ関数を実装する。

namespace iml {

	// スカラー演算及び標準演算の必要条件の記述
	// 左スカラー演算に成り得るかの判定
	template <template <class, class> class Op, class T1, class T2, bool>
	struct is_possibility_lscalar_operation_impl1;
	template <template <class, class> class Op, class T1, class T2>
	struct is_possibility_lscalar_operation : std::conditional_t<is_weak_principal_high_rank_math_type_v<T1, T2> || is_weak_principal_high_rank_math_type_v<T2, T1>, std::false_type, is_possibility_lscalar_operation_impl1<Op, T1, T2, is_math_type_has_basis_v<T2>>> {};
	template <template <class, class> class Op, class T1, class T2>
	inline constexpr bool is_possibility_lscalar_operation_v = is_possibility_lscalar_operation<Op, T1, T2>::value;
	// 右スカラー演算に成り得るかの判定
	template <template <class, class> class Op, class T1, class T2>
	struct is_possibility_rscalar_operation : is_possibility_lscalar_operation<Op, T2, T1> {};
	template <template <class, class> class Op, class T1, class T2>
	inline constexpr bool is_possibility_rscalar_operation_v = is_possibility_rscalar_operation<Op, T1, T2>::value;
	// スカラー演算と成り得るかの判定
	template <template <class, class> class Op, class T1, class T2>
	struct is_possibility_scalar_operation : std::bool_constant<is_possibility_lscalar_operation_v<Op, T1, T2> || is_possibility_rscalar_operation_v<Op, T1, T2>> {};
	template <template <class, class> class Op, class T1, class T2>
	inline constexpr bool is_possibility_scalar_operation_v = is_possibility_scalar_operation<Op, T1, T2>::value;
	// 標準演算と成り得るかの判定
	template <template <class, class> class Op, class T1, class T2>
	struct is_possibility_standard_operation : std::bool_constant<!is_possibility_scalar_operation_v<Op, T1, T2>> {};
	template <template <class, class> class Op, class T1, class T2>
	inline constexpr bool is_possibility_standard_operation_v = is_possibility_standard_operation<Op, T1, T2>::value;


	// is_standard_operationの定義がここに記述される
	template <template <class, class> class Op, class T1, class T2>
	inline constexpr bool is_standard_operation_v = is_standard_operation<Op, T1, T2>::value;


	// 左スカラー演算になり得るかの判定でis_standard_operation_vを用いるためここで定義
	template <class T1, class T2, bool = is_math_type_has_basis_v<T2>, bool = (is_weak_high_rank_math_type_v<T1, T2> || is_weak_high_rank_math_type_v<T2, T1>)>
	struct is_possibility_lscalar_operation_impl2 : std::true_type {};
	template <class T1, class T2>
	struct is_possibility_lscalar_operation_impl2<T1, T2, false, false> : std::false_type {};
	template <class T1, class T2>
	struct is_possibility_lscalar_operation_impl2<T1, T2, true, false> : is_possibility_lscalar_operation_impl2<T1, typename T2::basis_type> {};
	template <template <class, class> class Op, class T1, class T2, bool = is_math_type_has_basis_v<T2>>
	struct is_possibility_lscalar_operation_impl1 : std::bool_constant<is_standard_operation_v<Op, T1, typename T2::basis_type> || is_possibility_lscalar_operation_impl2<T1, typename T2::basis_type>::value> {};
	template <template <class, class> class Op, class T1, class T2>
	struct is_possibility_lscalar_operation_impl1<Op, T1, T2, false> : std::false_type {};
}

 このとき、テンプレート引数Opというのは

namespace iml {

	// 加算可能
	template <class T1, class T2>
	struct is_addable;
	// 減算可能
	template <class T1, class T2>
	struct is_subtractable;
	// 乗算可能
	template <class T1, class T2>
	struct is_multipliable;
	// 除算可能
	template <class T1, class T2>
	struct is_divisible;
}

のようにして与えられる演算可能判定をするメタ関数である。

各種演算の実装

 スカラー演算の適用可能性の実装にis_standard_operationを用いたが、まずはこのような2項演算がスカラー演算および標準演算となるかを判定するメタ関数を実装する。このとき、このメタ関数はis_possibility_lscalar_operationを用いて再帰的に実装されるため、この再帰が停止(収束)可能となるように留意して実装する必要がある。

namespace iml {

	// 実際にスカラー演算と標準演算を定義するときに用いる
	// 実際に部分特殊化するのはis_~_impl2
	// 左スカラー演算となるかの判定
	template <template <class, class> class Op, class T1, class T2>
	struct is_lscalar_operation_impl2 : Op<T1, T2> {};
	template <template <class, class> class Op, class T1, class T2, bool = is_possibility_lscalar_operation_v<Op, T1, T2>>
	struct is_lscalar_operation_impl1 : is_lscalar_operation_impl2<Op, T1, T2> {};
	template <template <class, class> class Op, class T1, class T2>
	struct is_lscalar_operation_impl1<Op, T1, T2, false> : std::false_type {};
	template <template <class, class> class Op, class T1, class T2>
	struct is_lscalar_operation : is_lscalar_operation_impl1<Op, T1, T2> {};
	template <template <class, class> class Op, class T1, class T2>
	inline constexpr bool is_lscalar_operation_v = is_lscalar_operation<Op, T1, T2>::value;
	// 右スカラー演算となるかの判定
	template <template <class, class> class Op, class T1, class T2>
	struct is_rscalar_operation_impl2 : Op<T1, T2> {};
	template <template <class, class> class Op, class T1, class T2, bool = is_possibility_rscalar_operation_v<Op, T1, T2>>
	struct is_rscalar_operation_impl1 : is_rscalar_operation_impl2<Op, T1, T2> {};
	template <template <class, class> class Op, class T1, class T2>
	struct is_rscalar_operation_impl1<Op, T1, T2, false> : std::false_type {};
	template <template <class, class> class Op, class T1, class T2>
	struct is_rscalar_operation : is_rscalar_operation_impl1<Op, T1, T2> {};
	template <template <class, class> class Op, class T1, class T2>
	inline constexpr bool is_rscalar_operation_v = is_rscalar_operation<Op, T1, T2>::value;
	// 標準演算となるかの判定
	template <template <class, class> class Op, class T1, class T2>
	struct is_standard_operation_impl2 : Op<T1, T2> {};
	template <template <class, class> class Op, class T1, class T2, bool = is_possibility_standard_operation_v<Op, T1, T2>>
	struct is_standard_operation_impl1 : is_standard_operation_impl2<Op, T1, T2> {};
	template <template <class, class> class Op, class T1, class T2>
	struct is_standard_operation_impl1<Op, T1, T2, false> : std::false_type {};
	template <template <class, class> class Op, class T1, class T2>
	struct is_standard_operation : is_standard_operation_impl1<Op, T1, T2> {};
	template <template <class, class> class Op, class T1, class T2>
	inline constexpr bool is_standard_operation_v = is_standard_operation<Op, T1, T2>::value;
}

 また、実際にそれぞれの数学型について二項演算がスカラー演算および標準演算となるかを判定する場合には、is_lscalar_operation_impl2等を部分特殊化することで与える。例えば、複素数型complexに対しては以下のように部分特殊化して記述する。

namespace iml {

	// 複素数の加算でスカラー演算および標準演算となるかの判定
	template <class T1, class T2>
	struct is_lscalar_operation_impl2<is_addable, T1, complex<T2>> : std::bool_constant</* 略 */> {};
	template <class T1, class T2>
	struct is_rscalar_operation_impl2<is_addable, complex<T1>, T2> : std::bool_constant</* 略 */> {};
	template <class T1, class T2>
	struct is_standard_operation_impl2<is_addable, complex<T1>, complex<T2>> : std::bool_constant</* 略 */> {};
}

 このとき、略しているのは演算可能性を示す論理である。例えば、複素数型complex同士の加算であれば


namespace iml {

	template <class T1, class T2>
	struct is_standard_operation_impl2<is_addable, complex<T1>, complex<T2>> : std::bool_constant<is_addable_v<T1, T2>> {};
}

と記述される。これにより、複素数の加算は

namespace iml {

	// 標準演算
	template <class T1, class T2, class = std::enable_if_t<is_standard_operation_v<is_addable, complex<T1>, complex<T2>>>>
	auto operator+(const complex<T1>& lhs, const complex<T2>& rhs) {
		/* 略 */
	}
	// 右スカラー演算
	template <class T1, class T2, class = std::enable_if_t<is_rscalar_operation_v<is_addable, complex<T1>, T2>>>
	auto operator+(const complex<T1>& lhs, const T2& rhs) {
		/* 略 */
	}
	// 左スカラー演算
	template <class T1, class T2, class = std::enable_if_t<is_lscalar_operation_v<is_addable, T1, complex<T2>>>>
	auto operator+(const T1& lhs, const complex<T2>& rhs) {
		/* 略 */
	}
}

のように実装される。ただし、複素数型complexについては標準ライブラリのものではなく、独自に実装をする必要があることに留意しなければならない

 以上で基礎的な実装が完了した。

おわりに

 これにより、当初の目的であった数式を直感的に記述するということがほとんどの場合で達成される。現状の問題点として、同値な表現が存在する数学型の場合、例えば複素ベクトル型は

\[\begin{pmatrix}a+bi \\c+di\end{pmatrix}\in({\mathbb R}\rightarrowtail{\mathbb C}\rightarrowtail V)\ \Leftrightarrow\ \begin{pmatrix}a \\c\end{pmatrix}+i\begin{pmatrix}b \\d\end{pmatrix}\in({\mathbb R}\rightarrowtail V\rightarrowtail{\mathbb C})\]

のような2つの表現を持もち、この場合は上手く演算の適用をすることができない。もちろん、左の場合の複素ベクトル型を用いれば正しく演算ができる。このような問題を解決するには、理論を修正する公理を与えて数学型の制限を与えるかということとなるが、現状はそこまで良い解決方法が浮かんでいない(実用上そこまで問題にならないため考える気力が起きないというのが正しい)。

 また、演算を直感的に記述することのメリットとしては数学関数等の数値計算アルゴリズムがキャスト等なしに記述できるため汎用性が上がることがある。例えば、あるアルゴリズムを外部のライブラリ等からの数学型に対して適用したいとき、その数学型に関して適切にメタ関数等の特殊化を与えることで直ちに利用できる。数学関数の実装については、多変数関数を考慮してstd::common_typeの数学型バージョンであるcommon_math_type等を定義すると、より数学関数を抽象的に利用することができる。このとき、数学関数の引数は数学型であるという型制約を与えるといいだろう。

 ちなみに、これらの内容は私自身の(勝手に)やっている研究の一部である。そのうち(遅くても2020年2月下旬までには)、これらの理論をまとめたライブラリを公開する予定である。


前の記事
C++でグラフ型を作成
次の記事
棒人間アクションゲームの制作1

コメント欄(詳細な記述方法についてはこちら)

まだコメントの投稿はありません