C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

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 XS に依存せず S やそのお仲間の T 派生型たちが増えてもこの1回の定義だけで対応できます。 'static は…つらいですね。

設計を enumunion へ変更できる場合はしちゃったらいい状況もありますが、それはそれ、適材適所で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 TS など派生型の制約としては意味があるものの、共通関数を 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 や関数ポインターレベルの操作が何故かここで行われるはずもなく。

今回のメモのだいじなところ

  1. オブジェクト指向言語向けっぽい UML で設計したなコイツ…的なソフトウェアの設計を Rust で実装するはめになったら mopa crate を検討しましょう。
  2. Rust らしい enum ディスパッチは安全でうつくしーですが、OOPのクラスやインターフェースの継承構造を変換するには退屈なディスパッチを大量に書くことになるので適材適所で mopa りましょう。 Let's mopafy!

参考

Rust の tia crate で accessor とか interface 的な trait 群の impl を楽できるようになりました。のメモ

と、いうわけで 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 );
}

このコードは MyStructimpl を手書きしていません。しかし、ビルドでき、実行できます。エラーも生じません。 .get_foo().get_bar() アクセサー群は tia が proc-macro により与えられたディレクティブに基づき全自動で生成してくれます。↑の例のようにもっとも単純に MyStruct へ Getter や Setter を実装する効果はやや弱いのですが、それでも tiarsi ディレクティブで 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(❤ここ❤) { }
}

❤ここ❤ の部分の書き方と、書き方に応じてどのような効果、意図として扱われうるかを整理します。

  1. fn f()
  2. fn f(self)
  3. fn f(mut self)
  4. fn f(&self)
  5. 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を産み落としたり、 selfSelf も使わない謎のヴォイド () を生じる闇のメンバー関数はプログラミングソースコードの巨大な迷宮を作るのが目的(そういうゲームもあるかもしれません、遊びなら楽しそうですね🤣)でなければ素直に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 );
}

この実装「ならば」意図通りの動作と「同じ結果」(=効果)を生じているかもしれません。

f:id:USAGI-WRP:20200814170517p:plain

本当に 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 );
}

f:id:USAGI-WRP:20200814171651p:plain

はい、死にました。

^ 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__ まで付けるのはやり過ぎ感もありますが、意図が伝わらないよりは合理性も高いかもしれません。ちなみに selfmut 修飾されていないので 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_messages.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() );
 // ↑💀

↑は↓のように死にます。

f:id:USAGI-WRP:20200814184409p:plain

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! など何かしらにも使えます。

f:id:USAGI-WRP:20200814185650p:plain

ふつーの setter ではこんなの面倒なだけなので、こんな設計にはしません💀 このパターンでは呼び出し元の世界の smut でなくてもナカミを実質的に変更できるという利点はありますが、そんなモナド感のある実装は一般的な setter ではおおよそ使われません。(note: 一般的な setter は次の次の &mut self パターンを使います。)

4. fn f(&self)

関数スコープに参照 &self を持ってきます。 &self なのでオリジナルは呼び出し元に在るままです。呼び出し元の世界には通常は効果を及ぼさずに &self で副作用なくできる事をする場合に使います。一般的に多くの pub なメンバー関数はこのパターンの妥当性の高いかもしれません。MOVEしない getter もこれが最適です。

fn get_my_something(&self) -> &String { &self.my_something }

f:id:USAGI-WRP:20200814192231p:plain

すべてがおおよそユーザーの期待しそうな挙動を示すようになりました。🙌 でもまだ 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 の implpubfn 、つまりたいていアクセサーなどの一般的な実装で第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() );
}

f:id:USAGI-WRP:20200814193719p:plain

つまり、だいじなことは?

Rust の impl で pub る fn の第1引数も他の引数も修飾と借用と参照の考え方、効果はフリー関数を設計、実装する場合と同じです。迷ったら、Rustでどう実装するのかとかそういった事ではなく、何をしたいのか設計から入力と出力の修飾を考えるだけでよい、という事です。アタリマエではあるけれど、それがアタリマエって大事だと思うのでメモを書き残しました。

参考

Adobe Acrobat Reader DC 20.009.20074 のホイールによるページの上下方向のスクロール量が体感0.1行単位くらいの遅さでつらい時に思い出したいメモ

default 状態でホイールのスクロール量が体感0.1行くらいでつらすぎる場合にスクロール量を数行単位程度へ改善する設定手順:

  1. Menu bar: Edit -> Preferences (CTRL+K) -> Page Display -> Default Layout and Zoom -> Page Layout
    • Single Page Continuous へ変更 ( default はたぶん Automatic )
  2. Menu bar: View -> Page Display -> Enable Scrolling
    • 設定しないとページが上下に繋がらずしんどいので一緒に設定するとGOOD 🙆‍♀️

f:id:USAGI-WRP:20200808195118p:plain

f:id:USAGI-WRP:20200808195142p:plain

参考

逆引き風 roxmltree の基礎的な使い方のメモ; rust のたぶん今の所いちばん速くて安全な XML ぱーさー

note: このメモは roxmltree-0.13.0 の時代に書きました。

はじめに知っておくとよいこと

  • 与えられた XMLDocumentNode木構造に分解して Node を基準に操作するための crate = roxmltree
  • NodeXML の Element (≈タグ)とは限りません。 Node が出てきたらカモシレナイ match/if が必要なパターンがあります
    • NodeType::Root = Document (文書全体)
    • NodeType::Element = XML Element (要素≈タグ)
    • NodeType::PI = XML Processing Instruction (<?xml ?> とか <?hogehoge ?> 的なタグもどきのやつ)
    • NodeType::Comment = XML Comment (コメント)
    • NodeType::Text = XML Text (テキスト≈<tag>abc</tag>abcの部分)
  • ancestor は上位(先祖)、 sbling は同位(姉妹)、 descendant は下位(子孫)
  • parent は直近の上位(親)、 child は直近の下位(子)

XMLを開く

let document: roxmltree::Document = roxmltree::Document::parse( "<root/>" );

XMLのルート要素の Node を取得する

let node: roxmltree::Node = d.root_element();

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();

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();

XML Namespace 群を確認する

for ns: &roxmltree::Namespace in document.root_element().namespace()
{
  println!("name={:?} uri={}", ns.name(), ns.uri());
}

Node の Attribute 群を取得する

for attribute: &roxmltree::Attribute in node.attributes()
{
  println!("name={} value={}", attribute.name(), attribute.value());
}

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 の上位(先祖)群、直近の上位(親)、下位(子孫)群

// 上位群
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() );
}

小さな 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 はやい
    • note: 但し roxmltree には XPath を扱うオプションはありません
    • note: minidom ≈ quick-xml
    • note: amxml は遅いし放棄気味だけど XPath-3.1 対応でコードも書きやすいです
  • sxd-document vs. sxd-xpath 👉 同じ操作でも XPath を噛むと4倍処理時間が長くなってしまう
    • note: 代わりに XPath を使えるという事は特に実行時のパース定義もしやすくなるなど利点は得られます
    • note: sxd は若干DOM操作のコードに癖がある気がしますが DOM ごりごりにも XPath にも同じエンジンを共有して対応できる点は嬉しい事もありそうです

そーす

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 で使い勝手を確認したメモです。

  1. amxml
  2. 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"]);
 }
}
  • 👍 嬉しいところ:
    • XPath-3.1 対応
    • read only という事になっているけれど、基本的な DOM 操作は実装されている; append_child, insert_as_previous_sibling, insert_as_next_sibling, delete_child, replace_with, set_attribute, delete_attribute
    • XPath を渡して結果を取得する実装が簡単で扱いやすい印象。実装コスト低く、保守性も良好。
      • eval_xpath でも each_node でも NodePtr または事実上 NodePtr として扱える Item が可換が列挙される (≈XPathだけでなくXPath+Rustコード実装によるDOM的な処理を行いやすい)
  • ⚠ 注意が必要なところ:
    • 2018 年から更新が無く、ごく簡単な Issue/PR も放置されている
    • note(†): 文字列値を直接取り出すとダブルクォート囲いなリテラル付きの表現になります
      • 例えばルート要素の名前を XPath で直接 /*/name() のように取得すると "\"abc\"" が取れます
// 参考おまけ: 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-1.0 のみ対応; (XSLT-1.0 には将来的に対応予定と README に記載あり。 libxml, libxslt の Rust による完全な置き換えを目標にしている)
    • evaluate_xpath から Value を取り出して Nodeset を取り出して Node を列挙する実装を書くのががややめんどくさい(XPath の規格通りの "QName に LocalPart があって…" などの構造を辿りやすいものの…)
    • XPath で直接要素名を取る /*/name() のような、XPath の結果がノード群になりえない XPath は失敗するようです

どちらを使うのが嬉しそうか?

  • XPath-3.1 対応が欲しい 👉 amxml
  • Author / Comitter が活きていて欲しい 👉 sxd-xpath
  • ユーザーコードを簡潔に済ませたい 👉 amxml
  • XPath 以外の XML の DOM アクセスや read だけでなく write も欲しい 👉 sxd-xpath > amxml (基本的なDOM操作はどちらでもできます)

追記: どちらが速そう?; 2020-08-05

  • たぶん sxd-xpath の方がはやいです (少なくとも↑の test の程度の小さな XML のパースなら)

↓は

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)

参考