何とは言わない天然水飲みたさ

Rust (1.19.0) でオレオレ unsized type を定義する

TL;DR: 結論

概要

Rust 用の ActivityPub ライブラリとサーバを実装しようとしているのだが、 オブジェクトの ID を IRI (RFC 3987, まあ URI や URL みたいなもの) で表現することになっている。 これは Unicode の文字列のサブセットであるから、 IRI を表現する型は &strString 型をベースとして値の範囲を制限した 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) 型」である。 f32f64 に使える演算子はだいたいそのまま用意されており(つまり使い勝手は既存の 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()) であるから、これは望んでいたものではない。

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Iri<'a>(&'a str);

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IriBuf(String);

impl<'a> ::std::borrow::Borrow<Iri<'a>> for IriBuf {
    fn borrow(&self) -> &Iri<'a> {
        Iri(self.0.to_str())  // <- mismatched types (expected `&Iri<'a>`, found `Iri<'_>`)
    }
}
まあ型が合わないよね

これが std::path::Pathstd::path::PathBuf のようにいい感じに使えるようになってほしい場合、必要なのは pub sturct Iri(str); のような定義だ。

unsized な型を初期化する

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Iri(str);

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IriBuf(String);
ここまでは良し

さて次の問題は、自身の内部に str のような型を持つとき、これをどう初期化すべきかである。

impl Iri {
    pub fn from_str(s: &str) -> Option<&Self> {
        Some(&Iri(*s))
    }
}

error[E0161]: cannot move a value of type Iri: the size of Iri cannot be statically determined
 --> src/main.rs:6:15
  |
6 |         Some(&Iri(*s))
  |               ^^^^^^^

error[E0161]: cannot move a value of type str: the size of str cannot be statically determined
 --> src/main.rs:6:19
  |
6 |         Some(&Iri(*s))
  |                   ^^

error[E0597]: borrowed value does not live long enough
 --> src/main.rs:6:15
  |
6 |         Some(&Iri(*s))
  |               ^^^^^^^ does not live long enough
7 |     }
  |     - temporary value only lives until here
  |
note: borrowed value must be valid for the anonymous lifetime #1 defined on the method body at 5:5...
 --> src/main.rs:5:5
  |
5 | /     pub fn from_str(s: &str) -> Option<&Self> {
6 | |         Some(&Iri(*s))
7 | |     }
  | |_____^

error[E0507]: cannot move out of borrowed content
 --> src/main.rs:6:19
  |
6 |         Some(&Iri(*s))
  |                   ^^ cannot move out of borrowed content

error: aborting due to previous error(s)
これは駄目 (playground)

上のような素直な(?)実装は、当然ながらコンパイルエラーとなる。 str の値はサイズが未知であるから move できないこと、また必要なのは &self と同じ lifetime を持つ &Iri であるにも関わらず一時変数の参照を返そうとしていること、この2つがエラーの原因である。

既存の実装

では既存の実装(特に std::path::Path) ではどうなっているのか。 その謎を明らかにすべく、我々は libstd の奥地へ向かった。

pub struct Slice {
    pub inner: [u8]
}
std::sys::unix::os_str::Slice の定義
impl Slice {
    fn from_u8_slice(s: &[u8]) -> &Slice {
        unsafe { mem::transmute(s) }
    }

    pub fn from_str(s: &str) -> &Slice {
        Slice::from_u8_slice(s.as_bytes())
    }
std::sys::unix::os_str::Slice の実装
std::sys::unix::os_str::Slice

ついでに、リプで教えていただいた 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

上に挙げた例から、本来記述すべきだった処理は、「 Iristr の値を突っ込む」ことではなく、「 Iristr を同一のものと見做し、 &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 {
        ::std::mem::transmute(s)
    }

    /// 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()
    }
}
やったね! (playground)

あとはお好みで std::ops::Deref やら std::convert::AsRef やら諸々を実装すればおk。 まあその諸々が結構多くて手間なんだけど。

追記 (2017-08-29): 参考

くわしい。