Rustのderiveはあまり頭がよくない

概要: Rustの derive はあまり頭がよくない。

derive がドジを踏む例

derive の問題は顕在化しやすく、RustコンパイラGitHub上でも何度も重複するissueが投げられていた。今は主に #26925 を中心に議論がまとまっているので、そちらを参照するとよいだろう。

不必要な境界を与える例

use std::rc::Rc;

// 本来不必要な X: Clone を要求する
#[derive(Clone)]
struct A<X>(Rc<X>);

struct B;

fn main() {
    A(Rc::new(B)).clone(); // Error
}
error: no method named `clone` found for type `A<B>` in the current scope
  --> <anon>:10:19
   |
10 |     A(Rc::new(B)).clone(); // Error
   |                   ^^^^^
   |
   = note: the method `clone` exists but the following trait bounds were not satisfied: `B : std::clone::Clone`
   = help: items from traits can only be used if the trait is implemented and in scope; the following trait defines an item `clone`, perhaps you need to implement it:
   = help: candidate #1: `std::clone::Clone`

error: aborting due to previous error

必要な境界が不足している例

struct A<X>(X);

impl<X: Copy> Clone for A<X> {
    fn clone(&self) -> Self { A(self.0) }
}

// 本来必要な X: Copy を要求しない
// Error
#[derive(Clone)]
struct B<X>(A<X>);

fn main() {}
rustc 1.17.0 (56124baa9 2017-04-24)
error[E0277]: the trait bound `X: std::marker::Copy` is not satisfied
  --> <anon>:10:13
   |
10 | struct B<X>(A<X>);
   |             ^^^^^ the trait `std::marker::Copy` is not implemented for `X`
   |
   = help: consider adding a `where X: std::marker::Copy` bound
   = note: required because of the requirements on the impl of `std::clone::Clone` for `A<X>`
   = note: required by `std::clone::Clone::clone`

error: aborting due to previous error

展開結果を見るにはnightlyコンパイラrustc --pretty expanded src/main.rs とやるとよい。

#![feature(prelude_import)]
#![no_std]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std as std;
use std::rc::Rc;

// 本来不必要な X: Clone を要求する
struct A<X>(Rc<X>);
#[automatically_derived]
#[allow(unused_qualifications)]
impl <X: ::std::clone::Clone> ::std::clone::Clone for A<X> {
    #[inline]
    fn clone(&self) -> A<X> {
        match *self {
            A(ref __self_0_0) =>
            A(::std::clone::Clone::clone(&(*__self_0_0))),
        }
    }
}

struct B;

fn main() {
    A(Rc::new(B)).clone(); // Error
}
#![feature(prelude_import)]
#![no_std]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std as std;
struct A<X>(X);

impl <X: Copy> Clone for A<X> {
    fn clone(&self) -> Self { A(self.0) }
}

// 本来必要な X: Copy を要求しない
// Error
struct B<X>(A<X>);
#[automatically_derived]
#[allow(unused_qualifications)]
impl <X: ::std::clone::Clone> ::std::clone::Clone for B<X> {
    #[inline]
    fn clone(&self) -> B<X> {
        match *self {
            B(ref __self_0_0) =>
            B(::std::clone::Clone::clone(&(*__self_0_0))),
        }
    }
}

fn main() { }

deriveはマクロの仲間

derive はマクロと同様の構文拡張に分類される。Rustコンパイラは原則として「構文解析→構文拡張の展開→名前解決」の順で進む(ただし、パスマクロの解決のために名前解決が先に実行されることはある)ため、 derive 実行時は名前解決がなされておらず、型の情報もほぼない状態である。

deriveの境界生成規則

derive は境界を決めるために、以下のようにして型を収集する。

  • 全ての型引数。
  • フィールドの型の一部分として登場する型のうち、型引数名で始まるパス型。ASTでの修飾パスのデータ構造の性質から、以下のいずれかの条件を満たせばマッチする。
    • X::Output::Item のように、型引数名で始まるパス。
    • <Frob>::X::Output のようなパス (QSelfが削除されて X::Output になってしまう) (※おそらくバグ)
    • <Frob as X::Output>::Bar::Baz のようなパス (QSelfが削除されて X::Output::Bar::Baz になってしまう) (※おそらくバグ)

おそらくバグだが、 <X>::Output のように書くとマッチしない。

例えば derive(Clone) の場合は、このようにして集めた各型に対して、 Clone を実装すべしという制約を課す。他の多くの derive も同じ機構を用いている。

なぜフィールド型に直接境界を課さないのか

上記のようなヒューリスティクスを用いず、フィールド型に直接 Clone を課せるようにするのが好ましいが、以下のような問題がある。

問題1: 可視性

現在のRustでは “private in public” 制約というものがある。詳しくは過去の記事を参照。 これにより以下のようなコードがエラーになる。

#[derive(Clone)]
struct A<X>(X);

pub struct B<X>(A<X>);

// Error
impl<X> Clone for B<X> where A<X>: Clone {
    fn clone(&self) -> Self {
        B(self.0.clone())
    }
}


fn main() {
    let x = B(A(0u32));
    x.clone();
}

B<X>は公開される型だがそのメンバ型 A<X> は非公開である。そのため X: Clone は課すことができても A<X>: Clone を課すことはできない。

問題2: 再帰的な制約

以下のように再帰的な型に対して derive(Clone) をすることを考える。現在のヒューリスティックスでは正しく動作するが、以下のようにフィールド型に直接 Clone を課すと、実体化のタイミングで再帰エラーになる。なお、実体化させない限りは問題が顕在化しない。

struct Node<T>(Option<Box<(Node<T>, T)>>);

impl<T> Clone for Node<T> where Option<Box<(Node<T>, T)>>: Clone {
    fn clone(&self) -> Self {
        Node(self.0.clone())
    }
}

fn main() {
    let x = Node(Some(Box::new((Node(None), 0u32))));
    x.clone(); // Error
}

問題3: 後方互換

トレイトは実装を増やしても減らしても互換性がない。そのため、今から derive(Clone) の挙動を変えると大きな破壊的変更となる可能性がある。

まとめ

Rustの derive は構文的に実装を生成するだけなので、推論されたトレイト境界が間違っている場合がある。これを正しく動作させるにはいくつかの困難があると予想される。

Rustのスレッドローカル変数について

Rustには2種類のスレッドローカル変数がある。

#[thread_local] 属性

#[thread_local] 属性は、指定した static アイテムをスレッドローカルにするようにLLVMに伝える。これによりC言語のスレッドローカル変数と同様に、ELFリンカ側の処理によりスレッドローカル変数が実現される。

この方法には以下のような問題点がある。

thread_local! マクロ

thread_local! マクロは、スレッドローカル変数に安全にアクセスするためのラッパーを生成する。例えば、

thread_local! {
    static X : Cell<u32> = Cell::new(0);
}

と書いたら、実際には以下のようなアイテムが生成される。

static X : LocalKey<Cell<u32>> = LocalKey { ... };

thread_local! は、もともとN:Mの並行性モデルを採用していたRustがグリーンスレッドを廃止して1:1に移行するにあたり、タスクローカル変数を提供するための local_data! を整理する形でできたものである。現在のネイティブスレッドの観点からも、このマクロ以下の点で優れているといえる。

  • 参照の有効期間が LocalKey::with により制限されるため、健全性の問題が解決されている。
  • ELFリンカに依存したスレッドローカル領域が提供されていない場合も、OS非依存の代替手段が利用される。

オブジェクトごとのスレッドローカル変数

以上とは別の話だが、オブジェクトごとにスレッドローカルなフィールドを持つ必要がある場合は thread_local crate というライブラリが使えるようだ。

まとめ

Rustでスレッドローカル変数を使うときは、 thread_local! マクロを使うのが望ましい。これは LocalKey という安全で移植性の高いラッパーを提供している。

Rustにおけるキーワードの役割一覧

概要: Rustにおけるキーワードの役割をできる限り列挙する。キーワードではないものについても一部取り上げる。

強キーワード部門

キーワード 役割
as 修飾パス <T as Tr>::foo
useとextern crateにおける別名 use foo as bar;
型の変換 x as usize
box box式 box 1
boxパターン box x
break break式 break 'a 1;
const 定数・関連定数 const X : i32 = 0;
関数型・関数宣言・関数定義 const fn f()
生ポインタ型 *const u8
continue continue式 continue 'a;
crate extern crate extern crate combine;
pub(restricted)構文 pub(crate) mod m;
else if-else if b {} else {}
if-let-else if let p = e {} else {}
enum 列挙型 enum E { ... }
extern 関数型・関数宣言・関数定義 extern "C" fn f()
外部アイテム extern "C" { ... }
extern crate extern crate combine;
false bool型の定数・パターン
fn 関数型・関数宣言・関数定義 fn f()
for トレイト実装 impl Tr for T {}
高階の関数ポインタ for<'a> fn f(&'a u8)
高階のトレイトオブジェクト・トレイト境界 for<'a> Tr<&'a T>
高階のwhere節 where for<'a> &'a [u8]: Tr<'a>
for式 for i in (0..10) {}
if if式 if b {} else {}
match腕のガード match x { Some(t) if t < 0 => { ... }, ... }
impl 実装 impl T {} impl Tr for T {}
匿名型 fn f() -> impl Fn()
in pub(restricted)構文 pub(in path::to::module) fn f();
配置構文 in place { expr }
for式 for i in (0..10) {}
let let文 let x = 0;
if let/while let式 if let Some(x) = x {}
loop loop式 loop {} 'a: loop {}
match match式 match x { Some(y) => {}, None => {}}
mod モジュール mod foo; mod foo {}
move クロージャ(ムーブキャプチャー) move |x, y| z
廃止されたムーブ参照? &move T
mut 書き換え可能な生ポインタ型 *mut T
専有参照型 &mut T
可変static static mut X : i32 = 0;
可変束縛 let (x, mut y) = (1, 2);
専有借用 &mut x
専有参照外しパターン &mut p
pub 可視性 pub pub(self) pub(super) pub(crate) pub(in path)
ref 借用パターン ref p
return return式 return return x
self 相対パス self::foo::bar
pub(restricted)構文 pub(self)
self引数 fn f(self: Box<Self>)
self引数ショートカット fn f(&self)
Self Self型引数 fn new() -> Self;
static 外部staticアイテム extern "C" { static X : u32; }
staticアイテム static X : u32 = 0;
struct 構造体 struct S {}
super 相対パス super::super::X
trait トレイト trait Tr {}
true bool型の定数・パターン
type 型別名 type A<X> = Box<Box<X>>;
関連型(トレイトまたはトレイト実装) impl Tr for T { type X = (); }
unsafe トレイト unsafe trait Send {}
関数型・関数宣言・関数定義 unsafe fn f()
実装 unsafe impl Send for T {}
unsafe式 unsafe { ... }
use インポートと再エクスポート use foo::bar;
where where節(トレイト・実装・構造体・共用体・列挙型・型別名・関数宣言・関数定義) where T: Clone
while while式 while b {}

弱キーワード部門

弱キーワードは特定の文脈でキーワードに準じる扱いを受ける識別子のことである。

キーワード 条件 役割
default impl内、 fn/type/constの前
※実装アイテムマクロ名として使えないがバグかもしれない
特殊化におけるデフォルトの実装 default fn f() { ... }
union モジュールまたはブロック内、識別子の前 共用体 union U { A(u8), B(u32) }
'static 生存期間 最小(最長)の生存期間境界

予約キーワード部門

予約キーワードは構文上の役割を持たないキーワードだが、将来に向けて部分的に実装されている場合がある。

キーワード 役割
abstract
alignof
become
do
final
macro 宣言マクロ2.0
offsetof
override
priv
proc
pure
sizeof
typeof typeof式
unsized
virtual
yield

基本型名部門

基本型名は構文上は通常の識別子であり、名前解決時に特別扱いされる。

識別子 役割
i8, i16, i32, i64, i128, isize 符号つき整数
u8, u16, u32, u64, u128, usize 符号なし整数
f32, f64 浮動小数点数
char Unicodeスカラー
bool 真理値
str UTF-8文字列

Prelude部門

preludeは既定で use されているアイテムである。現在は std::prelude::v1が使われている。

識別子 役割
Copy ムーブしてもムーブ元が有効であることを示すトレイト T: Copy
Send 他スレッドにムーブできることを示すトレイト T: Send
Sized バイト数が一定で、ポインタがthin pointerであることを示すトレイト T: ?Sized
Sync 参照を他スレッドにムーブできることを示すトレイト T: Sync
Drop デストラクタを定義するためのトレイト impl Drop for T { fn drop(&mut self) {} }
Fn &self で呼ばれるクロージャが実装するトレイト F: Fn(&[u8]) -> &u8
FnMut &mut self で呼ばれるクロージャが実装するトレイト Box<FnMut(i32) + 'a>
FnOnce self で呼ばれるクロージャが実装するトレイト F: FnOnce(i32) -> T
drop 値をその場で破棄する関数 drop(this_variable)
Box 単一の所有者をもつヒープ上のデータ Box::new([1, 2, 3])
ToOwned 別の型に複製できる型 s.to_owned()
Clone 同じ型に複製できる型 s.clone()
PartialEq 推移対称関係 x == y
PartialOrd 推移反対称関係 x < y
Eq 反射推移対称関係 x == y
Ord 全順序 x < y
AsRef 共有参照への明示的な変換 s.as_ref()
AsMut 専有参照への明示的な変換 s.as_mut_ref()
Into 明示的な変換 x.into::<io::Error>()
From 明示的な変換 (Intoの逆) impl From<T> for S { ... }
Default デフォルト値をもつ型 S { x: 1 .. Default::default() }
Iterator イテレータ for i in v.iter() {}
Extend 複数要素の一括挿入 v.extend([1, 2, 3]).iter()
IntoIterator イテレータに変換可能 for i in vec![1, 2, 3] {}
DoubleEndedIterator 両側から走査できるイテレータ for i in v.rev() {}
ExactSizeIterator 大きさの判明しているイテレータ v.iter().map(|&x| x + 1).len()
Option NoneかSomeをとる型 fn f() -> Option<i32> { ... }
Some 中身があるとき let mut x = Some(0);
None 中身がないとき let mut x = None;
Result 成功したときの結果か失敗したときのエラー内容をとる型 fn read() -> Result<T, E> { ... }
Ok 成功 { ...; Ok(()) }
Err 失敗 return Err(MyError::MysteriousError)
SliceConcatExt joinとconcat用 ["Chicken", "Egg"].join(" + ")
String 所有権のある文字列 let s = String::from("foo");
ToString to_string用 let s = 1.to_string();
Vec 所有権のある可変長配列 let x = Vec::with_capacity(100);

標準ライブラリのマクロ部門

標準ライブラリはデフォルトで #[macro_use] されるため、以下のマクロは指示なしですぐに使うことができる。

マクロ 役割
assert! falseならパニックする assert!(x < n);
assert_eq! 等しくないならパニック assert_eq!(a + b, b + a);
assert_ne! 等しいならパニック assert_ne!(i, j);
cfg! #[cfg()] のマクロ版。コンパイル時の条件次第でtrue/falseになる if cfg!(debug_assert) { ... }
column! コンパイル時行番号 println!("{}", column!());
concat! リテラルの連結 println!("{}", concat!("foo", "bar", 10));
debug_assert!
debug_assert_eq!
debug_assert_ne!
デバッグビルドでのみ有効化される assert*! マクロ
env! コンパイル環境変数展開 env!("PATH")
file! コンパイル時ファイル名 println!("{}", file!());
format! sprintf的なやつ let s = format!("{:08x}", 12345);
format_args! format!println! の内部処理
include! #include 的なやつ include!("precompiled.rs");
include_bytes! コンパイル時に外部ファイルをバイト列リテラルに変換 let icon = include_bytes!("icon.png");
include_str! コンパイル時に外部ファイルを文字列リテラルに変換 let csv = include_str!("data.csv");
line! コンパイル時行番号 println!("{}", line!());
module_path! コンパイル時モジュールパス println!("{}", module_path!());
option_env! コンパイル環境変数展開(ないかもしれない場合) option_env!("PATH")
panic! パニックする panic!("There must be a bug!");
print! printf的なやつ print!("Hello, {}!\n", name);
println! printf的なやつ+改行 print!("Hello, {}!", name);
stringify! 文字列化 println!("{}", stringify!(foo bar 0 1 # $ %));
thread_local! スレッドローカル変数の定義 thread_local! { static X : RefCell<Data> = RefCell::new(Data::new()); }
try! try構文 ? とほぼ同じ
unimplemented! パニックするが、未実装が理由のときに使うとよい
unreachable! パニックするが、本来なら到達しないはずのところで使うとよい
vec! 可変長配列の生成 vec![1, 2, 3] vec![10; 10000]
write! fprintf的なやつ write!(f, "Hello, {}!\n", name);
writeln! fprintf的なやつ+改行 writeln!(f, "Hello, {}!", name);

Rustにおける記号の役割一覧

概要: Rustにおける記号の役割をできる限り列挙する。

ASCII制御文字部門

文字 役割
HT (水平タブ) 空白
LF (ラインフィード) 空白
文字列中の改行
バン #! や行コメント // の終わり
VT (垂直タブ) 空白
CR (キャリッジリターン) 空白
CRLFでLFと等価に振る舞う
それ以外のASCII制御文字 用途なし

ASCII印字可能文字部門

文字 役割
SP (スペース) 空白
! 内部doc-comment //!, /*!
内部属性 #![]
バン #!
不等号 !=
否定やビット反転 !x
否定実装 !Trait
マクロ呼び出しとマクロ定義 macro!
" 文字列 " r" r#"
バイト列 b" br" br#"
# 内部属性 #![]
外部属性 #[]
バン #!
生文字列/生バイト列のガード r#####" foo "#####
$ 宣言マクロの仮引数 $x:expr
宣言マクロの変数展開 $x
衛生的な絶対パス $crate
宣言マクロの繰り返し束縛と繰り返し展開 $($x:tt)* $($x),*
% 剰余 x % y x %= y
& 参照型 &T/&self
アドレス演算子 &x
参照外しパターン &p
論理積やビットごとの論理積 x & y x &= y
短絡回路論理積 x && y
' 文字 '
バイト b'
生存期間 'lt
( ) 式・パターン・型の優先度変更 (x)
タプル式・タプルパターン・タプル型 (x, y)
関数呼び出し・タプル構造体パターン・タプル構造体や列挙型のバリアントの定義・関数宣言の引数部 f(x, y)
メソッド呼び出し x.f(y, z)
typeof(x)
クロージャトレイト用型パラメータ Fn(u8) -> u8
pub(restricted)構文 pub(self)/pub(super)/pub(crate)/pub(in somewhere)
マクロ呼び出し・定義 macro!(..)
宣言マクロの腕 ( pat ) => ( tt )
宣言マクロの繰り返し束縛と繰り返し展開 $($x:tt)* $($x),*
トークンツリー
* ブロックコメント /* /** /*! とその終わり */
globインポート use somewhere::*;
生ポインタ型 *const T *mut T
参照外し *x
乗算 x * y x *= y
宣言マクロの繰り返し束縛と繰り返し展開 $($x:tt)* $($x),*
+ 境界の併記 T: Tr1 + Tr2 + 'a Box<Tr + Send + 'a>
加算 x + y x += y
宣言マクロの繰り返し束縛と繰り返し展開 $($x:tt)+ $($x),+
, リストインポート use std::{io, cmp};
ジェネリック引数 F<T1, T2> F(T1) -> T2
where節 where T: Tr1, S: Tr2
タプル式・タプルパターン・タプル型 (x, y)
列挙型 enum E { .. , .. }
関数呼び出し・タプル構造体パターン・タプル構造体や列挙型のバリアントの定義・関数宣言の引数部 f(x, y)
クロージャ |x, y| z
メソッド呼び出し x.f(y, z)
配列とスライスパターン [x, y]
レコード構造体や列挙型のバリアントの定義・構造体式と構造体パターン S { x, y }
パターンマッチ match .. { .. , .. }
- 戻り値 fn() -> T |x, y| -> T { .. }
配置構文 place <- expr
指数表記 1e-3
負号演算 -x
減算 x - y x -= y
. タプル・タプル構造体パターンの残り (x, y, .., z)
構造体式のデフォルト・構造体パターンの残り S { x .. y }
スライスパターンの残り [x, y, .., z]
既定実装 impl T for .. {}
C互換外部関数の可変長引数 fn f(x: u8, ...);
半開区間 x .. y .. y x .. ..
区間 x ... y
フィールド x.y
メソッド呼び出し x.f(y, z)
小数点 3.14
/ コメント /* */ //
除算 x / y x /= y
0-9 整数 0xF 080 0o42 0b11
浮動小数点数 0.33
識別子の一部 x0
タプル・タプル構造体のフィールド x.0 S { 0: x }
: パス区切り ::x::y
関連アイテム <T as Tr>::X X::default
ジェネリクス引数 T::f::<X>()
宣言マクロの仮引数 $x:expr
境界(ジェネリクス引数、スーパートレイト、where節、関連型) T<X: Clone>
型(let, const, static, 引数など) let x : T;
構造体式・パターンのフィールド S { x: y }
名前つきブロック 'a: loop { .. }
; アイテムの終わり type X = u32;
文の終わり let x = 0;
宣言マクロの腕の区切り macro_rules! { .. ; .. }
< ジェネリクス引数 T<X, Y>
配置構文 place <- expr
左シフト x << y x <<= y
不等号 x < y x <= y
= 属性 #[cfg(feature="foo")]
型引数の既定値 trait Tr<X=Self> {}
関連型束縛 Iterator<Item=i32>
等式境界 T == S T = S
関連型または型別名 type T = ();
列挙型の判別子 enum E { X = 100 }
const/staticの初期化 const X : i32 = 0;
let/if let/while let let x = 0;
代入 x = y
複合代入 += -= *= /= ^= &= |= >>= <<=
等号・不等号 == <= >= !=
パターンマッチの腕 pat => expr
宣言マクロの腕 pat => expansion
> ジェネリクス引数 T<X, Y>
戻り値 fn() -> T |x, y| -> T { .. }
右シフト x >> y x >>= y
不等号 x > y x >= y
パターンマッチの腕 pat => expr
宣言マクロの腕 pat => expansion
? 既定のSized境界の削除 T: ?Sized
try構文 File::open()?
@ 別名パターン x @ (y, z)
廃止されたmanaged box型の記法
A-F a-f 識別子の一部 Feed
16進数 0xC0FFEE
b については下も参照
b バイト・バイト列 b'x' b"Foo"
r 生文字列・生バイト列 br##"# # #"##
それ以外の英字 識別子の一部 Zoo
[ ] 内部属性 #![]
外部属性 #[]
配列型・スライス型 [T; 5] [T]
配列またはスライスパターン [x; 5] [x, y]
添字記法 x[i]
マクロ呼び出し・定義 macro![..]
宣言マクロの腕 [ pat ] => [ tt ]
トークンツリー
\ エスケープ \\ \t \n \r \" \' \0
1バイトエスケープ \x7F
Unicodeエスケープ \u{00007F}
改行エスケープ (\ + 改行 + 任意個の空白)
^ 排他的論理和・ビットごとの排他的論理和 x ^ y x ^= y
_ 識別子の一部 is_some
数値リテラルの区切り文字 1_000_000_000
型推論 Vec<_>
束縛しないパターン (_, x)
` 未使用 (字句解析されない)
{ } リストインポート use std::{io, cmp};
構造体、共用体、列挙型のバリアント、列挙型、トレイト、実装、モジュール、外部アイテム trait X { ... }
ブロック (関数定義、制御構造) loop { ... }
パターンマッチ
構造体式・構造体パターン
マクロ呼び出し・定義 macro!{..}
宣言マクロの腕 { pat } => { tt }
トークンツリー
| パターンの並列 pat1 | pat2 => expr
クロージャ |x, y| z
論理和・ビットごとの論理和 x | y x |= y
短絡回路論理和 x || y
~ 廃止されたowned box型の記法
現在は未使用

Unicode制御文字・特殊文字部門

文字 役割
NEL, LRM, RLM, LINE SEPARATOR, PARAGRAPH SEPARATOR 空白

Unicode印字可能文字部門

XID_Start, XID_Continue 属性のついている文字は識別子の一部と解釈される。

FnBoxについて

Rustの FnBox について、動機・仕組み・問題点を説明する。

FnBox の動機

以前の記事では、「「クロージャを boxせずに 返したい」という欲求は人類の四大欲求のひとつと言われている。 」と書いたが、出所の異なるクロージャを同じ型で扱う必要がある場合は無理せず Box に入れるべきである。

例えば、非同期処理など、継続渡し形式で何らかの処理をする場合は、一般に生のクロージャでは使い勝手が悪い。おもちゃ的な例として、フィボナッチ数列の計算を継続渡し形式で行うことを考える。以下のように書くとうまくいかない。

fn fib_cont<T, F: Fn(u32) -> T>(n: u32, continuation: F) -> T {
    if n <= 1 {
        continuation(n)
    } else {
        fib_cont(n - 1, |a| {
            fib_cont(n - 2, |b| {
                continuation(a + b)
            })
        })
    }
}

fn main() {
    fib_cont(13, |a| {
        println!("{}", a);
    });
}

そこで、生のクロージャではなく、トレイトオブジェクトを渡すようにする。動的ディスパッチになってしまうが、異なるクロージャを同じ型で渡せるようになる。

fn fib_cont<'a, T>(n: u32, continuation: Box<Fn(u32) -> T + 'a>) -> T {
    if n <= 1 {
        continuation(n)
    } else {
        fib_cont(n - 1, Box::new(|a| {
            fib_cont(n - 2, Box::new(|b| {
                continuation(a + b)
            }))
        }))
    }
}

fn main() {
    fib_cont(13, Box::new(|a| {
        println!("{}", a);
    }));
}

ここで Fn を使ったが、場合によっては FnMutFnOnce をインターフェースとして使いたい場合もある。 Fn/FnMut/FnOnce は以下のような違いがある。

  • Fn は所有権や書き込み権限がない参照に対しても呼ぶことができる。その代わり、キャプチャーした変数に書き込めないし、仮にmoveキャプチャーでもその変数を外に動かすことはできない。
  • FnMut は所有権がなくても呼べるが、 mut である必要はある。キャプチャーした変数には書き込めるが、仮にmoveキャプチャーでもその変数を外に動かすことはできない。
  • FnOnce は所有権があるときに1回だけ呼べる。その代わり、キャプチャーした変数には書き込めるし、moveキャプチャーならキャプチャーした変数を外に動かすことができる。

実践的な例ではないが、先ほどのフィボナッチ数列の例を FnOnce で書き直してみる。

fn fib_cont<'a, T>(n: u32, continuation: Box<FnOnce(u32) -> T + 'a>) -> T {
    if n <= 1 {
        continuation(n)
    } else {
        fib_cont(n - 1, Box::new(move |a| {
            fib_cont(n - 2, Box::new(move |b| {
                continuation(a + b)
            }))
        }))
    }
}

fn main() {
    fib_cont(13, Box::new(|a| {
        println!("{}", a);
    }));
}

ところがこのプログラムは現在のRustではエラーになる。これは FnOnce の定義を見るとわかる。

pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

この call_once メソッドは Self 型の引数をとるにも関わらず、 Self: Sized を要求していない。以前の記事で指摘したように、このことは実は許されているのであった。また、オブジェクト安全性の条件も実は満たされているため、 FnOnce(u32) -> T の対応するトレイトオブジェクト自体は実際に存在することになる。

しかし実際に FnOnce(u32) -> T を呼ぶためには、このオブジェクトをムーブで渡す必要がある。しかしムーブで渡すにはその型が Sized である必要があるから、うまくいかない。つまりオブジェクトを生成する時点では問題にはならないが、実際にメソッドを呼ぶ段階で問題が発生することになる。

FnBox の仕組み

std::ops で定義されている Fn, FnMut, FnOnce とは異なり、 FnBox は単なるユーザー定義のトレイトである。 (extern "rust-call" と丸括弧記法を採用しているという特殊性はあるものの)

そしてその定義は以下のようになっている。

pub trait FnBox<A> {
    type Output;

    fn call_box(self: Box<Self>, args: A) -> Self::Output;
}

selfBox<Self> 型をとれることは以前の記事で説明してある

さて、このトレイトの定義は FnOnce とほぼ同じである。唯一の違いは、 call_onceselfSelf なのに対して、 call_boxselfBox<Self> であるという点である。したがってこの2つのトレイトは、 Self: Sized である限り、相互に実装可能である。実際、 FnBox は全ての FnOnce に対して実装されている。 (もちろん逆はない。一貫性条件に反するからである。)

impl<A, F> FnBox<A> for F where F: FnOnce<A> { ... }

ここで重要なのは、 Box<FnOnce>Box<FnBox> の違いである。以前の記事で説明したvtableの構造 によると、これらのfatポインタの指すvtableは以下の構造を持つことになる。

  • Box<FnOnce> のfatポインタの指す先には、
    • 0番目: Drop Glue
    • 1番目: 元の型のバイト数
    • 2番目: 元の型のアラインメント
    • 3番目: call_once へのポインタ
  • Box<FnBox> のfatポインタの指す先には、
    • 0番目: Drop Glue
    • 1番目: 元の型のバイト数
    • 2番目: 元の型のアラインメント
    • 3番目: call_box へのポインタ

さて、このfatポインタが判明している状態で、クロージャを呼ぶことを考える。 call_once を呼ぶことを考えると、これはほぼ不可能であると考えられる。例えば f64u64 は通常同じバイト数、同じアラインメントを持つが、 f64u64 をそれぞれ第1引数に持つ関数は格納先のレジスタが異なるかもしれない。これはvtableだけからは区別できない。

一方 call_boxFnMut::call_mutFn::call と同じように簡単である。 第1引数はthinポインタなので、fatポインタの0番目をそのまま渡せばよい。

最後に、 FnBoxFnOnce を実装していないが、 Box<FnBox>FnOnce を実装している。

impl<'a, A, R> FnOnce<A> for Box<FnBox<A, Output = R> + 'a> { ... }

言語処理系は FnBox を特別扱いしないが、上のように FnOnce を実装しているため通常の関数呼び出し構文が利用可能になっている。

結局、 FnOnce から出発して、 FnBox を経由して FnOnce に戻ってきたことになる。このように、 FnBox は適切なvtableを提供するための中間表現であると言える。

FnBox の使い方

FnBox の使い方は簡単で、 Box<FnOnce> として使いたい場所を Box<FnBox> に変えるだけでよい。ただし、以下の2点に注意する必要がある。

  • 機能ゲートで守られているので、nightlyコンパイラを使い、冒頭で #![feature(fnbox)] を宣言する必要がある。
  • preludeには入っていないので、 use std::boxed::FnBox; する必要がある。
#![feature(fnbox)]

use std::boxed::FnBox;

fn fib_cont<'a, T>(n: u32, continuation: Box<FnOnce(u32) -> T + 'a>) -> T {
    if n <= 1 {
        continuation(n)
    } else {
        fib_cont(n - 1, Box::new(move |a| {
            fib_cont(n - 2, Box::new(move |b| {
                continuation(a + b)
            }))
        }))
    }
}

fn main() {
    fib_cont(13, Box::new(|a| {
        println!("{}", a);
    }));
}

安定化していない理由

FnBox は以上のようによくできた仕組みだが、安定化されていない。 追跡用のissueによると、これは主に次の2つの理由によるもののようだ。

  • 「値渡しのself」に取って代わられる可能性がある。
  • トレイト実装の一貫性に引っかかる部分の解決ができていない。
  • 高階トレイト境界とうまく相互作用しない。

「値渡しのself」

By-value-self (「値渡しのself」) とはまさに上で述べた FnOnce のような状況を指している。

現段階で有効な追跡issueが見当たらなかったので、現在は誰も動いていないようだが、意図されているのは次のような内容だと思われる: selfを値渡しするメソッドがあった場合は、vtableにはサイズが不明でも渡せるような別の関数へのポインタを入れておく。selfの値渡しメソッドの呼び出しが実際に発生した場合は、それがBoxの参照外しとセットだったら Sized でなくても許可するようにする。このようにして FnOnce に限らない一般のトレイトに対してもワークアラウンドなしにトレイトオブジェクトが動作するようにする。

もしこの機能が本当に計画されているのであれば、 FnBox はやがて不要になり、廃止されることになる。これが安定化されていない第一の理由である。

ただし、2015年12月の時点で、値渡しのselfまでの一時的な措置として安定化させることが決定されているため、現在はこの理由は有効ではない。

トレイト実装の一貫性

Rustでは、複数のトレイト実装が非決定的に選択可能な状況を避けるための「一貫性(coherence)」規則を採用している。これはorphan ruleとoverlapping ruleからなる。

クロージャーの実装は難しい一貫性の問題を抱えている。というのもクロージャFn, FnMut, FnOnce と3段階に分けて提供されているため、ラッパーに対する Fn* 実装を提供するとほぼ必ず複数の経路での実装が存在することになってしまうのである。例えば、

impl<A, F: ?Sized> Fn<A> for Box<F> where F: Fn<A> { ... } // 実装A
impl<A, F: ?Sized> FnMut<A> for Box<F> where F: FnMut<A> { ... } // 実装B
impl<A, F: ?Sized> FnOnce<A> for Box<F> where F: FnOnce<A> { ... } // 実装C

という3種類のラッパーを提供するのはごく自然である。しかし3種類全てを提供すると、一貫性の問題が発生する。例えば F: Fn<A> だとして、 Box<F>: FnOnce<A> を解決したいとする。すると以下の3種類の経路の実装が選択可能である。

  • F: Fn<A> と実装Aから、 Box<F>: Fn<A> が提供される。このスーパートレイトとして Box<F>: FnOnce<A> が導出される。
  • F: Fn<A> のスーパートレイト F: FnMut<A> と実装Bから、 Box<F>: FnMut<A> が提供される。このスーパートレイトとして Box<F>: FnOnce<A> が導出される。
  • F: Fn<A> のスーパートレイト F: FnOnce<A> と実装Cから、 Box<F>: FnOnce<A> が提供される。

Rustはこれらの同一性を確かめる術は今のところないから、そもそも上記の実装A,B,Cの共存を認めていない。

これは FnBox に関係なく Fn, FnMut, FnOnce, ひいてはより一般のトレイト (Into など) に広く認められている問題である。ただ FnBox も同じ問題を抱えている。というのも、

impl<'a, A> FnOnce<A> for Box<FnBox<A> + 'a> { ... }
impl<A, F: ?Sized> FnOnce<A> for Box<F> where F: FnOnce<A> { ... }

がoverlapping ruleに引っかかるからである。

これは本来overlapしないが、何かのバグでそうなっているようだ。(例えば、 A 引数を削除した例を作って実験するとコンパイルが通る。)

高階トレイト境界との相互作用

また、 FnBox は高階トレイト境界に対してうまく動作しないという問題がある。

例えば Box<FnBox(&i32)>Box<for<'a> FnBox(&'a i32)> の構文糖衣だが、このように高階トレイト境界を含んでいる場合は FnOnce が実装されないため、クロージャを呼び出すことができない。

これは一般に、高階の生存期間束縛を含む型に対するジェネリックな実装をうまく与える手段がないことに由来する。

まとめ

FnOnce をbox化して呼び出す手段は必要性は認識されているものの、安定的な実現はまだ得られていない。どうしても今必要なら、上に挙げた問題点を把握しつつ、 std::boxed::FnBox を使うのが望ましいだろう。

Rustで引数型と戻り値型がSizedでなくてもよい条件

過去の記事Sizedについて説明したが、関数の引数と戻り値についてはやや直感に反する条件が適用されているので説明する。

trait Foo {
    fn f(x: str) -> str; // OK
    // fn g(x: str) -> str { x } // error
}
fn h(_: str) {} // OK
// fn i(x: str) -> str { x } // error
fn j(ref x : str) {} // OK, but no way to call it
fn main() {
}

この規則は一見すると重箱的な問題のように見えるが、実は FnOnce トレイトを見ると実際に活用されていることがわかる。

pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
    //                              ^^^^ Self : ?Sized
}

トレイトの Self 引数だけは暗黙の Sized 境界が適用されないため、ここでは Self: ?Sized である。(関連型は Sized が適用される。)

この関数に Self: Sized 境界が必要ないのは、上記の条件のためである。

RustのUnsafeCellとFreeze

概要: UnsafeCell の存在目的について説明する。

UnsafeCell とは

UnsafeCell とは、その名前の通り、内部可変性(interior mutability)を実現するためのunsafeはプリミティブである。

UnsafeCell の代表的な用途は内部可変性を実現するための安全なラッパーであるCellRefCell である。それらの他に、並行性を扱うモジュール(std::sync, std::thread, std::sys)の内部で使われている。

UnsafeCell 自体は以下のように単なるラッパー構造体である

pub struct UnsafeCell<T: ?Sized> {
    value: T,
}

したがって UnsafeCell の特異性はコンパイラによる特殊な扱いに由来することになる。

UnsafeCell の扱い

UnsafeCell は主に以下の点で特殊である。

Freezecore::marker 内に定義されているプライベートなマーカートレイトである。その定義は以下のようになっている。(1.17.0時点では Freeze はまだ存在せず、コンパイラに組込みのフラグで管理されている。)

#[lang = "freeze"]
unsafe trait Freeze {}

unsafe impl Freeze for .. {}

impl<T: ?Sized> !Freeze for UnsafeCell<T> {}
unsafe impl<T: ?Sized> Freeze for PhantomData<T> {}
unsafe impl<T: ?Sized> Freeze for *const T {}
unsafe impl<T: ?Sized> Freeze for *mut T {}
unsafe impl<'a, T: ?Sized> Freeze for &'a T {}
unsafe impl<'a, T: ?Sized> Freeze for &'a mut T {}

impl Tr for .. {} については過去の記事を参照。これにより、Freezeはおよそ「UnsafeCell をメンバとして持たない型」として定義されることになる。 ([T; 0] という例外はある。)

Freeze の扱い

Freeze は主に次のような役割をもつ。

なお、「エイリアスを持たない」というのは正確にいうと全く正しくないが、LLVMの最適化フラグとしてのnoaliasは、「エイリアスによるデータ競合が発生しない」という意味であるため、問題ない。

逆に、真にエイリアスを持たないはずの &mut にはnoaliasがついていない。これはLLVMの最適化バグへのワークアラウンドによるものである。

まとめ

mut がついていないのにメモリ領域が書き換えられる可能性がある場合は、該当部分を UnsafeCell でラップする必要がある。通常は UnsafeCellCellRefCell などのより高級なライブラリを経由して呼び出されるので、この内容をより具体的に把握する必要はない。