トレイト境界は加法群なのか?

結論

(トレイト境界は加法郡では)ないです

きっかけ

Rustでトレイトの制限を記述する際、複数のトレイトを実装していて欲しい場合は+記号を使って表現します

where T: Clone + Default, ..

Rust習いたてでこの記法を使い始めたとき、言葉にならない違和感がありました
初めのうちは足してるのに=記号がないうんぬん、という表記の問題かなと思っていましたが、色々考えていく内にそういうわけでもないんじゃないかと思う様になりました

使う側(関数とか)から見ると、気になるのはその型で何ができるのかという「機能」が大事になってくるからトレイトを機能として見るならば自然な表現かな?というふうに自分を納得させていました

ただ、トレイト境界の記述は関数のジェネリクスだけではなくトレイト自身の制限を表現する際にも使われます

trait Additive: Closure + Associativity + IdentityElement + InverseElement {}

(トレイトの捉え方によるかもですが)この場合はむしろ&の方が自然に感じます
Additiveな型はClosureでかつAssociativityでかつ..という意味なので

より意味論にフォーカスしていくと、加法群における+演算の結果は一意ですが、トレイト境界を記述する場合そうはなりません(ps. この時点でそもそも群じゃないですね)
例えば、

trait X : A + B {}
trait Y : A + B {}

この時AとBは別々のトレイトとして扱われます

そもそもトレイトはタグ付けの様なものなので性質として加法群の+演算にはなり得ませんよね

じゃあ&で良くね..?なんか自分が見落としてるのかな..という具合でずっとモヤモヤしていました

回答

最近こんな記事を読みまして、まさにドンピシャ、言いたい事を言ってくれた感がありました
要は、曖昧な足すという言葉と数学的な足すを区別なく+で表現している点が自分の感じた違和感の正体でした(文字列結合に関してもその通りって感じですね)

トレイト境界が加法群でないことを数学的に証明するならば、単位元が存在しないことを示すのが簡単そう
実装次第では実質的に単位元となるトレイトを定義することはできますが、それはトレイト境界の性質ではなく個別具体的な実装の結果です

ちなみに逆元トレイトの表現っぽいもの自体はRustに存在しており
https://doc.rust-lang.org/beta/unstable-book/language-features/negative-impls.html
に記述があります

じゃあ他の表記の方がいいのか問題

トレイト境界が加法群でないのは良いとして、じゃあ+記号を使った表記はマズいのでしょうか
何かもっと良い表記はあるのでしょうか

幾つか候補を考えてみたのでみなさんも一緒に妄想してみましょう

and, or, not, xor ..

// pesudo rust code

trait X: A and B and (not C or D) {}

//もしくは

trait Y: A && B && (!C || D) {}

一つは論理演算で表現する方法です
現にトレイトを実装していない事を確約する文法では減算記号ではなく否定を意味する!記号が使われています(上記のnegative-implsのやつですね)
orやxorがあったら面白いだろうなーとは思うのですが使う機会はほとんど無さそうですね
どの道、機能を持っていることが保証されていて欲しいケースが大半だと思うので

集合記号

// pesudo rust

// comp はcomplement(補集合)の略
trait X: A cap B cap (comp C cup D) - E {}

もう一つは集合記号を使う方法(どうやってキーボードで打つの)
トレイトを型の分類と捉えた場合は自然な選択に思えます
トレイトに求められているのはそれだけではない感はありますが..
後は、表現に馴染みがないのと無闇に予約語が増えてしまうのもいただけない点です

関数を使う

これは現状のRustの型システムを大幅に改変する必要がありそうですが、プログラミング言語っぽい表現方法で面白いんじゃない?ってことで考えてみます

// pesudo rust

// 通常の関数として
trait X: trait_bounds(&[A, B]) {}

// 何かしらのメソッドとして
trait Y: TraitBounds::default()
	.deps(&[A, B])
	.dep(TraitBounds::new().either(C.not(), D))
	.exc(E)
{}

// メソッド2
trait Y {
	#[trait_bounds]
	fn traits() -> TraitBounds {
		Y::default_traits()
			.deps(&[
				A,
				B,
				D.or(C.not()),
			])
			.exclude(E)
			.optional(F)
	}
}

マクロであれば現状から大きな変更なく近いことは実現できそう
トレイト境界の表現としては余り利点を感じないどころか普通に解りにくいですが、const genericsを絡めると面白そうだなーと感じています

fn takes_const_generics<N: usize, T>() -> T
// ブロックにした方が見やすそう
where {
	if N < 3 {
		T::default_traits()
			.deps(&[
				A,
				B,
				D.or(C.not()),
			])
			.exclude(E)
			.optional(F)
	} else {
		compile_error!("error message")
	}
}{
	// 関数本体	
}

現状const genericsを利用して条件分岐のような高度な事をしようとすると、橋渡しをするトレイトを用意する必要があったり文法がadhocだったりして取っ付きにくいなーと感じています
それを避ける方法として現状でもマクロを使うと言う選択肢があります
確かにRustのマクロは強力ですがあまり安直にマクロを使用したく無いのと、ジェネリクスの問題はジェネリクスの方法で解決する方が良いと思っています
それをいきなりマクロに頼ってしまうと、じゃあ何でもマクロで解決すればいーじゃん、となりかねません

toml形式

こいつ急にどうしたって思われるかも知れないですが、トレイト同士の関係ってパッケージの依存関係と似てません?
理由それだけ

終わりに

なんだかんだ+記号を使うのって理に適った選択だったんだなと思いました
考えてて楽しかったです まる

杉浦 寛行

 杉浦 寛行

スキル:Rust/Lua/Java/C/C++/Lisp/TypeScript/WebAssembly/Assembly/osdev/Neovim/nix/AWS/Linux

コメント

この記事へのコメントはありません。

関連記事