LoginSignup
14
7

More than 5 years have passed since last update.

Rustと継承に関するメモ

Last updated at Posted at 2018-04-06

はじめに

継承といっても#[derive(..)]のことではなくC++的な継承が欲しい場面でRustならどうするかという話。結論めいたものはない。アドバイスいただけると嬉しい。

発端

AtCoderの勉強にとRustでダイクストラ法の実装をおこなっていた。
いろいろ省略しているが以下の様なトレイトを作った。

mod dijkstra {

    #[derive(Copy, Clone, Eq, PartialEq)]
    pub struct State<T>
    {
        pub cost: usize,
        pub pos: T
    }

    pub trait Context<T>
    {
        fn cost(&self, pos: T) -> usize;
        fn update_cost(&mut self, s: &State<T>);
        fn neighbours(&self, s: &State<T>) -> Vec<State<T>>;

        fn run(&mut self, start: State<T>, end: T) -> Option<usize>
          where T: Copy+Ord {
            // 省略
        }
    }
}

あとは各問題に合わせてContextトレイトを実装すればよい(はず)。
ところで(全てではないが)多くの場合costはHashMapで管理すれば良さそうだ。
しかしトレイトにHashMapをメンバとして持たすことはできない、さてどうしようという話。

C++での実装

このときC++であればContextを継承してメンバと実装を持たせてしまうのが手っ取り早い。

// 疑似コード
class ContextWithHashMap : public Context {
    HashMap cost_table;

    size_t cost(T pos) {
        return cost_table[pos];
    }

    void update_cost(State<T>& s) {
        cost_table[s.pos] = s.cost;
    }
};

ユーザーはContextWithHashMapを継承してneighbours()の実装だけおこなえばよい。

Rustでの実装その1(差分をトレイトにする)

NeighbourトレイトとDefaultContextを新たに作る。
ユーザーはNeighbourトレイトを実装して、それを引数にDefaultContextを作ることになる。

    pub trait Neighbours<T>
    {
        fn neighbours(&self, s: &State<T>) -> Vec<State<T>>;
    }

    pub struct DefaultContext<'a, T, U>
    where U: 'a
    {
        pub cost_table: HashMap<T, usize>,
        pub data: &'a U
    }

    impl<'a, T, U> DefaultContext<'a, T, U>
    where T: Eq+Hash, U: Neighbours<T>
    {
        pub fn new(data: &'a U) -> Self {
            DefaultContext{ cost_table: HashMap::new(), data: data }
        }
    }

    impl<'a, T, U> Context<T> for DefaultContext<'a, T, U>
    where T: Copy+Clone+Eq+Hash, U: Neighbours<T>
    {
        fn cost(&self, pos: T) -> usize {
            self.cost_table.get(&pos).map_or(::std::usize::MAX, |c| *c)
        }

        fn update_cost(&mut self, s: &State<T>) {
            self.cost_table.insert(s.pos, s.cost);
        }

        fn neighbours(&self, s: &State<T>) -> Vec<State<T>> {
            self.data.neighbours(s)
        }
    }

むりやりC++っぽい実装を実現しようとするとこうなるだろうか。
DefaultContext::new()にNeighbourを渡すというのは回りくどいし、モジュール側のコードがC++より多いのもどうもいただけない。

Rustでの実装その2(ラムダ渡し)

DefaultContext作るところはその1と同じだが、neighbour()をラムダとして渡すようにしている。

    pub struct DefaultContext<'a, T: 'a>
    {
        pub table: HashMap<T, usize>,
        pub f: &'a Fn(&State<T>) -> Vec<State<T>>
    }


    impl<'a, T> Context<T> for DefaultContext<'a, T>
    where T: Copy+Clone+Eq+Hash
    {
        fn cost(&self, pos: T) -> usize {
            self.table.get(&pos).map_or(::std::usize::MAX, |c| *c)
        }

        fn update_cost(&mut self, s: &State<T>) {
            self.table.insert(s.pos, s.cost);
        }

        fn neighbours(&self, s: &State<T>) -> Vec<State<T>> {
            (self.f)(s)
        }
    }

    #[allow(dead_code)]
    pub fn create_default_context<'a, T, F>(f: &'a F) -> DefaultContext<'a, T>
    where T: Eq + Hash, F: Fn(&State<T>) -> Vec<State<T>> {
        DefaultContext{ table: HashMap::new(), f: f}
    }

その1よりはスマートな感じがするが、ユーザーが実装するメソッドが増えるとこの手は使えなさそう。

Rustでの実装その3(マクロ使用)

マクロを使ってみる

    macro_rules! cost_table {
        ($cost_table:ident, $t:ty) => (
            fn cost(&self, pos: $t) -> usize {
                self.$cost_table.get(&pos).map_or(::std::usize::MAX, |c| *c)
            }

            fn update_cost(&mut self, s: &State<$t>) {
                self.$cost_table.insert(s.pos, s.cost);
            }
        )
    }

ユーザーはこういう感じで使う

struct ContextImpl {
    cost_table: HashMap<(usize, usize), usize>
}

impl Context<(usize, usize)> for ContextImpl {
    cost_table!(cost_table, (usize, usize));

    fn neighbours(&self, s: &State<(usize, usize)>) -> Vec<State<(usize, usize)>> {
        // 省略
    }
}

モジュール側の実装は簡単だが、ユーザー側でHashMapを持ったり、マクロにStateの型パラメータを指定する必要があるのがかなりツライ。

まとめ

思いつく実装を3つほど挙げて見たもののどれもピンとこない。何かもっと良い実装はないだろうか。

14
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7