Rust (1.19.0) でオレオレ unsized type を定義する
TL;DR: 結論
概要
Rust 用の ActivityPub ライブラリとサーバを実装しようとしているのだが、 オブジェクトの ID を IRI (RFC 3987, まあ URI や URL みたいなもの) で表現することになっている。
これは Unicode の文字列のサブセットであるから、 IRI を表現する型は &str
や String
型をベースとして値の範囲を制限した strong typedef により実装できそうである。
しかし、 str
は unsized な型であるから、安直に実装しようとしてもうまくいかない。
そこで、似たような型である std::path::Path
等を参考にしつつ方法を調べた。
C/C++ で言うところの VLAIS みたいなのは未対応っぽいので、 unsized type とはいってもそういう話ではない。 (詳細は以下リンク)
strong typedef とは
strong typedef とは、内部表現としてある型 (A
とする)を用いつつ、暗黙の型変換を禁止したり、明示的な型変換であっても値が制約を満たすかチェックすることで、別の型として扱わせるというテクニックである。
これを活かすと、コンストラクタ等での構築時にチェックをかけることで、値のとれる範囲を A
型よりも小さくしたり、演算子を独自に定義することで A
と挙動を変化させたりといったことが可能になる。
たとえば「偶数しか値を持たない(持てない)ような整数 (i32
) 型」を定義すると、このようになる。
(この例では演算子の定義をサボったが、もし整数と同様に透過的に使えるようにするなら std::ops::Add
をはじめとする数々の演算子を用意することになる。めっちゃめんどい。)
より実用的でちゃんとした例としては、 ordered-float crate の NotNaN
型などがある。
これは「 NaN をとらない float (f32
, f64
) 型」である。
f32
や f64
に使える演算子はだいたいそのまま用意されており(つまり使い勝手は既存の float に劣らない)、更に追加で std::cmp::Ord
等も実装されている嬉しい型だ。
unsized な型
サイズが固定となるような型であれば、前述のようにして strong typedef が可能である。
では、本題の str
を strong typedef してオレオレ型を用意することは可能なのか。
結論から言うと可能なのだが、素直なやりかたにはならない。
何故 unsized でないといけないのか
そもそも何故 unsized にしたいのかという話である。
素直にやるなら pub struct Iri<'a>(&'a str);
と pub struct IriBuf(String);
のようにしてしまえば済む話で、実際これでも使い物にはなる。
しかし unsized な型を用意しておかないと、 Cow<Iri>
ができないのである。
std::borrow::Cow<'a, B>
型は、where B: 'a + ToOwned + ?Sized
を要求しているstd::borrow::ToOwned
trait は、type Owned: Borrow<Self>;
という associated type を持っているstd::borrow::Borrow<Borrowed>
trait は:where Borrowed: ?Sized
を要求しているfn borrow(&self) -> &Borrowed;
を持つ
この状況で pub struct IriBuf(String);
に対して Borrowed<Iri>
trait を実装しようとすると、 &Iri
を返さねばならないが、返したかったのは Iri(self.0.to_str())
であるから、これは望んでいたものではない。
これが std::path::Path
と std::path::PathBuf
のようにいい感じに使えるようになってほしい場合、必要なのは pub sturct Iri(str);
のような定義だ。
unsized な型を初期化する
さて次の問題は、自身の内部に str
のような型を持つとき、これをどう初期化すべきかである。
上のような素直な(?)実装は、当然ながらコンパイルエラーとなる。
str
の値はサイズが未知であるから move できないこと、また必要なのは &self
と同じ lifetime を持つ &Iri
であるにも関わらず一時変数の参照を返そうとしていること、この2つがエラーの原因である。
既存の実装
では既存の実装(特に std::path::Path
) ではどうなっているのか。
その謎を明らかにすべく、我々は libstd の奥地へ向かった。
ついでに、リプで教えていただいたrocket crate の http::uncased::UncasedStr
も見てみる。
#[derive(Debug)]
pub struct UncasedStr(str);
rocket::http::uncased::UncasedStr
の定義
impl UncasedStr {
/// Returns a reference to an `UncasedStr` from an `&str`.
///
/// # Example
///
/// ```rust
/// use rocket::http::uncased::UncasedStr;
///
/// let uncased_str = UncasedStr::new("Hello!");
/// assert_eq!(uncased_str, "hello!");
/// assert_eq!(uncased_str, "Hello!");
/// assert_eq!(uncased_str, "HeLLo!");
/// ```
#[inline(always)]
pub fn new(string: &str) -> &UncasedStr {
unsafe { &*(string as *const str as *const UncasedStr) }
}
rocket::http::uncased::UncasedStr
の実装
rocket::http::uncased::UncasedStr
上に挙げた例から、本来記述すべきだった処理は、「 Iri
に str
の値を突っ込む」ことではなく、「 Iri
と str
を同一のものと見做し、 &str
を &Iri
として再解釈する」ことであったことが読み取れる。
結論
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Iri(str);
impl Iri {
/// Converts a string slice to an IRI slice without checking that the string contains valid
/// IRI.
#[inline]
pub unsafe fn from_str_unchecked(s: &str) -> &Self {
&*(s as *const str as *const Self)
}
/// Converts a string slice to an IRI slice.
pub fn from_str(s: &str) -> Result<&Self, ParseError> {
run_iri_validation(s)?;
Ok(unsafe { Self::from_str_unchecked(s) })
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IriBuf(String);
impl IriBuf {
/// Coerces to an `Iri` slice.
#[inline]
pub fn as_iri(&self) -> &Iri {
unsafe { Iri::from_str_unchecked(self.0.as_str()) }
}
}
impl ::std::borrow::Borrow<Iri> for IriBuf {
fn borrow(&self) -> &Iri {
self.as_iri()
}
}
あとはお好みで std::ops::Deref
やら std::convert::AsRef
やら諸々を実装すればおk。
まあその諸々が結構多くて手間なんだけど。
追記 (): 参考
くわしい。
追記2 (): crate 作った
- opaque_typedef - Cargo: packages for Rust
- lo48576/opaque_typedef: Easy opaque typedef for Rust programming language
opaque typedef 用の crate を書いた。 新たな型の定義と、ありがちな trait の自動実装ができる。 unsized type も簡単に定義できるようにしてある。