Если б миром правил краб бы —
правил если б миром краб б,
я б сказал бы, что пора бы
снаряжать большой корабль,
отправляться б за моря бы
посоветовал тогда б
я б — непросто жить у краба
под клешней, как слабый раб.
Вскрытие показало, что я немного отстал от жизни, и язык программирования «Кровожадный краборжав» уже вполне себе пригоден для написания простеньких хелоуворлдов…
Ладно. В кои-то веки обойдусь без ёрничанья. Официально заявляю: я написал свою первую библиотеку на расте и мне понравилось. Раст — несомненно местами красивый и приятный для работы язык. Написание кода укладывается в зелёный диапазон плотности wtf/sec, а инструментарий заслуживает всяческих похвал (кроме кросс-публикации документации на https://docs.rs/, которая в 2025 году занимает час — хоть донаты шли, ей-богу).
Итак, я написал библиотеку, которая позволит эрлангистам проще вкатываться в раст. Акторная модель притворяется краденой из эрланга, с примитивами GenServer и GenStatem, с деревьями супервизоров, с боксированными сообщениями, мэйлбоксами, и привычной терминологией. Библиотека названа joerl, светлой памяти Джо Армстронга, с которым мне посчастливилось быть знакомым, и который сильнейшим образом повлиял на менталитет разработчика во мне.
Но довольно болтовни!
Результат бенчмарков:
• Actor spawn: ~6.15 µs per spawn
• GenServer calls: ~85 µs for 10 synchronous calls
• GenServer casts: ~6.16 ms for 10 async casts (including wait)
• GenStatem transitions: ~156 µs for 10 state transitions (20 events)
• Supervisor creation: ~11-12 ms for 5-20 children
Неплохо!
Примеры использования можно найти в репе https://github.com/am-kantox/joerl/tree/main/joerl/examples — покажу тут, чисто для затравки, самый простой пример — счётчик инкапсулированный в процесс.
// стейт «процесса» (рарутины)
struct Counter {
count: i32,
}
// имплементация трейта «примитивный процесс» (рарутина)
#[async_trait]
impl Actor for Counter {
async fn started(&mut self, ctx: &mut ActorContext) {
println!("Counter actor started with pid {}", ctx.pid());
}
async fn handle_message(&mut self, msg: Message, ctx: &mut ActorContext) {
if let Some(cmd) = msg.downcast_ref::<&str>() {
match *cmd {
"increment" => {
self.count += 1;
println!("[{}] Count incremented to: {}", ctx.pid(), self.count);
}
"decrement" => {
self.count -= 1;
println!("[{}] Count decremented to: {}", ctx.pid(), self.count);
}
"get" => {
println!("[{}] Current count: {}", ctx.pid(), self.count);
}
"stop" => {
println!("[{}] Stopping counter", ctx.pid());
ctx.stop(joerl::ExitReason::Normal);
}
_ => {
println!("[{}] Unknown command: {}", ctx.pid(), cmd);
}
}
}
}
async fn stopped(&mut self, reason: &joerl::ExitReason, ctx: &mut ActorContext) {
println!("[{}] Counter stopped with reason: {}", ctx.pid(), reason);
}
}
Пора запускать и проверять:
#[tokio::main]
async fn main() {
println!("=== Counter Actor Example ===\n");
let system = ActorSystem::new();
let counter = system.spawn(Counter { count: 0 });
// Send some messages
counter.send(Box::new("increment")).await.unwrap();
counter.send(Box::new("increment")).await.unwrap();
counter.send(Box::new("increment")).await.unwrap();
counter.send(Box::new("get")).await.unwrap();
counter.send(Box::new("decrement")).await.unwrap();
counter.send(Box::new("get")).await.unwrap();
counter.send(Box::new("stop")).await.unwrap();
// Wait for actor to process messages
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
println!("\nExample completed!");
}
Давайте посмотрим, что там:
❯ cargo run --example counter
Compiling joerl v0.1.0 (/opt/Proyectos/Rust/joerl)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/examples/counter`
=== Counter Actor Example ===
Counter actor started with pid <0.1.0>
[<0.1.0>] Count incremented to: 1
[<0.1.0>] Count incremented to: 2
[<0.1.0>] Count incremented to: 3
[<0.1.0>] Current count: 3
[<0.1.0>] Count decremented to: 2
[<0.1.0>] Current count: 2
[<0.1.0>] Stopping counter
[<0.1.0>] Counter stopped with reason: normal
Example completed!
Вот, как-то так.
Итоги: я не писал ничего серьёзного на расте до сегодняшнего дня. Имплементация библиотеки заняла у меня часов восемь довольно напряженной работы. Помогло то, что я досконально знал, как это реализовано в эрланге, поэтому по сути не сочинял код с нуля, а переводил свои знания из головы в редактор. Документация и бо́льшая часть тестов написаны моим стажёром Т1009 (это контаминация терминатора и Т9).
Если без фанатизма, присущего, почему-то, адептам раста, — язык мне понравился. Я интуитивно понимал, что гуглить, когда спотыкался о незнакомый синтаксис (а чаще — просто догадывался). Местами (в тестах и бенчмарках, не в основном коде, конечно) я плевал на понятия и просто писал говнокод, если утыкался в непонятные мне по неопытности ворнинги:
#[derive(Message)]
#[rtype(result = "()")]
pub struct PingMsg(#[allow(dead_code)] pub usize);
Директив слишком много, должно быть возможно проще, но мне уже лень. Ну и, напоследок, конечно битва за пояс племени якодзун с Актиксом.
1. Actor Spawn Time
◦ joerl: 6.04 µs
◦ actix: 9.56 µs
◦ Winner: joerl is ~37% faster ✓
2. Message Send (10 messages)
◦ joerl: 3.11 ms
◦ actix: 3.48 ms
◦ Winner: joerl is ~11% faster ✓
3. Message Send (100 messages)
◦ joerl: 3.13 ms
◦ actix: 3.51 ms
◦ Winner: joerl is ~11% faster ✓
4. Message Send (1000 messages)
◦ joerl: 3.44 ms
◦ actix: 3.71 ms
◦ Winner: joerl is ~7% faster ✓
5. Throughput (1000 messages)
◦ joerl: 11.86 ms
◦ actix: 11.64 ms
◦ Winner: actix is ~2% faster ✗
6. Multiple Actors (10 actors)
◦ joerl: 6.18 ms
◦ actix: 6.57 ms
◦ Winner: joerl is ~6% faster ✓
7. Multiple Actors (50 actors)
◦ joerl: 6.23 ms
◦ actix: 6.64 ms
◦ Winner: joerl is ~6% faster ✓
После этого мне язык понравился еще больше.
Документация: https://docs.rs/joerl/latest/joerl/
Еще раз вставлю сюда КДПВ, уж очень она мне пришлась по душе.

Комментарии (30)

voidptr0
05.12.2025 18:00Век живи - век учи новые языки. Мне вот понадобился недавно язык с goto. Искал-искал, а нашел, к своему стыду, FreeBasic. И залип. А в нем и goto и классы и namespaces и Clib и еще куча. Всего. Понятно, что писать на нем ничего серьезного не планирую за пределами текущей задачи, но очень впечатлило.

cupraer Автор
05.12.2025 18:00Я не «учу» новые языки, я просто взял поиграться еще один из хайповых. Тут как с человеческими языками: когда свободно говоришь на полутора десятках из всех возможных языковых групп — на уровень C1 в еще одном нужно пару дней.
понадобился недавно язык с goto
А что не так с си и паскалем?

Lewigh
05.12.2025 18:00Мне вот понадобился недавно язык с goto. Искал-искал
C# язык в котором есть практически все, в том числе goto

domix32
05.12.2025 18:00Выглядит довольно неплохо, хотя к содержимому репы вопросы.
Ссылка на примеры из статьи ведёт на 404.
-
По канону в расте можно завести папочки example, tests и benchmarks, чтобы складировать туда соответствующие компоненты и делать штуки типа
cargo run --example counter cargo test cargo bench не очень понятно зачем боксировать строки, когда можно завести обычный enum - благодатный и довольно маленький тип, представляющий из себя почти честные алгебраические типы, ещё и опечататься не даст. Ну, а если хочется по сети строку передавать, то просто делать сериализацию каким-нибудь serde
enum Commands { Increment(n: i32), Get, Stop, }; /* */ match *cmd { Increment(n) => { self.counter += n; log(ctx, "Count changed {} to {}", n, self.count ); } Get => log(ctx, "Current count {}", self.count), Stop => { log(ctx, "Stopping counter"); ctx.stop(ExitReason::Normal); } }
cupraer Автор
05.12.2025 18:00к содержимому репы вопросы
Ватсон просто без трубки уже не может, я прохожу уровень «макросы», поэтому репа уже триста раз модифицировалась, ща поправлю. Папочки есть, просто они отъехали в подпапочку.
зачем боксировать строки
Так там теоретически может быть
pub type Message = Box<dyn Any + Send + 'static>;Строки — это же пример просто.

Dhwtj
05.12.2025 18:00Эх
Я помню, что ещё летом обещал переписать что-то интересное с эрланг/эликсир на раст. Но времени нет совсем, сорян.

cupraer Автор
05.12.2025 18:00Да я вон за неполный трудовой день основную (козырную) часть эрланга переписал. И даже конечные автоматы снова по уму сделал: https://github.com/am-kantox/joerl/blob/main/joerl/examples/document_workflow.rs#L35-L50

rivo
05.12.2025 18:00Итак, я написал библиотеку, которая позволит эрлангистам проще вкатываться в раст.
Так оно не страшно, самое неудобное это async. Обмазывать код Send/arc. Когда лайфтам инференсом не выводит - боль. Ну и библиотеки внешние должны быть с поддержкой async. Еще не придумали как одноцветные функуции сделать .
Обработка ошибок отдельная тема, её прям со старта планировать надо.Акторная модель притворяется краденой из эрланга, с примитивами GenServer и GenStatem, с деревьями супервизоров
Насколько понимаю, отбрабатывать panic/unwrap сейчас невозможно и сервер схлопывается. Интересно посмотреть, как может развиваться remote_actor. Rust/tokio склоняют делать один большой монолит.

cupraer Автор
05.12.2025 18:00Еще не придумали как одноцветные функуции сделать.
Кто не придумал? Что значит, «сделать»?
её [обработку ошибок] прям со старта планировать надо.
Кому надо? Зачем надо?
отбрабатывать panic/unwrap
С паникой я разберусь, конечно.
интересно посмотреть, как может развиваться remote_actor
Куда развиваться? Зачем ему развиваться? Вспоминается КВН: «—Слышал, Бузова развелась? — А она что, развивалась?»

rivo
05.12.2025 18:00Еще не придумали как одноцветные функуции сделать.
Кто не придумал? Что значит, «сделать»?Имеется ввиду function coloring. Нельзя подключить библиотеку которую написали в синхронном стиле и наоборот. Есть даже отдельная реализация стандартной библиотеки async-std. В Erlang нет такого разделения, просто пишешь линейный код и не заморачиваешься.
её [обработку ошибок] прям со старта планировать надо.
Кому надо? Зачем надо?Ну как пользователь свои типы ошибок прокинет, если разрешается использовать только ActorError, то окай.
интересно посмотреть, как может развиваться remote_actor
Куда развиваться? Зачем ему развиваться? Вспоминается КВН: «—Слышал, Бузова развелась? — А она что, развивалась?»1. Transport Layer
2. Serialization
3. Node Discovery
4. Location Transparency
5. Fault Tolerance
6. Performance Optimizations
Этож эпичекий обьем работы, целая экосистема, а не простой crate.

cupraer Автор
05.12.2025 18:00Я знаю, в принципе, что такое function coloring, но передо мной именно эта проблема не стоит, от имплементации акторов ничкего не зависит, насколько я могу судить.
свои типы ошибок
Зачем? Кроме того, сделать
ActorErrorпоумнее можно в любой момент.эпичекий обьем работы, целая экосистема, а не простой crate
И да, и нет.
Node Discovery и Serialization я делать [пока] не буду, потому что это и в OTP сделано через жопу, и люди через одного переписывают
epmd.Location Transparency, Transport Layer — я знаю, как сделать, и скоро сделаю.
Fault Tolerance уже есть.
Performance — ну это вообще сейчас тридцать пятая проблема же.

cupraer Автор
05.12.2025 18:00Панику я отработал, как в эрланге, fwiw.

rivo
05.12.2025 18:00:thumbup:
У меня устарешвая информация, catch_unwind уже стабилизировали. Cloudflare почему-то свои unwrap не ловят , наверное тоже не знают.

cupraer Автор
05.12.2025 18:00У меня было сходное ощущение, когда я завернул на PR
Enum.sum/2в эликсире с комментарием «галлюцинации ллмки надо проверять, нет такой функции, используйтеEnum.reduce/3», — а выяснилось, что с тех пор, как я в последний раз заглядывал в документацию, — многое изменилось :)

mayorovp
05.12.2025 18:00Cloudflare почему-то свои unwrap не ловят , наверное тоже не знают.
Проблема-то не в том, чтобы поймать панику, проблема в том, чтобы поймать её в правильном месте.

sergey-gornostaev
05.12.2025 18:00У Erlang/OTP есть огромное преимущество, которое пока смогла повторить только Akka - простая и удобная кластеризация.

cupraer Автор
05.12.2025 18:00У Erlang/OTP есть огромное преимущество […]
Не может быть!
В примерах есть
distributed_chat.rs, который уже готов к кластеру. Писать свойepmdя не [пока] собираюсь, потому что я и для OTP писал свой, а значит — нужен только протокол, дальше — сами. Всё остальное готово и работает (кроме прозрачной трансляции пидов, но это на три часа работы и в приоритете).

sdramare
05.12.2025 18:00А чем это отличается от https://github.com/actix/actix? Ну кроме того что зачем-то убрали строгую типизацию сообщений акторов и сделали все на даункастах, что довольно спорно с точки зрения идиоматичного раста

cupraer Автор
05.12.2025 18:00Названием.

sdramare
05.12.2025 18:00Ну я это серьезно спрашивал, может какие-то идеи свежие есть, концепции, может он для эмбеддет подходит или еще что-то. Потому что акторных фреймворков на расте уже примерно 300 штук написано и действительно интересно зачем нужен 301-й

cupraer Автор
05.12.2025 18:00Вы это серьёзно спрашивали в комментарии к тексту, в котором есть вот такой абзац:
Итак, я написал библиотеку, которая позволит эрлангистам проще вкатываться в раст. Акторная модель притворяется краденой из эрланга, с примитивами
GenServerиGenStatem, с деревьями супервизоров, с боксированными сообщениями, мэйлбоксами, и привычной терминологией.Actix и его меньшие собраться написаны людьми, которые никогда в глаза не видели правильно реализованной акторной модели. Линки, мониторы, trap exists, cleanup on exit guarantees, деревья супервизоров, бесплатная кластеризация (не доделано, но там три часа разработки осталось), абстракции
gen_serverиgen_statem(!), корректная отработка паники в акторах, наконец. Да, и динамические сообщения, потому что акторы должны уметь хорошо работать в коллективе, где не всё написано на расте.Вы спрашивали про свежие идеи? — Их нет. Всё это 40 лет назад придумал Джо, в честь которого и названа библиотека. Как и во многом другом, растаманам катастрофически не хватило эрудиции, и они изобрели свой велосипед, кривой и с одной педалью. Я постарался этот изъян поправить, но, как мы видим, вы даже не понимаете, в чем разница (тут просто: если не понимаете, вам противопоказано пользоваться акторной моделью в принципе).

domix32
05.12.2025 18:00вы даже не понимаете, в чем разница
так он буквально с этого вопроса и начал

cupraer Автор
05.12.2025 18:00В тот момент я еще надеялся на лучшее, и даже продублировал ответ на этот вопрос, скопипастив его из текста выше.
Там был вопрос об отличиях
actixот моей реализации, мой последний пассаж — про непонимание, в чем отличиеactixот эрланговской реализации. Это разные уровни непонимания.
wilcot
Спасибо за статью! У меня появилось желание изучить тему акторов поподробнее, выглядит интересно.
Посмотрел результаты и код бенчмарка. Подозреваю, что бенчмарки все-таки протестировали время инициализации runtime (tokio vs actix). Выглядит так, что 10, 100, 1000 сообщений не достаточно для того, чтобы увидеть заметный результат.
cupraer Автор
Да бенчмарки никогда ничего из реального мира эмулировать не могут, только показать бинарное значение «на глаз»: годится / надо доработать. Меня результат устроил. Отдохну и допишу бенчмарки на миллионах сообщений, но сомневаюсь, что что-то прям изменится: узким звеном акторной модели были есть и будут хендлеры сообщений (читай: mailbox).
Не очень понял. Actix не переизобретал велосипед, там тоже токио под капотом.
wilcot
Все-таки на маленьких значениях не видно разницы, т.к. в результатах не видно кратного увеличения времени при кратном увеличении количества сообщений. Если добавить побольше вариантов (10k, 100k, 1kk, 10kk), то вырисовывается другая картина:
Это не критика библиотеки, скорее замечение к тому, что бенчмарк не показательный получился, т.к. значения по большей степени константные получились. А уже на таких результатах можно по flamegraph найти горячие места и пооптимизировать.
cupraer Автор
Спасибо!
Но еще раз, это не разница вообще. Сообщения шлют, чтобы что-то сделать, поэтому стерильный тест в вакууме на пустых сообщениях не означает вообще ничего. В реальном мире надо тестировать не газиллионы пустых сообщений, а тысячи непустых. Я сделаю, когда руки дойдут.
wilcot
Да, я с вами полностью согласен, что здесь скорее бенчмарк в вакууме, чем тест реальной производительности. НО, я подчеркну, что бенчмарк все-таки хоть что-то должен показывать. Когда он показывает ~3 мс для 10, 100, 1000 вызовов функции, стоит задуматься, нужен ли такой бенчмарк. А когда он показывает 3 мс, 30 мс и 300 мс (если линейная асимптотика), то с этим уже проще работать, так как можно по flamegraph искать горячие точки и делать оптимизации константы или даже асимптотики.
cupraer Автор
В данной ситуации 3мс — это хорошее число. Оно сопоставимо с проверенной в боях библиотекой, и оно «правдоподобно». Этого на данной стадии достаточно.
Конечно, это не нужно называть «бенчмарк», это нужно называть «PoC», но смысл от этого не меняется; кроме того, мне понравилось, что мой код без единого онапиливания показывает результат не хуже асинкса, и я, конечно, похвастался :)