Rust のダウンキャストめんどくさいでござる問題を mopa crate ですっきりするメモ
trait T
を実装した struct S
について、 S
から &dyn T
へアップキャストした後、 &dyn T
から &S
へダウンキャストでき…ません。Rustでtrait
継承のdyn
なオブジェクトを扱おうとすると悲しむポイントです。
trait T { fn hello(&self); } struct S {} impl T for S{ fn hello(&self){ println!("It's S!") } } impl S { fn hi(&self) { print!( "I can do HI!" ) }
↑これが定義できる、というところまではよいとして、↓はできません。
fn main() { // OOPの派生型に相当する型の元のオブジェクト let s: S = S{}; // OOPの基底型に相当するトレイトオブジェクトレベルでの保持 let rs: &dyn T = &s as &dyn T; // トレイトオブジェクトレベルでの関数呼び出しと自動的な派生型の関数へのディスパッチ rs.hello(); // OOPで言うところの基底型から派生型へのダウンキャスト let s: &S = rs as &S; // ダウンキャスト済みなのでダウンキャスト先の派生型にしかない関数も呼べるはず s.hi(); }
error[E0605]: non-primitive cast: `&dyn T` as `&S` --> src\main.rs:9:14 | 9 | let s: &S = rs as &S; | ^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object error: aborting due to previous error
「にゃーん?」って言ったら助けてくれるイケメンが臨席にいればよいのですが、いません。これは↓のように std::any::Any
を介するとできます:
// にゃーんだけど一応動く…と見せかけて期待した実行結果にならないやつ trait T: std::any::Any { fn hello(&self); } struct S { } impl T for S{ fn hello(&self){ println!("It's S!") } } impl S { fn hi(&self) { print!( "I can do HI!" ) } } fn main() { // OOPの派生型に相当する型の元のオブジェクト let s: S = S{}; // OOPの基底型に相当するトレイトオブジェクトレベルでの保持 let rs: &dyn std::any::Any = &s as &dyn std::any::Any; // トレイトオブジェクトレベルでの関数呼び出しと自動的な派生型の関数へのディスパッチ match rs.downcast_ref::<&dyn T>() { // Any から T へのダウンキャスト、呼べる…ハズヨネ?? Some(t) => t.hello(), None => println!("downcast to T was failed.") } // OOPで言うところの基底型から派生型へのダウンキャスト match rs.downcast_ref::<S>() { // ダウンキャスト済みなのでダウンキャスト先の派生型にしかない関数も呼べるはず Some(s) => s.hi(), None => println!("downcast to S was failed.") } }
downcast to T was failed. I can do HI!
Rustなのにいろいろすっとばして突然の動的型付け言語化です。お気持ちとしては Any
までアップキャストしなくてもいいのよ… C++ の dynamic_cast
的な…と思いますが、それを trait
で実装しているのが std::any::Any
さんなのでにゃーんです。翻訳できて実行できて、だけど Any
から T
へのダウンキャストに失敗しています。悲しい。
Any
から T
へ .downcast_ref
できないので、仕込みを増やしてどうにかします:
// Any 化するトレイトを追加 trait A { fn as_any(&self) -> &dyn std::any::Any; } // T を A 派生にして trait T: A { fn hello(&self); } struct S { } impl T for S{ fn hello(&self){ println!("It's S!") } } impl S { fn hi(&self) { print!( "I can do HI!" ) } } // ジェネリクスで T を実装する型 X で as_any を実装 impl<X:'static + T> A for X { fn as_any(&self) -> &dyn std::any::Any { self as &dyn std::any::Any } } fn main() { // OOPの派生型に相当する型の元のオブジェクト let s: S = S{}; // OOPの基底型に相当するトレイトオブジェクトレベルでの保持 let rt: &dyn T = &s as &dyn T; // トレイトオブジェクトレベルでの関数呼び出しと自動的な派生型の関数へのディスパッチ rt.hello(); // OOPで言うところの基底型から派生型へのダウンキャスト match rt.as_any().downcast_ref::<S>() { // ダウンキャスト済みなのでダウンキャスト先の派生型にしかない関数も呼べるはず Some(s) => s.hi(), None => println!("downcast to S was failed.") } }
It's S! I can do HI!
一応、出力としては理想を得られました。新たに trait A
を用意するのは1回だけ、 S
の仲間の T
派生型たちが増えてもしんどくなりません。 impl<X: 'static + T> A for X
も S
に依存せず S
やそのお仲間の T
派生型たちが増えてもこの1回の定義だけで対応できます。 'static
は…つらいですね。
設計を enum
や union
へ変更できる場合はしちゃったらいい状況もありますが、それはそれ、適材適所でOOP継承風のパターンが楽な事はあります。と、いうわけで mopa crate の出番です🎉
// mopafy マクロ、かわいいですね! use mopa::mopafy; // B を mopa::Any 派生にして; 先の例までは T にしていましたが、 mopafy! マクロ内部で T だと事故が起こるので。通常の命名で問題になることはたぶんないでしょう。 trait B: mopa::Any { fn hello(&self); } // おまじないを唱えて mopafy!(B); struct S { } impl B for S{ fn hello(&self){ println!("It's S!") } } impl S { fn hi(&self) { print!( "I can do HI!" ) } } fn main() { // OOPの派生型に相当する型の元のオブジェクト let s: S = S{}; // OOPの基底型に相当するトレイトオブジェクトレベルでの保持 let rt: &dyn B = &s as &dyn B; // トレイトオブジェクトレベルでの関数呼び出しと自動的な派生型の関数へのディスパッチ rt.hello(); // OOPで言うところの基底型から派生型へのダウンキャスト match rt.downcast_ref::<S>() { // ダウンキャスト済みなのでダウンキャスト先の派生型にしかない関数も呼べるはず Some(s) => s.hi(), None => println!("downcast to S was failed.") } }
It's S! I can do HI!
😭mopa🙏 ありがとう。ナマスカール🙏
おまけ1: enum
化
設計を変えて trait
継承とアップキャストによるディスパッチを enum
に変更する場合は、
trait T { fn hello(&self); } struct S { } impl T for S{ fn hello(&self){ println!("It's S!") } } impl S { fn hi(&self) { print!( "I can do HI!" ) } } enum E { S(S), /* 実際には T, U, V, ... 派生型をたくさん詰めます */ } fn main() { // OOPの派生型に相当する型の元のオブジェクト let s: S = S{}; // OOPの基底型に相当するオブジェクトを enum へ変更 let e: E = E::S(s); // OOPで言う基底型から派生型へのダウンキャストに相当する enum のディスパッチ match e { E::S(s) => { // S は T を実装しているので共通関数も呼べる s.hello(); // もちろん S の関数も呼べる s.hi(); } /* 実際には T, U, V, ... 派生型ののディスパッチを書く */ } }
と、なります。一見これでいいじゃない的に見えるのはディスパッチ先が1種類しか無いお勉強向けの実装例だからです。enum
を使うとディスパッチなしでは中身へアクセスできないので dyn T
は S
など派生型の制約としては意味があるものの、共通関数を dyn T
レベルから呼べるような機能は enum
ではディスパッチ無しでは実現できません。つまり、ディスパッチ先が増えれば共通処理を呼ぶための退屈なディスパッチを大量に並べる事になります。うつくしくない…😭
おまけ2: union
で無理やり reinterpret_cast しちゃったらどうなるの?
union
だと Copy+Clone
, unsafe
が必要になるものの、型システムは巧い具合に(ある意味でバギーに)騙されたりするだろうか?なんて邪悪な事も試してみましたが、
// 基底型に相当させる enum を作り union SuperTypeLikeUnion { // ディスパッチ対象の派生型に相当する型をフィールドへいれる a: StructA, b: StructB, } trait HasHello { fn hello(&self); } #[derive(Copy, Clone)] struct StructA {} #[derive(Copy, Clone)] struct StructB {} impl HasHello for StructA { fn hello(&self) { println!("It's A!!") } } impl HasHello for StructB { fn hello(&self) { println!("It's B!!") } } fn main() { let a: StructA = StructA{}; let e = SuperTypeLikeUnion{a}; unsafe { // 実際のナカミは A, ぶっちぎりディスパッチも A let dangerous1 = &e.a as &dyn HasHello; dangerous1.hello(); } let b: StructB = StructB{}; let e = SuperTypeLikeUnion{b}; unsafe { // 実際のナカミは B, ぶっちぎりディスパッチは A let dangerous2 = &e.a as &dyn HasHello; dangerous2.hello(); } }
この結果は
t's A!! It's A!!
でした。型システムの実装を考えれば、まあ、そうですよね😅 C++ の RTTI による dynamic_cast
や関数ポインターレベルの操作が何故かここで行われるはずもなく。
今回のメモのだいじなところ
- オブジェクト指向言語向けっぽい UML で設計したなコイツ…的なソフトウェアの設計を Rust で実装するはめになったら mopa crate を検討しましょう。
- Rust らしい
enum
ディスパッチは安全でうつくしーですが、OOPのクラスやインターフェースの継承構造を変換するには退屈なディスパッチを大量に書くことになるので適材適所で mopa りましょう。 Let's mopafy!
参考
Rust の tia crate で accessor とか interface 的な trait 群の impl を楽できるようになりました。のメモ
🎉publishedhttps://t.co/hJD0s7Leoh: tiahttps://t.co/Rpx0g9zfdp
— Usagi Ito 👩💻 High-tech Hermit 🧙♀️ (@usagi_network) August 16, 2020
github: usagi/tiahttps://t.co/IrrhxsH58R
tia; trait, impl accessors | automatic
This is a syntax sugar proc-macro crate for trait, impl accessors patterns.#Rust #rustlang
と、いうわけで tia crate を公開しました。
tia は何をしてくれる lib な crate ですか?
tia
を使うとstruct
|enum
|union
のフィールドに Getter/Setter 的なアクセサーのimpl
を全自動化できます。
はじめて使用、あるいは tia
の機能の概要を知りたい場合用の簡単なエグザンプルは以下のコードです:
use tia::Tia; // 1. use #[derive(Tia)] // 2. derive #[tia(rg)] // 3. tia directives struct MyStruct { foo: i32, bar: String } fn main() { let mys = MyStruct{ foo: 123, bar: "Hello".into() }; let foo = mys.get_foo(); // <-- 4. !! generated by tia automatically !! let bar = mys.get_bar(); // <-- 5. !! generated by tia automatically !! println!("foo={} bar={}", foo, bar ); }
このコードは MyStruct
に impl
を手書きしていません。しかし、ビルドでき、実行できます。エラーも生じません。 .get_foo()
や .get_bar()
アクセサー群は tia
が proc-macro により与えられたディレクティブに基づき全自動で生成してくれます。↑の例のようにもっとも単純に MyStruct
へ Getter や Setter を実装する効果はやや弱いのですが、それでも tia
の rsi
ディレクティブで Into
パターンをフィールドへ自動実装したり、便利なこともあるかもしれません。
tia
が作者に必要とされた理由は trait
に対応したフィールドを自動生成できるようにしなければ、XMLSchemaにゲームを楽しみ料理を楽しむ時間を奪われそうになった事に起因します。起因の部分の細かい話はまた別の機会として、 trait
に対応したアクセサーの自動生成は:
use tia::Tia; trait FooGettable<T>{ fn get_foo(&self) -> T; } trait Fruit{ fn get_bar(&self) -> &String; } trait Sushi{ fn tuna(&self) -> u8; fn avocado(&mut self, v: u8); } #[derive(Tia, Debug, Default)] // derive struct MyStruct { #[tia(s, "FooGettable<i32>", g)] foo: i32, #[tia("Fruit",rg,"",rsi)] bar: String, #[tia("Sushi",g*="tuna",s*="avocado")] // <- `g` and `s`: Sushi trait baz: u8 } fn main() { let mut mys = MyStruct::default(); mys.set_foo(123); mys.set_bar("meow"); let foo_gettable = &mys as &dyn FooGettable<i32>; let fruit = &mys as &dyn Fruit; println!("{}, {}", foo_gettable.get_foo(), fruit.get_bar() ); let sushi = &mut mys as &mut dyn Sushi; sushi.avocado(32); println!("{}", sushi.tuna()); }
こんな具合です。たくさんの trait たちに手書きで対応しなければならないときには便利そうです。
より詳しくは README へ書いたので興味がわいたらどうぞ❤
にゃーん
tia
を作るため proc-macro
や [syn][] をトリアエズ扱える程度に修行できました。ソースを見るとバレそうですが、はじめはもうちょっと簡単にほいほい作れちゃうのでは…と思い大雑把でわりと簡単な設計に実験などしつつごちゃごちゃ実装になってしまいました。需要があればリファクタリングというか、一部再設計もして保守したいと思います。
ちなみに…Rustでは enum
, union
, Box
, などで多相性(=ぽりもーふぃずむ)の要件をばばばっとやっつけてしまう設計の方が言語仕様的にも楽ですが、世の中のすべてが Rust を前提に設計されているわけではなく、オブジェクト指向パラダイムを主とした言語、例えば C#, Java, C++ など向けを当初想定して作られた、 UML で設計された、そんなような何かや複雑な XMLSchema を前提とした何かを大量に扱うための構造を実装したい場合、素直にオブジェクト指向パラダイム風に trait
を interface 的に Rust の設計に落とし込み、 dyn
した trait
型のオブジェクトを介してアクセサーで多相性をどうにかする、という事が必要な場合もあるかもしれません。
XMLSchema はそもそも人が手書きでパーサーやプログラミング言語の中での構造、あるいはUMLでいう汎化(≈継承)や包含の実装を全自動化できるハッピーな仕組み…なのですが、意外とまともな言語バインディングって少ないのです。UMLにせよおおよそ実質的にJava界隈を強く意識して作られているので仕方ないのだけど。Rust も XMLSchema の言語バインディングがまだまだ未成熟でごく簡単な単一ファイルの XMLSchema ならかろうじて対応できる crate もあるのですが…実質実用はまだまだ無理そうです。一般的に XMLSchema さんは Java のソースファイルのように大量の別れたファイル群で相互に複雑にリンクを持っていて、… まあ…そのはなしはまたこんど別の記事で…。
おまけメモ
tia
に似た機能の crate には yangby-cryptape/rust-property というのがあります。trait
に対応していないのと、たぶん方向性として主な用途、目標が違いそうなのでばばばっと…夜な夜なtia
を作りました。- XMLSchema の lib crate の希望は media-io/xml-schema というのがあります。 flatten 未対応だったりまだまだ先は長いですが何年後かには実用性を獲得できるかもしれません。
- bin な binding 生成器で estk/xmlschemer というのも見つけましたが、こちらはビルドから困難な状態で放棄されているので触るのはしんどそうです。
Rust の impl で pub る fn の第1引数の定義の方法と効果のメモ
主題
Rust の impl な fn の第1引数、つまり:
struct S { } impl S { fn f(❤ここ❤) { } }
❤ここ❤
の部分の書き方と、書き方に応じてどのような効果、意図として扱われうるかを整理します。
fn f()
fn f(self)
fn f(mut self)
fn f(&self)
fn f(&mut self)
すべて異なる効果を持ちえます。だいじなこと、同じ効果になるパターンはありません。同じ効果に派生できる組み合わせならあります。
1. fn f()
一番簡単なやつ。呼び出しは S::f()
で行う。関数呼び出しのフルネームに struct のシンボル名が付く。実質的にフリー関数と同じ。
このパターンを作る場合は、
fn new() -> Self { }
とかfn my_awesome_construct() -> Self { ... }
のような構築パターンfn holy_sit() -> i64 { 42 }
とかfn my_void() { }
のような「それ、フリー関数でよいのでは」的なその他のパターン
の何れか。前者は Self
を使い、呼び出し時にユーザーが型を強く意識し、その型または少なくとも関連を持つ型が構築される意図の疎通を図りやすいです。
後者の self
(object) も Self
(type) も使わない聖なるUNKOを産み落としたり、 self
も Self
も使わない謎のヴォイド ()
を生じる闇のメンバー関数はプログラミングソースコードの巨大な迷宮を作るのが目的(そういうゲームもあるかもしれません、遊びなら楽しそうですね🤣)でなければ素直にimpl
の外でフリー関数として適切な mod
へ放り込むのが妥当かもしれません。
2. fn f(self)
struct のオブジェクト自身を基準とした操作を行いたい場合、意図で使います。self
は参照でもなんでもないので self
でなければならない強い効果やその意図を醸し出します。
struct S { my_something: String } impl S { fn get_my_something(self) -> Self { self.my_something } }
さて、この実装は get_
プリフィックスの付いたアクセサー風のメンバー関数から関数の仕様者に伝わる意図に対して期待通りの動作をするでしょうか?もちろん、実装詳細を確認して使えば何でもワカルという話はそのとおりですが、未知の機械にレバーが付いていれば人はレバーを上げ下げしたりすると何か機械を操作できるハズ、という意図はデザイン=設計によって伝わるものです。伝わってくれるのか、伝わってしまうのかはさておき。
fn main() { let s = S{ my_something: "サクレ・レモンは最高に美味しい氷菓です🍋".to_string() }; let awesome_message = s.get_my_something(); ptinrln!( "おめでとうございます。あなたは「{}」と暗示を受け始めました。", awesome_message ); }
この実装「ならば」意図通りの動作と「同じ結果」(=効果)を生じているかもしれません。
本当に self
の実装は呼び出しを記述するユーザーが get_
アクセサーの見た目から意図した効果を及ぼす実装でしょうか?
fn main() { let s = S{ my_something: "サクレ・レモンは最高に美味しい氷菓です🍋".to_string() }; let awesome_message = s.get_my_something(); ptinrln!( "おめでとうございます。あなたは「{}」と暗示を受け始めました。", awesome_message ); let more_message = s.get_my_something(); println!( "だいじな事なのでもう一度お伝えします: 『{}』!!", more_message ); }
はい、死にました。
^ value moved here
とか
^ value used here after move
とか言われます。訓練された Rust 技師なら「あらあらうふふ」程度の事です。初心者には手荒い洗礼に感じられるかもしれません。
S
のオブジェクト s
のフィールド my_something
の move は本当にユーザーがライブラリーに意図した実装でしょうか?あるいは、 move を意図する関数名として現代の高級言語の多くでは shallow で light な雰囲気の漂う get_
は妥当性の高い命名だったでしょうか?おそらく何れもそうではないでしょう。
本当に、何らかの効率や要求に対応する手段として s
のメンバーを move して取り出せるようにライブラリーを設計する場合、
/// ⚠ この関数は MOVE 効果を生じます。意図しない場合は呼び出してはいけません。必要に応じて参照バージョンまたは複製バージョンのアクセサーの使用を検討して下さい。 fn __danger__move_my_something(self) -> { self.my_awesome }
これくらいうるさく意図が伝わりやすくしてもよいかもしれません。__danger__
まで付けるのはやり過ぎ感もありますが、意図が伝わらないよりは合理性も高いかもしれません。ちなみに self
は mut
修飾されていないので setter 的な実装は次項の mut self
のパターンになります。
3. fn f(mut self)
mut
修飾した mut self
のパターンでは setter 的な事に使うような気がしたでしょうか?ふつうは使いません。流れで修飾の少ない順にメモを残しているだけです💀 もちろん、 setter を書けないわけではありません:
fn set_my_something( self, new_value: String ) { self.my_message = new_value; }
↑こうすると、 my_new_message
は s.my_awesome
へ MOVE されます。 s.my_something
が "サクレ・レモン!サクレ・レモン!サクレ・レモン!"
を保持する String
のメモリーに望まれる、"最終的に予定された運命" のような場合は少なくないように思います。 struct 等へ値を入れたら、入れる元のスコープにオリジナルが残ってくれる必要がない、そんな場合。↑の set_my_something
は翻訳も通ります。では何が問題でここまで setter として使う事は通常は無い空気をメモに漂わせているかというと、
let my_new_message = "サクレ・レモン!サクレ・レモン!サクレ・レモン!".to_string(); s.set_my_something( my_new_message ); // ↑ここまでなら一見、意図された設計、ユーザーにもそれほど不便でもなくMOVEになってくれる効率よい方法、そう思えるかもしれません。 println!( "👉{}👈", s.get_my_something() ); // ↑💀
↑は↓のように死にます。
error[E0382]: use of moved value:
s
つまり、set_my_something
の関数スコープへ MOVE された self
は関数内で self.my_something = new_value;
したあと、この世界には痕跡(s
というかつてself
が在ったところ…)だけを残して実体は完全に消失しています。そういう実装です。このパターンを一般的な setter の用途で使いたい場合は、
fn set_my_something( mut self, new_value: String ) -> Self { self.my_something = new_value; self }
↑こんな具合で Self
型の return を関数シグニチャーへ設定し、関数本体で最後に self
(または return self;
) し、 関数スコープへ MOVE された self
を関数スコープの呼び出し元へ return する設計に変更し、呼び出し元で、
let s = s.set_my_something( my_new_message );
↑こんな具合に s.set_my_something
で関数スコープへ飛んで行ってしまう s
を関数スコープの return から回収して新たな let s
として再定義しています。こうすると mut self
パターンの setter でも 呼び出し元のスコープに対しては実体が失われず旅行の末に帰ってきてくれます。"それ"はそこに実際に帰ってきて"在る"のでその後に println!
など何かしらにも使えます。
ふつーの setter ではこんなの面倒なだけなので、こんな設計にはしません💀 このパターンでは呼び出し元の世界の s
が mut
でなくてもナカミを実質的に変更できるという利点はありますが、そんなモナド感のある実装は一般的な setter ではおおよそ使われません。(note: 一般的な setter は次の次の &mut self
パターンを使います。)
4. fn f(&self)
関数スコープに参照 &
で self
を持ってきます。 &self
なのでオリジナルは呼び出し元に在るままです。呼び出し元の世界には通常は効果を及ぼさずに &self
で副作用なくできる事をする場合に使います。一般的に多くの pub なメンバー関数はこのパターンの妥当性の高いかもしれません。MOVEしない getter もこれが最適です。
fn get_my_something(&self) -> &String { &self.my_something }
すべてがおおよそユーザーの期待しそうな挙動を示すようになりました。🙌 でもまだ setter は面倒さが残っています。
5. fn f(&mut self)
fn set_my_something( mut self, new_value: String ) -> Self { self.my_something = new_value; self }
関数スコープを &mut self
で受ける実装にすると、一般的には呼び出し元のオリジナルの"存在"までは効果を及ぼさず、 S
がフィールドとして持つ何かしらの変更を行う実装と意図を著せます。Rustでは impl
な関数スコープでもフリー関数でも借用(Borrow)と参照(Reference)は修飾による効果はすべて同じという事を理解すると Rust の impl
で pub
る fn
、つまりたいていアクセサーなどの一般的な実装で第1引数や続くパラメーターの修飾、return の修飾、関数本体の実装に困らなくなるかもしれません。
struct S { my_something: String } impl S { //fn get_my_something( self ) -> String { self.my_something } fn get_my_something( &self ) -> &String { &self.my_something } //fn set_my_something( mut self, new_value: String ) { self.my_something = new_value; } //fn set_my_something( mut self, new_value: String ) -> Self { self.my_something = new_value; self } fn set_my_something( &mut self, new_value: String ) { self.my_something = new_value } } fn main() { let mut s = S{ my_something: "サクレ・レモンは最高に美味しい氷菓です🍋".to_string() }; let awesome_message = s.get_my_something(); println!( "おめでとうございます。あなたは「{}」と暗示を受け始めました。", awesome_message ); let more_message = s.get_my_something(); println!( "だいじな事なのでもう一度お伝えします: 『{}』!!", more_message ); let my_new_message = "サクレ・レモン!サクレ・レモン!サクレ・レモン!".to_string(); s.set_my_something( my_new_message ); //let s = s.set_my_something( my_new_message ); println!( "👉{}👈", s.get_my_something() ); }
つまり、だいじなことは?
Rust の impl で pub る fn の第1引数も他の引数も修飾と借用と参照の考え方、効果はフリー関数を設計、実装する場合と同じです。迷ったら、Rustでどう実装するのかとかそういった事ではなく、何をしたいのか設計から入力と出力の修飾を考えるだけでよい、という事です。アタリマエではあるけれど、それがアタリマエって大事だと思うのでメモを書き残しました。
参考
Adobe Acrobat Reader DC 20.009.20074 のホイールによるページの上下方向のスクロール量が体感0.1行単位くらいの遅さでつらい時に思い出したいメモ
default 状態でホイールのスクロール量が体感0.1行くらいでつらすぎる場合にスクロール量を数行単位程度へ改善する設定手順:
- Menu bar: Edit -> Preferences (CTRL+K) -> Page Display -> Default Layout and Zoom -> Page Layout
Single Page Continuous
へ変更 ( default はたぶんAutomatic
)
- Menu bar: View -> Page Display -> Enable Scrolling
- 設定しないとページが上下に繋がらずしんどいので一緒に設定するとGOOD 🙆♀️
参考
逆引き風 roxmltree の基礎的な使い方のメモ; rust のたぶん今の所いちばん速くて安全な XML ぱーさー
- https://crates.io/crates/roxmltree/
- https://github.com/RazrFalcon/roxmltree/
- https://docs.rs/roxmltree/0.13.0/roxmltree/
note: このメモは roxmltree-0.13.0 の時代に書きました。
はじめに知っておくとよいこと
- 与えられた XML ≈
Document
をNode
の木構造に分解してNode
を基準に操作するための crate =roxmltree
Node
は XML の Element (≈タグ)とは限りません。Node
が出てきたらカモシレナイ match/if が必要なパターンがありますancestor
は上位(先祖)、sbling
は同位(姉妹)、descendant
は下位(子孫)parent
は直近の上位(親)、child
は直近の下位(子)
XMLを開く
let document: roxmltree::Document = roxmltree::Document::parse( "<root/>" );
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Document.html#method.parse
- note: ここから先で
document
が出てきたらこの続きと思って下さい。
XMLのルート要素の Node
を取得する
let node: roxmltree::Node = d.root_element();
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Document.html#method.root_element
- note:
.root()
はドキュメントそのもの的なNode
が帰ってきます。 - note: ここから先で
node
が出てきたらroxmltree::Node
な変数だと思って下さい。
Node
が Element なのか何なのか確認する
match node.node_type() { roxmltree::NodeType::Element => println!("This Node is an Element."), _ => println!("This Node is NOT an Element.") }
または
let is_element: bool = node.is_element(); let is_comment: bool = node.is_comment();
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.node_type
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.is_root
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.is_element
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.is_pi
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.is_comment
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.is_text
Element な Node
のタグ名を確認または取得する
// <xxx> か判定 let is_xxx_tag: bool = node.has_tag_name("xxx"); // タグ名を取得 let tag_name: roxmltree::ExpandedName = node.tag_name(); // タグ名のローカル名部分を取得; <abbrns:localpart> の localpart の部分 let tag_local_name: &str = tag_name.name(); // タグ名の名前空間部分を取得; <abbrns:localpart> の abbrns の部分の xmlns: 定義の uri 値 let tag_namespace: &str = tag_name.namespace();
- note:
ExpandedName
の.namespace()
は<abbrns:localname>
に対してabbrns
ではなくxmlns:abbrns="http://example.com/"
を解決したhttp://example.com/
を取得します。 - https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.has_tag_name
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.tag_name
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.ExpandedName.html
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.ExpandedName.html#method.name
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.ExpandedName.html#method.namespace
XML Namespace 群を確認する
for ns: &roxmltree::Namespace in document.root_element().namespace() { println!("name={:?} uri={}", ns.name(), ns.uri()); }
- Note: 任意の
Node
で実装できますが、実際に XML でxmlns:
群が定義されている Element なNode
じゃないと何も出てきません。 - https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.namespaces
Node
の Attribute 群を取得する
for attribute: &roxmltree::Attribute in node.attributes() { println!("name={} value={}", attribute.name(), attribute.value()); }
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.attributes
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Attribute.html
Node
の直近の下位(子) Element な Node
群を取得、または同位(姉妹)の Node
群を取得など
// 手法 A; .children から Element を .filter let child_elements = document.root_element().children().filter(|n|n.node_type()==roxmltree::NodeType::Element); for node: roxmltree::Node in child_elements { println!("name={} text={:?}", node.tag_name().name(), node.text() ); }
// 手法 B; .first_element_child から Option を while して .next_sibling_element let mut node_maybe = document.root_element().first_element_child(); while let Some(node) = node_maybe { println!("name={} text={:?}", node.tag_name().name(), node.text() ); node_maybe = node.next_sibling_element(); }
- 残念ながら
Node
に.element_children
的な関数はいまのところ無いので.children
から.filter
します。 - https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.children
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.first_element_child
Node
の上位(先祖)群、直近の上位(親)、下位(子孫)群
// 上位群 let ancentors: AxisIter = node.ancestors(); for node: roxmltree::Node in ancentors { println!("ancentor name={} text={:?}", node.tag_name().name(), node.text() ); } // 直近の上位 let parent_maybe: Option<roxmltree::Node> = node.parent_element(); let node: roxmltree::Node = parent_maybe.unwrap(); println!("parent name={} text={:?}", node.tag_name().name(), node.text() ); // 下位群 let descendants: Descendants = node.descendants(); for node: roxmltree::Node in descendants { println!("descendant name={} text={:?}", node.tag_name().name(), node.text() ); }
AxisIter
≈ [.parent()
,.parent().parent()
->.parent().parent().parent()
, .. ] 的に列挙してくれるやつDescendants
≈ 再帰的に.children
で下位Node
を根こそぎして.flatten
したような、下位Node
すべてがごそっと取り出せるやつ- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.ancestors
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.parent_element
- https://docs.rs/roxmltree/0.13.0/roxmltree/struct.Node.html#method.descendants
小さな XML のパース速度で比べる rust の XML crate たち; roxmltree vs. minidom ≈ quick-xml vs. sxd-document vs. sxd-xpath vs. amxml
結果
test benches::amxml_root ... bench: 40,783 ns/iter (+/- 6,365) test benches::amxml_sec ... bench: 28,487 ns/iter (+/- 1,295) test benches::minidom_root ... bench: 1,942 ns/iter (+/- 73) test benches::minidom_sec ... bench: 2,127 ns/iter (+/- 1,183) test benches::roxmltree_root ... bench: 999 ns/iter (+/- 36) test benches::roxmltree_sec ... bench: 1,171 ns/iter (+/- 205) test benches::sxd_document_root ... bench: 3,453 ns/iter (+/- 123) test benches::sxd_document_sec ... bench: 3,566 ns/iter (+/- 136) test benches::sxd_xpath_root ... bench: 13,206 ns/iter (+/- 2,809) test benches::sxd_xpath_sec ... bench: 16,499 ns/iter (+/- 1,370)
- 速度: roxmltree >> minidom >> sxd-document >>>> sxd-xpath >>>>>>>> amxml 👉 roxmltree はやい
- sxd-document vs. sxd-xpath 👉 同じ操作でも XPath を噛むと4倍処理時間が長くなってしまう
そーす
Cargo.toml/dependencies:
[dependencies] roxmltree = "0.13.0" sxd-xpath = "0.4.2" sxd-document = "0.3.2" amxml = "0.5.3" quick-xml = "0.18.1"
benches/bench.rs:
#![feature(test)] extern crate test; include!("../tests/test.rs"); #[cfg(test)] mod benches { use test::Bencher; #[bench] fn minidom_root(bencher: &mut Bencher) { bencher.iter(|| crate::tests::minidom_root()); } #[bench] fn minidom_sec(bencher: &mut Bencher) { bencher.iter(|| crate::tests::minidom_sec()); } #[bench] fn roxmltree_root(bencher: &mut Bencher) { bencher.iter(|| crate::tests::roxmltree_root()); } #[bench] fn roxmltree_sec(bencher: &mut Bencher) { bencher.iter(|| crate::tests::roxmltree_sec()); } #[bench] fn amxml_root(bencher: &mut Bencher) { bencher.iter(|| crate::tests::amxml_root()); } #[bench] fn amxml_sec(bencher: &mut Bencher) { bencher.iter(|| crate::tests::amxml_sec()); } #[bench] fn sxd_document_root(bencher: &mut Bencher) { bencher.iter(|| crate::tests::sxd_document_root()); } #[bench] fn sxd_document_sec(bencher: &mut Bencher) { bencher.iter(|| crate::tests::sxd_document_sec()); } #[bench] fn sxd_xpath_root(bencher: &mut Bencher) { bencher.iter(|| crate::tests::sxd_xpath_root()); } #[bench] fn sxd_xpath_sec(bencher: &mut Bencher) { bencher.iter(|| crate::tests::sxd_xpath_sec()); } }
tests/test.rs:
#[cfg(test)] mod tests { const INPUT: &str = "<abc><x/><y/><z/></abc>"; #[test] pub fn minidom_root() { let root: minidom::Element = INPUT.parse().unwrap(); let root_name = root.name(); assert_eq!(root_name, "abc"); } #[test] pub fn minidom_sec() { let root: minidom::Element = INPUT.parse().unwrap(); let sec_names: Vec<&str> = root.children().into_iter().map(|element| element.name()).collect(); assert_eq!(sec_names, ["x", "y", "z"]) } #[test] pub fn roxmltree_root() { let document = roxmltree::Document::parse(INPUT).unwrap(); let root_name = document.root_element().tag_name().name(); assert_eq!(root_name, "abc"); } #[test] pub fn roxmltree_sec() { let document = roxmltree::Document::parse(INPUT).unwrap(); let sec_nodes = document.root_element().children(); let sec_names: Vec<&str> = sec_nodes.into_iter().map(|node| node.tag_name().name()).collect(); assert_eq!(sec_names, ["x", "y", "z"]) } #[test] pub fn amxml_root() { let document = amxml::dom::new_document(INPUT).unwrap(); // let xpath_result = document.eval_xpath("/*"); // let root_name = xpath_result.unwrap().get_item(0).as_nodeptr().unwrap().name(); let xpath_result = document.eval_xpath("/*/name()"); let root_name = xpath_result.unwrap().get_item(0).to_string(); assert_eq!(root_name, "\"abc\""); } #[test] pub fn amxml_sec() { let mut sec_names: Vec<String> = vec![]; let document = amxml::dom::new_document(INPUT).unwrap(); document .each_node("/*/*", |node| { sec_names.push(node.name().clone()); }) .unwrap(); assert_eq!(sec_names, ["x", "y", "z"]) } #[test] pub fn sxd_document_root() { let package = sxd_document::parser::parse(INPUT).unwrap(); let document = package.as_document(); let root_element = document.root().children().first().unwrap().element().unwrap(); let root_name = root_element.name().local_part(); assert_eq!(root_name, "abc"); } #[test] pub fn sxd_document_sec() { let package = sxd_document::parser::parse(INPUT).unwrap(); let document = package.as_document(); let root_element = document.root().children().first().unwrap().element().unwrap(); let sec_elements = root_element.children(); let sec_names: Vec<&str> = sec_elements .iter() .map(|child_of_element| child_of_element.element().unwrap().name().local_part()) .collect(); assert_eq!(sec_names, ["x", "y", "z"]) } #[test] pub fn sxd_xpath_root() { let package = sxd_document::parser::parse(INPUT).unwrap(); let document = package.as_document(); let xpath_result = sxd_xpath::evaluate_xpath(&document, "/*"); let value = xpath_result.unwrap(); let root_name = match value { sxd_xpath::Value::Nodeset(nodes) => { let node = nodes.iter().next().unwrap(); let qname = node.expanded_name().unwrap(); qname.local_part() }, _ => panic!("Failed: `Value` -> `Nodeset`") }; assert_eq!(root_name, "abc"); } #[test] pub fn sxd_xpath_sec() { let package = sxd_document::parser::parse(INPUT).unwrap(); let document = package.as_document(); let xpath_result = sxd_xpath::evaluate_xpath(&document, "/*/*"); let value = xpath_result.unwrap(); let sec_names: Vec<String> = match value { sxd_xpath::Value::Nodeset(nodes) => { nodes .iter() .map(|node| node.expanded_name().unwrap().local_part().to_string()) .collect() }, _ => panic!("Failed: `Value` -> `Nodeset`") }; let expected = ["x", "y", "z"]; assert_eq!(sec_names.len(), expected.len()); for element in expected.iter() { assert!(sec_names.contains(&element.to_string())); } } }
rust で XPath できる crate たちのメモ; (1) amxml, (2) sxd-xpath
XPath 使いたい需要に対応できる XML パーサーを crates.io で探すと有用そうな crate が2つ見つかりました。簡単な XPath で使い勝手を確認したメモです。
- amxml
- sxd-xpath ( + sxd-document )
1. amxml
cargo add amxml
#[cfg(test)] mod tests { /// 共通: 入力XML const INPUT: &str = "<abc><x/><y/><z/></abc>"; /// ルートノードの名前をXPathで確認 #[test] fn amxml_root() { let document = amxml::dom::new_document(INPUT).unwrap(); let xpath_result = document.eval_xpath("/*"); let root_name = xpath_result.unwrap().get_item(0).as_nodeptr().unwrap().name(); assert_eq!(root_name, "abc"); } /// 第2階層(=ルート直下)ノード群の名前をXPathで確認 #[test] fn amxml_sec() { let mut sec_names: Vec<String> = vec![]; let document = amxml::dom::new_document(INPUT).unwrap(); document .each_node("/*/*", |node| { sec_names.push(node.name().clone()); }) .unwrap(); assert_eq!(sec_names, ["x", "y", "z"]); } }
- 👍 嬉しいところ:
- ⚠ 注意が必要なところ:
// 参考おまけ: note(†) let xpath_result = document.eval_xpath("/*/name()"); // XPath では /name() で要素名の文字列値を取れる let root_name = xpath_result.unwrap().get_item(0).to_string(); // XPathの結果がノードではなく文字列値なので Item::to_string を使える assert_eq!(root_name, "\"abc\""); // 文字列リテラル表現で取れてくるので比較など後の処理で"文字列リテラルな文字列"を気にする事になります
2. sxd-xpath ( + sxd-document )
cargo add sxd-xpath sxd-document
#[cfg(test)] mod tests { /// 共通: 入力XML const INPUT: &str = "<abc><x/><y/><z/></abc>"; /// ルートノードの名前をXPathで確認 #[test] fn sxd_root() { let package = sxd_document::parser::parse(INPUT).unwrap(); let document = package.as_document(); let xpath_result = sxd_xpath::evaluate_xpath(&document, "/*"); let value = xpath_result.unwrap(); let root_name = match value { sxd_xpath::Value::Nodeset(nodes) => { let node = nodes.iter().next().unwrap(); let qname = node.expanded_name().unwrap(); qname.local_part() }, _ => panic!("Failed: `Value` -> `Nodeset`") }; assert_eq!(root_name, "abc"); } /// 第2階層(=ルート直下)ノード群の名前をXPathで確認 #[test] fn sxd_sec() { let package = sxd_document::parser::parse(INPUT).unwrap(); let document = package.as_document(); let xpath_result = sxd_xpath::evaluate_xpath(&document, "/*/*"); let value = xpath_result.unwrap(); let sec_names: Vec<String> = match value { sxd_xpath::Value::Nodeset(nodes) => { nodes .iter() .map(|node| node.expanded_name().unwrap().local_part().to_string()) .collect() }, _ => panic!("Failed: `Value` -> `Nodeset`") }; // ☢ 要注意: sxd-xpath の nodes の列挙順序は実行ごとに変わります。 let expected = ["x", "y", "z"]; assert_eq!(sec_names.len(), expected.len()); for element in expected.iter() { assert!(sec_names.contains(&element.to_string())); } } }
- 👍 嬉しいところ:
- 更新頻度、コミッター数はぼちぼち
- ❓ 嬉しいかもしれないけれど判断の難しいところ:
- めんどくさいので触れませんでしたが
Context
を使うとより変態的な実装もできそうです
- めんどくさいので触れませんでしたが
- ⚠ 注意が必要なところ:
どちらを使うのが嬉しそうか?
- XPath-3.1 対応が欲しい 👉 amxml
- Author / Comitter が活きていて欲しい 👉 sxd-xpath
- ユーザーコードを簡潔に済ませたい 👉 amxml
- XPath 以外の XML の DOM アクセスや read だけでなく write も欲しい 👉 sxd-xpath > amxml (基本的なDOM操作はどちらでもできます)
追記: どちらが速そう?; 2020-08-05
↓は
test benches::amxml_root ... bench: 37,902 ns/iter (+/- 4,120) test benches::amxml_sec ... bench: 25,728 ns/iter (+/- 1,255) test benches::sxd_root ... bench: 13,392 ns/iter (+/- 2,052) test benches::sxd_sec ... bench: 16,544 ns/iter (+/- 2,413)