RustのActorを扱えるライブラリrikerを使ってみる
とりあえず使ってみた感想
- 軽く遊べる程度にはいろいろ揃っている。
- メッセージが型付きでとても Rust らしい。
- Akka とほとんど API の名前が同じで Akka を読み替えながら実装できそうな感じがする。
まだ TicketSeller 側は実装中だけど、『Akka実践バイブル』を参考に Scala のコードを Rust に直してみた。
使ってみた感じは、完全に Akka 笑。
実践 Akka のコードは Typed Akka ではないため、正直型付けをどうしたらいいかかなり悩んだ。これも完全なサンプルとは言えないと思う。とくに、パターンマッチングの部分。消費しないケースがあると Rust ではエラーになるが、サンプルコードの方は Any に対する match なのでそのケースが考慮されてない、など。
あとは私の Akka の習熟度の問題があるのだけど、Result
で返すところをことごとく unwrap している。recv の返り値は Unit なのだけど、Result を適切にさばくには…というのが研究の余地がありそう。
use riker::actors::*;
#[derive(Clone, Debug)]
pub struct Ticket {
id: i32,
}
impl Ticket {
pub fn new(id: i32) -> Self {
Ticket { id }
}
}
#[derive(Clone, Debug)]
pub enum TicketSellerMessage {
Add(Vec<Ticket>),
Buy(usize),
Ticket(i32),
Tickets { event: String, entries: Vec<Ticket> },
GetEvent,
Cancel,
}
#[derive(Default)]
pub struct TicketSellerActor {
pub event: String,
pub tickets: Vec<Ticket>,
}
impl Actor for TicketSellerActor {
type Msg = TicketSellerMessage;
fn recv(
&mut self,
ctx: &Context<TicketSellerMessage>,
msg: TicketSellerMessage,
sender: Sender,
) {
match msg {
TicketSellerMessage::Add(tickets) => {
let mut tickets = tickets;
self.tickets.append(&mut tickets)
}
TicketSellerMessage::Buy(nr_of_tickets) => {
let entries: Vec<Ticket> = self
.tickets
.iter()
.take(nr_of_tickets)
.map(|s| s.clone())
.collect();
if entries.len() >= nr_of_tickets {
unimplemented!()
} else {
unimplemented!()
}
}
TicketSellerMessage::GetEvent => unimplemented!(),
TicketSellerMessage::Cancel => unimplemented!(),
_ => (),
}
}
}
use crate::ticket_seller::{Ticket, TicketSellerActor, TicketSellerMessage};
use anyhow::Result;
use riker::actor::*;
#[derive(Clone, Debug)]
pub struct Event {}
#[derive(Clone, Debug)]
pub enum BoxOfficeMessage {
CreateEvent { name: String, tickets: i32 },
GetEvent(String),
GetTickets { event: String, tickets: i32 },
CancelEvent(String),
Event { name: String, tickets: i32 },
Events(Vec<Event>),
}
#[derive(Clone, Debug)]
pub enum EventResponse {
EventCreated(Event),
EventExists,
}
#[derive(Default)]
pub struct BoxOfficeActor {}
impl BoxOfficeActor {
fn create_ticket_seller(
&self,
name: &str,
context: &Context<BoxOfficeMessage>,
) -> Result<ActorRef<TicketSellerMessage>> {
let actor = context.actor_of::<TicketSellerActor>(name)?;
Ok(actor)
}
}
impl Actor for BoxOfficeActor {
type Msg = BoxOfficeMessage;
fn recv(&mut self, ctx: &Context<BoxOfficeMessage>, msg: BoxOfficeMessage, sender: Sender) {
match msg {
BoxOfficeMessage::CreateEvent { name, tickets } => {
let found = ctx
.myself
.children()
.find(|actor_ref| actor_ref.name() == name);
match found {
Some(base_ref) => {
base_ref
.try_tell(EventResponse::EventExists, Some(ctx.myself().into()))
.unwrap();
}
None => {
let event_tickets = self.create_ticket_seller(&name, ctx).unwrap();
let new_tickets: Vec<Ticket> = (1..tickets)
.map(|ticket_id| Ticket::new(ticket_id))
.collect();
event_tickets.send_msg(
TicketSellerMessage::Add(new_tickets),
Some(ctx.myself().into()),
);
sender.map(|base_ref| {
base_ref
.try_tell(
EventResponse::EventCreated(Event {}),
Some(ctx.myself().into()),
)
.unwrap()
});
}
}
}
_ => (),
}
}
}
かなり実装は適当できっと動かないけれど、エントリポイントはこんな感じになるのかな。もう Rust 版 Akka にしか見えない。
use riker::actors::*;
use riker_sandbox::box_office::{BoxOfficeActor, BoxOfficeMessage};
fn main() {
let sys = ActorSystem::new().unwrap();
let box_office_actor = sys.actor_of::<BoxOfficeActor>("box-office").unwrap();
box_office_actor.tell(
BoxOfficeMessage::CreateEvent {
name: "event".to_string(),
tickets: 200,
},
None,
);
}
ActorRef
と BasicActorRef
の違いが使っている限りではよくわからなかったけれど、ドキュメントを読んだらだいたいわかった。
まず、それぞれの構造体のシグネチャは下記のようになっている。
#[derive(Clone)]
pub struct ActorRef<Msg: Message> {
pub cell: ExtendedCell<Msg>,
}
#[derive(Clone)]
pub struct BasicActorRef {
pub cell: ActorCell,
}
BasicActorRef
の方は、メッセージを tell
する際、そのメッセージの型はなんでも入れられる(メッセージは型付けされていないといえる)。一方で、ActorRef
の方はメッセージを tell
する際には、その Actor
で定義したメッセージの型しか入れることができない。
BasicActorRef
の tell
する関数のシグネチャを見てみると、
pub fn try_tell<Msg>(
&self,
msg: Msg,
sender: impl Into<Option<BasicActorRef>>,
) -> Result<(), ()>
where
Msg: Message + Send,
{
self.try_tell_any(&mut AnyMessage::new(msg, true), sender)
}
となっており、関数に使われているメッセージの型を関数の型引数としているだけで、内部では AnyMessage
という内容を生成している(内部で std::any::Any
を使用していい感じにしておいてくれる)。
std::any::Any
というのは動的型付けをエミュレートできるように用意された標準ライブラリの機能。Rust には Scala のような Any
型は組み込みでは存在しないが、この標準ライブラリを使用することで、実質それと似たようなことができる。
一方で、ActorRef
で tell
する関数のシグネチャを見てみると、
impl<T, M> Tell<T> for ActorRef<M>
where
T: Message + Into<M>,
M: Message,
{
fn tell(&self, msg: T, sender: Sender) {
self.send_msg(msg.into(), sender);
}
(...)
}
このように T: Message + Into<M>
となっており、実質 Actor
に紐づくメッセージの型を参照している。
どのように ActorRef
と Actor
のメッセージの型が一致させられているかと言うと、Context
という構造体で一気に紐付けされる。
pub struct Context<Msg: Message> {
pub myself: ActorRef<Msg>,
pub system: ActorSystem,
pub(crate) kernel: KernelRef,
}
Context
は、Actor
がもつ recv
という関数の引数として登場してくる。type Msg: Message
と recv
は具象型のアクターで実装が必要になる。
pub trait Actor: Send + 'static {
type Msg: Message;
fn pre_start(&mut self, ctx: &Context<Self::Msg>) {}
fn post_start(&mut self, ctx: &Context<Self::Msg>) {}
fn post_stop(&mut self) {}
fn supervisor_strategy(&self) -> Strategy {
Strategy::Restart
}
fn sys_recv(&mut self, ctx: &Context<Self::Msg>, msg: SystemMsg, sender: Sender) {}
fn recv(&mut self, ctx: &Context<Self::Msg>, msg: Self::Msg, sender: Sender);
}