Rust 関連型(Associated types) を生かしたまま 動的ディスパッチ する
Rust の関連型(Associated types)は Trait に対して文字通り”関連づける”型を定義するものです。
Rust By Example の以下の例のtype A;
やtype B;
の部分です。
// `A` and `B` are defined in the trait via the `type` keyword.
// (Note: `type` in this context is different from `type` when used for
// aliases).
trait Contains {
type A;
type B;
// Updated syntax to refer to these new types generically.
fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
}
// <https://doc.rust-lang.org/rust-by-example/generics/assoc_items/types.html>より
struct に実装する際にはtype A = i32;
のように指定することになります。
今回これを動的ディスパッチに用いることを考えてみます。
関連型のついた Trait を動的ディスパッチに用いる際の問題点
挨拶のメッセージを取得するアプリケーションを考えます。
メッセージのリポジトリは Trait で実装しておいて、DI(Dependency Injection)をしたいとします。
たとえば以下のような形です。
trait GreetingRepository {
type Message;
fn get(&self) -> Self::Message;
}
struct GreetingService {
repo: Box<dyn GreetingRepository>, // here!
}
impl GreetingService {
fn get_message(&self) -> String {
let message = self.repo.get();
format!("The message is {}", message)
}
}
GreetingService
構造体には、GreetingRepository
を実装した構造体のインスタンスならなんでも受け取れるようにしようというアイデアです。
上記コードのhere!
部分が動的ディスパッチの部分です。
このコードはコンパイルできません。
GreetingRepository
にはMessage
という関連型が定義されているため、以下のように GreetingService で使う際にこれを指定するように指摘されます。
the value of the associated type `Message` (from trait `sample::GreetingRepository`) must be specified
つまり、以下のようにするとコンパイルは通るわけです。
struct GreetingService {
repo: Box<dyn GreetingRepository<Message = String>>,
}
では、GreetingService
を実装したリポジトリを作ってみます。
struct GreetingRepositoryA {
msg: String,
}
impl GreetingRepository for GreetingRepositoryA {
type Message = String;
fn get(&self) -> Self::Message {
self.msg.clone()
}
}
#[derive(Clone)]
pub struct Msg(String);
struct GreetingRepositoryB {
msg: Msg,
}
impl GreetingRepository for GreetingRepositoryB {
type Message = Msg;
fn get(&self) -> Self::Message {
self.msg.clone()
}
}
GreetingRepositoryA
ではMessage
をString
にGreetingRepositoryB
ではMessage
をMsg
という専用のメッセージ型を使うことにしました。
では、これらを実際に使ってみます。
fn main() {
let a_repo = Box::new(GreetingRepositoryA {
msg: "message from A".to_string(),
});
let a_service = GreetingService { repo: a_repo };
let message = a_service.get_message();
}
GreetingServiceA
はうまく使えました。ではGreetingServiceB
はどうでしょう?
fn main() {
// ...省略
let b_repo = Box::new(GreetingRepositoryB {
msg: Msg("message from B".to_string()),
});
let b_service = GreetingService { repo: b_repo };
let message = b_service.get_message();
// ...省略
}
これは以下のようにエラーとなります。
type mismatch resolving `<sample::GreetingRepositoryB as sample::GreetingRepository>::Message == std::string::String`
required for the cast to the object type `dyn sample::GreetingRepository<Message = std::string::String>`
直感的なメッセージではないですが、要するに GreetingService ではMessage
関連型がString
であることを要求しているのに対し、
GreetinRepositoryB
ではMessage
がMsg
型と定義されているため、型が一致していないということです。
これでは折角関連型を用意した甲斐も虚しく、リポジトリを利用する側であるサービスがリポジトリの関連型を制限する形になるわけです。
解決方法
解決方法は、関連型のついた Trait を別の Trait でラップすることです。
以下のような形になります。
trait GreetingRepository {
type Message;
fn get(&self) -> Self::Message;
fn to_string_message(msg: Self::Message) -> String;
}
trait GreetingRepositoryWrapper {
fn get(&self) -> String;
}
// TraitをTraitに実装する
impl<T: GreetingRepository> GreetingRepositoryWrapper for T {
fn get(&self) -> String {
let msg = self.get(); // <- ①
T::to_string_message(msg) // <- ②
}
}
上記 ① のself.get()
は大本のGreetingRepository
のメソッドです。
よって、msg
の型は<T as GreetingRepository>::Message
となっています。
② ではT::to_string_message(msg)
としていますが、このT
はGreetingRepository
のことです。
to_string_message
関数がインスタンスメソッドではないため、このような呼出となっています。
考え方としては、元の Trait は、メッセージの型を好きな型(Message 関連型)で扱うことを許すが、
基本的な型(ここでは String)に変換する方法は自身で知っておいて欲しいということです。
これを使って、前にエラーとなっていた GreetingRepositoryB を中心に書き直すと以下のようになります。
struct GreetingService {
repo: Box<dyn GreetingRepositoryWrapper>, // <- ③
}
impl GreetingRepository for GreetingRepositoryB {
type Message = Msg;
fn get(&self) -> Self::Message {
self.msg.clone()
}
fn to_string_message(msg: Self::Message) -> String { // <- ④
msg.0
}
}
上記 ③ は、Wrapper の方を持つように変更します。
④ については、String への変換方法の実装が必要になったので追加しています。
実は変更はこれだけで OK なのですが、こっそり中身だけ変わっている部分があります。それが以下のGreetingService
の実装の ⑤ 部分です。
impl GreetingService {
fn get_message(&self) -> String {
let message = self.repo.get(); // <- ⑤
format!("The message is {}", message)
}
}
この部分のコードそのものに変更はないのですが、当初GreetingRepository
のget
を呼び出していたものが、
今はGreetingRepositoryWrapper
のget
を呼ぶようになっているのです!
(③ の変更をおこなっているので当然ではありますが)
ではmain
関数はどうなったかというと、
fn main() {
// ...
let b_repo = Box::new(GreetingRepositoryB {
msg: Msg("message from B".to_string()),
});
let b_service = GreetingService { repo: b_repo };
let message = b_service.get_message();
}
ここもコードに変更はありませんが、今度はコンパイルが通るようになっています。
注目はGreetingService
に入れているb_repo
の型です。
GreetingService
のフィールドrepo
の型はBox<dyn GreetingRepositoryWrapper>
ですが、
b_repo
の Box の中身の型GreetingRepositoryB
が実装しているのは、大本の型GreetingRepository
なのです。
GreetingRepositoryWrapper
-> GreetingRepository
-> GreetingRepositoryB
と実装しているので、
結局GreetingRepositoryB
はGreetingRepositoryWrapper
を実装しているということです。
これで、関連型付きの Trait を使って動的ディスパッチができるようになりました!
まとめ
今回は Rust の関連型がついた Trait を動的ディスパッチに利用する方法を検討しました。
関連型つき Trait を別 Trait でラップし、基本的な型との変換方法の提供を実装側の責務とすることで、
DI 的な用途に用いることがわかりました。
例として挙げているコードでは動的ディスパッチを利用していますが、
ジェネリクスを用いた場合でもほとんど同じ感覚で利用できます。
その例も最後に挙げておきます。
最後に身も蓋もないことを言いますが、
例として挙げたコードは DDD を Rust で試していた時の実装から着想を得ているのですが、
DDD のリポジトリ部分などで今回の方法を使うのはあまりオススメできません。。。
Trait 内で扱う関連型の数が増えるたびに、本来のリポジトリが必要とするコードを覆い隠すほど、”ある意味無意味”な変換関数増えていきました。
他に良い用途が思いつけばまた書きたいと思います。
Rust を勉強し始めましたが、面白いですね!
コード全体
今回のコード全体を掲載します。色々と書き足して、テスト例も書いています。
こちらの Playgroundでも試せます。
mod domain {
pub trait GreetingRepository {
type Message;
fn get(&self) -> Self::Message;
fn to_string_messege(msg: Self::Message) -> String;
fn from_string_message<S: Into<String>>(msg: S) -> Self::Message;
}
pub trait GreetingRepositoryWrapper {
fn get(&self) -> String;
}
impl<T: GreetingRepository> GreetingRepositoryWrapper for T {
fn get(&self) -> String {
let msg = self.get();
T::to_string_messege(msg)
}
}
}
mod repository {
use super::domain::GreetingRepository;
pub struct GreetingRepositoryA {
msg: String,
}
impl GreetingRepository for GreetingRepositoryA {
type Message = String;
fn get(&self) -> Self::Message {
self.msg.clone()
}
fn to_string_messege(msg: Self::Message) -> String {
msg
}
fn from_string_message<S: Into<String>>(msg: S) -> Self::Message {
msg.into()
}
}
impl GreetingRepositoryA {
pub fn new() -> Self {
Self {
msg: "Hello from A".to_string(),
}
}
}
#[derive(Clone)]
pub struct Msg(String);
pub struct GreetingRepositoryB {
msg: Msg,
}
impl GreetingRepository for GreetingRepositoryB {
type Message = Msg;
fn get(&self) -> Self::Message {
self.msg.clone()
}
fn to_string_messege(msg: Self::Message) -> String {
msg.0
}
fn from_string_message<S: Into<String>>(msg: S) -> Self::Message {
Msg(msg.into())
}
}
impl GreetingRepositoryB {
pub fn new() -> Self {
Self {
msg: Msg("Hello from B".to_string()),
}
}
}
}
mod application {
use super::domain::GreetingRepositoryWrapper;
pub struct GreetingServiceA {
greeting_repo: Box<dyn GreetingRepositoryWrapper>,
}
impl GreetingServiceA {
pub fn new(greeting_repo: Box<dyn GreetingRepositoryWrapper>) -> Self {
Self { greeting_repo }
}
pub fn say(&self) -> String {
let msg = self.greeting_repo.get();
format!("[Dynamic Dispatch] {}", msg)
}
}
pub struct GreetingServiceB<T>
where
T: GreetingRepositoryWrapper,
{
greeting_repo: T,
}
impl<T: GreetingRepositoryWrapper> GreetingServiceB<T> {
pub fn new(greeting_repo: T) -> Self {
Self { greeting_repo }
}
pub fn say(&self) -> String {
let msg = self.greeting_repo.get();
format!("[Generics] {}", msg)
}
}
}
fn main() {
use application::{GreetingServiceA, GreetingServiceB};
use repository::{GreetingRepositoryA, GreetingRepositoryB};
let service_a_1 = GreetingServiceA::new(Box::new(GreetingRepositoryA::new()));
println!("{}", service_a_1.say());
let service_a_2 = GreetingServiceA::new(Box::new(GreetingRepositoryB::new()));
println!("{}", service_a_2.say());
let service_b_1 = GreetingServiceB::new(GreetingRepositoryA::new());
println!("{}", service_b_1.say());
let service_b_2 = GreetingServiceB::new(GreetingRepositoryB::new());
println!("{}", service_b_2.say())
}
#[cfg(test)]
mod test {
use super::{application::GreetingServiceA, domain::GreetingRepository};
#[test]
fn service_a_is_working() {
struct MockGreeting;
impl GreetingRepository for MockGreeting {
type Message = String;
fn get(&self) -> Self::Message {
"Hello from test".to_string()
}
fn to_string_messege(msg: Self::Message) -> String {
msg
}
fn from_string_message<S: Into<String>>(msg: S) -> Self::Message {
msg.into()
}
}
let test_service = GreetingServiceA::new(Box::new(MockGreeting {}));
let message = test_service.say();
assert_eq!(message, "[Dynamic Dispatch] Hello from test".to_string())
}
}