Комментарии 121
На каких языках писали до того как начали использовать Rust. Сложен ли переход был? Что было сложнее всего или просто не привычнее?
Так же хочется отметить что на расте архитектура кода получается менее перегруженной, чем в той же java или c#. Но к этому нужно придти, потому что в начале я по привычке все равно городил сложные абстракции кода. Но со временем приходит понимание как нужно писать на расте. В итоге решения на расте как правило получаются более читаемые и более тестированные. Я не говорю что данные вещи возможны только на расте, просто на других языках как правило больше возможностей в плане решения задачи. В итоге прописываешь первое попавшиеся и идешь дальше. Хотя данное решение может быть не самым оптимальным. А в расте приходится в некоторых моментах задумываться.Хочешь ли ты кучу лайфтаймов прописать или можно решить как то задачу более изящным способом. Но как я выше сказал это мой опыт, и может я только писал столько не оптимальный код в других язык))) И по этому возможно для Вас разницы ни какой не будет, после перехода на раст(Ну кроме лайфтаймов)
Резюмируя, лично меня Rust привлекает целостной экосистемой языка, приемлемым уровнем производительности, удобством интеграции в уже существующие проекты. На нем действительно можно писать как большие системы, так и маленькие низкоуровневые блоки.
Переходил на Rust с Java, полтора года назад. Понадобилось использовать C++, но после Java управление внешними зависимостями в C++ показалось настоящим кошмаром. А так, в разное время приходилось программировать на C/C++, Python, PHP, JavaScript и Java.
Переход был достаточно сложен: основная трудность состояла в том, чтобы переучиться и привыкнуть следовать правилам заимствования и владения. Тяжело было чисто психологически, а не технически (ошибки раздражали), ибо компилятор всегда выдавал исчерпывающие сообщения об ошибках и предложения по исправлению. Период "борьбы с компилятором" продолжался несколько месяцев (!), все время, пока я осваивал язык. Но после этого языком стало пользоваться весьма комфортно.
Синтаксис Rust'а поначалу казался вырвиглазным, но, как ни странно, к нему довольно быстро привык и трудности в чтении чужого кода не испытываю.
В целом — я доволен Rust'ом и пытаюсь его больше использовать в своих проектах, причем в основном для прикладных целей. К сожалению, не всегда это возможно: из-за молодости языка заказчики бывают против его использования. Еще минус — некоторая сырость языка и экосистемы вокруг него все еще присутствует. С каждым новым релизом (раз в 6 недель) она постепенно преодолевается, и сейчас состояние намного лучше, чем было год назад.
1) Невозможно наследовать структуры данных. Да, я знаю про агрегацию и композицию, но это не всегда применимо. Возникала ли у вас подобная проблема, и если да, то как вы с ней справлялись?
2) При наследовании трейтов не сохраняются реализации функций. Т.е. я, скажем наследую В от А, в А есть реализация какой-то функции, но вот в В её уже не будет. Я читал много SO и доков и там есть решения, но это всё же немного не то. Были ли у вас проблемы с этим?
3) Более-менее сложная иерархия объектов выглядит довольно громоздко. Допустим, какой-нибудь простой оконный интерфейс, где есть отношение parent-child между виджетами, причём, дочерний виджет, допустим может хранить слабую ссылку на родительский, в некоторых случаях. Да, это возможно и реализуемо, но выглядит переусложнённым.
Я тоже пробовал писать на Rust, но столкнулся с проблемами, которые я описал выше, и для себя решил, что я это язык сугубо для системного программирования, и альтернатива скорее С, чем С++. Хотя вот вижу, что ребята переходят с C# и Java, в общем-то вполне себе ОО-языков для прикладного программирования, и меня начинают мучить сомнения… Мб я сильно где-то не прав?
1) Где вам нужно наследовать данные — агрегация всегда применима, разве нет? Другое дело, что если таким образом имитировать наследование в стиле ООП, то придется писать много дополнительного кода с пробросом вызова методов. Но так писать не надо ) Я стараюсь просто избегать раздувания структур и создания больших иерархий. Лучше создавать больше маленьких типов, активнее пользоваться типажами и средствами обобщенного программирования. Проблема с этим была только в самом начале, когда я пытался писать на Rust в стиле ООП.
2) Не уверен, что правильно понял ваш вопрос, но предположу, что после:
trait Foo {
fn foo(&self);
}
trait FooBar: Foo {
fn foobar(&self);
}
Вы ожидаете, что в FooBar
появится метод foo
. Это не так, потому что требование реализации : Foo
относится к тому типу, который будет реализовывать FooBar
, а не к самому типажу FooBar
. Строго говоря, вы не можете наследовать методы между типажами. Да и зачем это нужно? Необходимости в этом у меня не возникало.
3) Но часто ли вам действительно нужны такого рода иерархии? Причем самописные? Да, в их реализации есть определенные сложности (писал о них здесь), но в большинстве случаев я обхожусь использованием алгебраического типа данных.
Самое главное, наверное, это сразу освоить обобщенное программирование и научиться думать в терминах статических типов и их отдельных характеристик (типажей). И отойти от привычки смешивать наследование данных с образованием подтипа. К сожалению, пока нет хороших учебных материалов для этого, но ковыряние исходников популярных библиотек здорово помогает :)
2) Допустим я хочу использовать интерфейс IFoo, и его реализацию по умолчанию IFooBase. Ну и наследоваться от IFooBase, и только там, где мне надо реализовывать другое поведение, но не во всех классах.
3) Мне да, очень часто.
С обобщённым программированием я знаком (на плюсах в основном), что такое характеристики типов я понимаю. В плане наследования я с вами не совсем согласен, т.к. считаю, что образование подтипа, путём наследования данных — это вполне себе валидно. Популярные и хорошо написанные C++ библиотеки и использую и код смотрю. Думаю, что надо больше посмотреть для Rust.
Сдаётся мне, что пока я просто или не до конца понимаю как использовать Rust для прикладного программирования, или просто неправильно его использую.
2) Чем вас не устраивают методы трейтов с реализацией «по умолчанию»? Не нужно никакого FooBase, вы просто пишете:
trait Foo {
// обязательный к реализации метод
fn foo1(&self);
// метод с дефолтной реализацией на основе метода foo1
fn foo2(&self) { .. }
}
// типаж-расширение (extension trait) для которого все методы реализованы
// с использованием методов типажа Foo
trait Bar: Foo {
fn bar1(&self) { .. }
fn bar2(&self) { .. }
}
struct A;
// поведение foo2 можно изменить
impl Foo for A {
fn foo1(&self) { .. }
}
// здесь можно переписать поведение методов типажа Bar при необходимости
// стоит отметить что без этой строчки вы не сможете использовать методы
// типажа Bar
impl Bar for A {}
3) Это означает что ООП подход сильно повлиял на ваш стиль мышления (импринтинг), и что, как уже написали, вам нужно ломать свои привычки и учиться смотреть на задачи с другой стороны. Дело нелёгкое и для многих весьма неприятное, но в целом полезное.
2) Спасибо посмотрю, действительно ли это то, что я хочу.
3) Да, согласен, не помешает.
- А что, если структуры у вас разойдутся по полям? Почти всегда лучше выделить другую структуру обычным копи-пастом. Потому как сегодня общих полей — 2, а завтра — 3. Если просто так совпали звезды и структуры похожи, то это не повод переиспользовать их структуру, это можно быть дорогой в никуда (примерно, как наследовать
Point3D : Point2D { public int Z }
). В крайнем случае можно просто написать макрос в стиле "скопируй поля вот той структуры". Мерзковато, но возможно. - Первый трейт называется
Bar
, опечатка. - Ответил ниже.
HKT в Rust пока нет (
С помощью типажей можно нечто подобное сымитировать, но это ограниченный и малопонятный подход.
Обычно используется простое решение с Option
, но оно не даст статических гарантий отсутствия значения:
#[derive(PartialEq, PartialOrd, Debug)]
struct Struct {
name: String,
age: i32,
salary: i32,
dept: Option<String>,
smth_else: Option<i32>,
}
let reduced = Struct { name: "meh".to_string(), age: 20, salary: 42, dept: None, smth_else: None };
let full = Struct { name: "meh".to_string(), age: 20, salary: 42, dept: Some("IT".to_string()), smth_else: Some(10) };
Здесь reduced
и full
будут одного и того же типа, со всеми вытекающими.
Я бы на вашем месте попробовал написать какой-нибудь средней сложности проект на языке без ООП, и посмотреть, что из этого получится.
Последний раз, когда я такое сделал, я научился нормально пользоваться шарповыми лямбдами, ну и в целом поставил мозги на место. Возможно, вам тоже этот способ подойдет. Занимает такой проект ну пусть пару недель по вечерам, но зато вы не будете тратить месяца, используя инструмент, который возможно для задачи плохо подходит, просто потому, что другой вы не поняли.
В общем, с точки зрения образования оно того стоит. ООП это не панацея, просто набор практик, хороший, но не единственный. Тот же Expression Problem очень неприятен при расширении иерарахии. В итоге в ООП мире обычно все сводится к заучиванию паттерна визитор и все. А в языках с богатыми типами возможно вообще оптимальное решение задачи с помощью tagless final. А вот в ООП оно реализуется, увы, костыльно, хотя и не нереально. Тут подробнее.
Как ни странно, не только пройдёт проверку, но даже и не потребует явным образом импортировать Foo: следующий код выводит HERE
.
mod foo {
pub trait Foo {
fn foo(&self);
}
}
mod foobar {
use crate::foo::Foo;
pub trait FooBar : Foo {
fn foobar(&self) {
self.foo();
}
}
}
mod strct {
use crate::foo::Foo;
use crate::foobar::FooBar;
pub struct Struct {}
impl Foo for Struct {
fn foo(&self) {
println!("HERE");
}
}
impl FooBar for Struct {}
}
mod runfoo {
use crate::foobar::FooBar;
pub fn run_foo(s : &FooBar) {
s.foo();
}
}
fn main() {
let s = strct::Struct {};
runfoo::run_foo(&s);
}
Вот если у вас много foo
, то извольте и импортировать, и указать явно, какой вы хотите (playground):
mod foo {
pub trait Foo {
fn foo(&self);
}
}
mod foo2 {
pub trait Foo {
fn foo(&self);
}
}
mod foobar {
use crate::foo::Foo;
use crate::foo2::Foo as Foo2;
pub trait FooBar : Foo {
fn foobar(&self) {
self.foo();
}
}
pub trait FooAllBar : Foo + Foo2 {
fn foobar(&self) {
Foo2::foo(self);
}
}
}
mod strct {
use crate::foo::Foo;
use crate::foo2::Foo as Foo2;
use crate::foobar::FooBar;
use crate::foobar::FooAllBar;
pub struct Struct {}
impl Foo for Struct {
fn foo(&self) {
println!("HERE");
}
}
impl Foo2 for Struct {
fn foo(&self) {
println!("THERE");
}
}
impl FooBar for Struct {}
impl FooAllBar for Struct {}
}
mod runfoo {
use crate::foobar::FooBar;
pub fn run_foo(s : &FooBar) {
s.foo();
}
}
mod runfooall {
use crate::foobar::FooAllBar;
use crate::foo2::Foo;
pub fn run_foo(s : &FooAllBar) {
Foo::foo(s);
}
}
fn main() {
let s = strct::Struct {};
runfoo::run_foo(&s);
runfooall::run_foo(&s);
}
Тут есть небольшая проблема, если я правильно понимаю, &FooBar
привести к &Foo
не получится.
Например, если run_foo
вдруг захочет вызвать другую функцию, которой нужен &Foo
. Или вернуть &Foo
в составе какой-то структуры.
В первом случае ещё можно выкрутиться как-то так (ну или через newtype):
impl <'a> Foo for &'a FooBar {
fn foo(&self) {
FooBar::foo(*self)
}
}
А вот вернуть из функции &Foo
, если дан &FooBar
уже никак (впрочем, решается тривиально через добавление функции fn as_foo(&self) -> &Foo
на FooBar
).
После ООП языков с Rust достаточно тяжело. Привычные приёмы не работают, попытки писать в привычном стиле ведут к достаточно кривому и некрасивому коду. С Rust нужно переучиваться. Но, когда понимаешь, как здесь правильно писать, всё становится на порядок проще, код становится лаконичен, прекращается борьба с компилятором и вместо соперника он становится другом и на самом деле очень мощным инструментом, берущим на себя массу рутинной работы.
При наследовании трейтов не сохраняются реализации функций
На самом деле это не совсем так. Реализации, сделанные в трейтах — вполне наследуются, но это не то наследование к которому Вы могли привыкнуть в других языках.
Запись B: A
, выглядит несомненно как наследование, но, несмотря на сходство — это не оно. Это декларация ограничения — типаж B
может быть реализован только для тех типов, для которых реализован типаж A
. Соответственно типаж B
может расчитывать на методы типажа A
, но он не находится в привилегированном положении наследника (и таких типажей может быть реализовано несколько для одного типа), он не переопределяет реализации из другого типажа, а при вызове без уточнения, какую реализацию мы желаем вызвать, компилятор закономерно удивится. Привычное интуитивное "наследование" здесь не работает, хотя вначале кажется — вот оно, и пытаешься строить наследование на типажах :-)
В качестве замены наследованию всё же предлагается другая фича — специализация, которая выглядит уже ближе по смыслу и местами она очень нужна. Однако она ещё не стабилизирована и это долгая история.
Будет здорово, если вы напишите статью о переходе с ОО язков на Rust. Думаю, многие скажут вам спасибо, да и карму вам выше 4х поднять получится, а то сейчас хабр не даёт :)
Я планировал написать статью еще полгода назад, но все руки не доходят, про то, как выглядит написание телеграм-бота глазами C# разработчика (меня). Только я сейчас смотрю на все эти футуры в 3 экрана, и думаю, что лучше подождать релиза async/await, а то вместо привлечения людей только напугаю всех :)
Однако я в большей степени говорил о статье, построенной по принципу, который использовал Майерс в молодости, когда переучивал С разработчиков на C++: «ты делал так, а вот теперь надо вот так вот». Ну, а глубина пояснения почему надо так может быть в принципе любой.
И хотелось бы в этом году, а не через пять лет.
Я немного писал на С++ и Java, пробовал много других языков, но в конце концов (это произошло в начале 2017 года) начал писать на расте и с тех пор пишу только на нем. Не могу сказать, что с самим языком возникали какие-то особенные трудности — первая версия официальной книги позволяла начать писать простые программы и читать чужой код буквально через пару недель чтения по вечерам. Сейчас уже есть уже третье, если не ошибаюсь, издание этой книги — не читал новые версии, но беглое просматривание показывает, что все стало только лучше.
Все новички непременно сталкиваются с лайфтаймами — да, у меня тоже было время, когда ты рандомно расставляешь в коде неведомые закорючки и молишься, чтобы компилятор сжалился. Впрочем, это довольно быстро проходит после некоторой практики. В ежедневной разработке с лайфтаймами почти не сталкиваешься — в большинстве случаев они просто не нужны или IDE расставит их за тебя.
Читается чужой код на расте очень легко — этому способствует удобная система документации, строгий синтаксис с возможностью автоматического форматирования и статическая типизация. Советую поискать на гитхабе проекты из той сферы, которая вас больше всего привлекает — и попытаться решить какую-нибудь из issues. Во многих проектах даже ведутся специальные списки задач для новичков.
Русскоязычное сообщество (по крайней мере та часть, что находится за пределами Телеграма) — очень приветливое, тоже самое можно сказать и про международное сообщество. Ссылок к сожалению не дам — модераторы свирепствуют :)
P.S. freecoder_xx отличная статья, мне прям понравилось. Долго писал?
Главный минус у него ровно один — он сейчас мало где используется в продакшене, и у работодателей не слишком-то популярен (как говорит один популярный сайт, «There are no open rust jobs anywhere in the world. That doesn't sounds right...»), поэтому пока все мои проекты на Rust — личные.
Писал на Rust несколько проектов:
Самый первый проект на Rust: преобразователь UART в CAN на основе микроконтроллера STM32. До этого такие проекты у нас писались на C, хотя я лично их не писал.
Из плюсов:
- Уже были отличные библиотеки (
cortex-m*
) и несколько структур данных для асинхронного программирования (вида «получили в прерывании байты, записали в очередь, в основном цикле обработали, записали ответ в очередь на отправку, начали отправку, уснули»). Найти готовую реализацию очереди для микроконтроллеров на C можно. Подключить её нормально в качестве зависимости — нет. - Написать тесты, запускающиеся на хосте, с Rust легче.
- Отладка с gdb уже работает.
Из минусов:
- Экосистема только развивающаяся. Многое из готового не найти. Значительная часть проектов висели на одном гуру (кстати, часть из них смотрю перехали в аккаунт организации https://github.com/rust-embedded). Там, где я реализовывал собственно UART и CAN было несколько больше unsafe кода, чем нужно, прямые записи в регистры. Пытаться создавать безопасные абстракции, соответствующие духу языка, я не стал — мало времени и, к тому же, я отлично понимаю, что в первом проекте «как надо» не получится ни за что.
- Непривычные макросы. Я не могу просто взять и «как в C» обратиться к переменной, объявленной за пределами макроса, без передачи её каким‐либо явным образом. Здесь, с одной стороны, всегда видно, что код макроса зависит от переменной. С другой стороны, вызовы макросов становятся длиннее.
- Для того, чтобы установить бит в РСН на Rust нужно написать конструкцию вида
apb1.enr().modify(|_, w| w.i2c1().set_bit());
. Как видите, здесь пять(!) вызовов функций (.enr()
,.modify()
, вызов лямбды (внутри.modify()
),.i2c1()
,.set_bit()
). Компилятор это каким‐то образом оптимизирует во что‐то адекватное, но, во‐первых, выглядит для человека, писавшего на C, это дико. Во‐вторых, если вы попросите его не оптимизировать, то он не оптимизирует — при отладке такое сильно замедляет программу.
- Уже были отличные библиотеки (
Писал программу, проводящую функциональный контроль микросхемы. Раньше писал такое на LabVIEW. Точнее не всю программу, а только часть, собственно анализирующую результаты работы микросхемы.
Из плюсов:
- Rust может компилироваться в DLL, которую можно вызвать из LabVIEW.
- На Rust легко написать простой tcp сервер.
- На Rust можно быстрее писать сложные программы, чем на LabVIEW (кто бы сомневался).
- Опять же, тесты создать проще.
- Легко обойтись без аллокаций памяти в куче.
- Rust обрабатывает данные быстрее LabVIEW.
Из минусов:
- Rust при использовании через DLL роняет LabVIEW. И вообще, этот интерфейс в LabVIEW медленнее, чем я думал (но это, конечно, не проблема Rust). Падения я сначала записывал на паники вроде тех, что происходят при переполнении в арифметических операциях, но исправление замеченных ошибок и изменение операторов сложения/вычитания/умножения на подходящие по смыслу функции вроде
saturating_add
/wrapping_add
/… ничего не дали. После переписывание DLL в tcp сервер LabVIEW перестал падать, а сервер падать не стал. - Паники сложно перехватывать. Насколько я понял, перехват не всегда работает. Мой код перехвата, кажется, так ни разу и не заработал — хотя, возможно, паник просто не было, а падения вызывались чем‐то ещё.
- Немного повоевал с borrow checker когда решил вести в программе отдельный журнал, но при этом получать имя журнала из LabVIEW. А до получения имени — отправлять данные в stderr.
Писал программу анализа бинарных логов, созданных LabVIEW. До этого использовалась программа на Python.
Из плюсов:
- С поправкой на чтение документации (только третий проект, я ещё не всё запомнил!) пишется примерно так же быстро, как и на Python. В т.ч. можно так же легко создать цепочку вида файл→буфер (правда на Python буфер включён по‐умолчанию и должен явно отключаться, а тут он является отдельной сущностью и должен быть прописан явно)→потоковый распаковщик (xz или gz, по расширению) и передать её в качестве объекта
std::io::Read
в процедуры анализа. - При этом работает на два порядка быстрее, при том что ни оптимизацией программы на Python, ни оптимизацией программы на Rust я не занимался.
- Компилятор вылавливал те ошибки, которые на Python я бы не заметил до запуска анализа журнала с определёнными данными.
Из минусов:
- Вот здесь уже пришлось много воевать с компилятором за то, чтобы вернуть trait object (тот самый
std::io::Read
) из функции и потом передать в другую функцию. Основная проблема: не сразу понял, что нужно делать, когда компилятор мне говорит, что он не знает размер данных и по этому поводу не хочет ничего никуда передавать. - Лапша парсера и сериализатора результатов в человекочитаемую форму на Python всё же короче.
- С поправкой на чтение документации (только третий проект, я ещё не всё запомнил!) пишется примерно так же быстро, как и на Python. В т.ч. можно так же легко создать цепочку вида файл→буфер (правда на Python буфер включён по‐умолчанию и должен явно отключаться, а тут он является отдельной сущностью и должен быть прописан явно)→потоковый распаковщик (xz или gz, по расширению) и передать её в качестве объекта
В принципе, на Java я ООП использовал весьма ограничено. Предпочитал отделять данные от функций, их обрабатывающих, неизменяемые структуры данный, стиль с легким налетом функционального программирования, минимум наследования. С этой частью проблем особо не было при переходе на Rust.
С владением в Rust поначалу было непривычно, но процентов 80, наверное, освоил довольно быстро (простые случаи). Более глубокое понимание на уровне интуиции, наверное, выработалось где-то через полгода, особенно после разработки нескольких вариантов API типа reflection в Java (обобщенный обход данных заданных некоторой схемой).
Из необычного было то, что мы используем Rust совсем не как системный язык программирования, а для самого что ни на есть энтерпрайза, фактически как аналог Java / .NET. Поэтому, сложно сказать, какая часть трудностей из-за языка как такового, а какая — из-за особенностей его применения. Часть сложностей была, возможно, из-за недостатка опыта (у меня) в обоих областях: и в знании Rust, и в знании предметной области.
Приходилось проявлять, гм, смекалку. Даже, не побоюсь признаться, приходится заимствовать некоторые подходы из Java (что порой вызывает удивление коллег).
С точки зрения «разгона» команды, старт у нас был довольно трудным, на мой взгляд, — Rust довольно сложный язык, особенно как второй язык после JavaScript. Но постепенно ситуация исправляется, сейчас у нас примерно человек 8 пишущих активно на Rust. Плюс нам повезло нанять пару человек, имеющих активное участи в Rust экосистеме (в том числе и с позиции менторинга), поэтому я настроен оптимистично :)
У меня нет опыта разработки и поддержки больших систем на динамически типизированных языках (Java отнесём в категорию «условно статически-типизированных»), но пока что наш код на Rust переживает рефакторинги весьма успешно.
У точки с запятой, однако, есть и свои синтаксические преимущества. Так что в Rust это не просто бесполезный рудимент.
У mkpankov есть об этом вопросе заметка на rustycrate — https://rustycrate.ru/обучение/2017/06/11/oop-in-rust.html — и есть целая глава в растбуке — https://doc.rust-lang.org/book/second-edition/ch17-00-oop.html (перевод).
Добавлю к вышесказанному, что с одной стороны, элементы ООП всё же есть — со структурами можно работать как с объектами, типажи можно наследовать и даже есть типаж-объекты, но с другой стороны, отсутствие классов и наследования структур данных — это фича языка, введённая с целью избежать негативных практик ООП, в том числе чрезмерно разрастающихся иерархий классов, которые становится впоследствии дорого поддерживать и развивать. И на примере достаточно крупных проектов можно сказать, что это достаточно продуктивная модель.
Вообще, Rust — это очередной гибрид функционального и объектного подходов, с существенным перевесом в пользу функционального, но без возведения его в абсолют. Однако история о том, какой будет ООП сторона Rust, ещё не завершена, и соответствующие фичи и RFC потихоньку пилятся и эволюционируют.
на примере достаточно крупных проектов
А можно примеры? Прям чтоб чтот крупное. А то с ходу нагуглися только проект Мозиллы Servo, но это и понятно.
Примеры можно посмотреть здесь.
Я периодически мониторю реддит — вопросы о таковых всплывают регулярно, ответы — в стиле ниже, «откройте крупный проект и смотрите».
Ведь речь идет о языке системного уровня, я понимаю, на уровне языка уменьшать количество ошибок это уже круто, но производительность было до сих пор чуть ли не главным достоинством c/c++, чье место по уму собирается забирать Rust?
Одна из главных идей раста — так называемые zero-cost abstractions. Это значит, что почти каждая языковая абстракция никак не влияет на производительность. Результаты бенчмарков с другими языками найти достаточно легко — Раст настолько же быстр, насколько и С/C++
Так вот, там первым пунктом «no variadic templates». Можно ругать шаблонную магию C++, но zero-cost abstractions обеспечивает именно она. И пока я в расте не нашёл замены. Ну и остальное (constexpr, int-bool template parameters, etc.). Просто процитирую:
But to be honest it feels much more like Rust is targeting «reasonably fast, safe», then «extremely fast».
Конвенторов из чего во что?
Для Си есть, как минимум, два более-менее живых:
Волшебства не делают — на выходе дают очень неидиоматичный код, наполненный сишными типами, сырыми указателями и unsafe'ом — т.е. годится только как первый шаг в портировании кода на раст.
Можно тут почитать отчет о недавней попытке использовать для портирования библиотеки: https://wiki.alopex.li/PortingCToRust
На мой взгляд, сейчас очень сильна хайп волна от Golang, но на мой взгляд Rust гораздо лаконичней и перспективней. Вопрос, не повлияет ли все таки пресловутая хайп волна на популярность Rust.
Раньше были опасения, что Rust может сильно отстать от Go и Swift из-за отсутствия маркетинговой раскрутки. Но сейчас таких опасений нет, Rust хорошо развивается как язык, появляется все больше интересных проектов, его сообщество постоянно растет. Лично я считаю, что Rust — сильно недооцененная технология. Это "мина замедленного действия", "подрыв" которой — вопрос времени.
При чем тут хайп к Go?
Его создали одни из крутейших инженеров в Ай-Ти мире, реально как раз хайпа и маркетинга особого и нету, что ещё пока живы куча бессмысленных вещей типа Python в вебе.
Ну и Go это не системный язык, хоть и компилируется в нативный код. Он не прямой конкурент для Rust. Для Rust, как я понимаю, конкурент C++, так как C он тоже скорее всего не заменит.
В Ржавчине от функциональщины в целом только мощные конвейеры итераторов да неизменяемость переменных по умолчанию. Функции первого порядка есть, но из-за особенностей работы с памятью на практике с их помощью делать хитрые функциональные финты не очень-то просто.
Обычно говорят что раст императивный, но с вывертом: чистые функциональные языки стараются не допускать общего изменяемого состояния (shared mutable state), запрещая изменения, а Ржавчина запрещает именно одновременность общего и изменяемого состояния. Т.е. в один момент времени состояние может быть или общим (много &
указателей на переменную, например), или изменяемым, но с уникальным доступом (&mut
указатель требует эксклюзивности доступа). Т.е. цель сходная с чистой функциональщиной, но путь к ней другой из-за необходимости быть более низкоуровневым.
неизменяемость переменных по умолчанию
?! * в шоке *
А как мне значение переменной поменять не одну тысячу раз? O_O
Выделять под каждый новый экземпляр переменной новую дополнительную память?! * в ауте *
Скоро можно будет. RFC о юникодных идентификаторах приняли не так давно, срачей вокруг него куча была.
А IDE позволяет отличить русское «с» от латинской «c»?
И вроде, ещё японские и китайские иероглифы имеют разные кодировки в Юникоде при одинаковом внешнем виде. Японские иероглифы — это немного модифицированные в XX веке китайские (не считая японского слогового алфавита), так что большая часть иероглифов выглядит одинаково, но часть иероглифов отличается.
Спокойно, просто вместо let a = 5;
надо написать let mut a = 5;
и она станет изменяемой.
Про идентификаторы — я подозреваю что в серьезных международных проектах просто будет включено предупреждение об использовании не-ascii идентификаторов (#![forbid(non_ascii_idents)]
) и все.
Хорошо, можно делать изменяемые переменные.
И это не работает в миллион раз медленнее, чем С, по многим причинам. И да, это зачастую удобнее, чем мутировать переменную.
можно написать полезную программу без единой изменяемой переменной. И там даже массивы неизменяемые, чтобы добавить один элемент (или поменять его), логически копируется все элементы массива, с одним измененным
Поскольку на ФП, я никогда не писал, то спрошу (возможно глупые) вопросы:
Разве копирование не жрёт память? А в случае огромного массива разве затраты времени на копирование нулевые?
Если я собираюсь использовать Rust вместо C++, то у меня вполне реальна ситуация когда свыше сотни раз в секунду будут меняться значения как десятков переменных, так и десятков массивов (какой-нибудь 3D-меш в несколько сот полигонов — вполне себе массив).
Если всё это и так, без ФП, само по себе, жрёт под гигабайт памяти (вместе с текстурами и прочим), то мне как-то очень страшновато переменные копировать, а не изменять. Или в этом случае следует всё же забить на неизменяемость, и использовать изменяемые переменные и массивы?
И что со сборкой мусора в Rust? Как я понимаю, она имеется по причине наличия неизменяемых переменных, или нет?
И если она есть, то её можно отключать?
Уточню, PsyHaSTe говорил о функциональных языках, похожих на Haskell. Rust к ним не относится и не использует оптимизации применяемые функциональными языками при работе с неизменяемыми массивами. Использовать изменяемые массивы в Rust — нормальная практика.
И что со сборкой мусора в Rust?
Сборки мусора нет. В отличие от функциональных языков, где массив может быть реализован как ссылка на предыдущую неизменяемую версию массива + список изменений, массив в Rust — это просто непрерывная область памяти в которой лежат данные, так же как и в C++. Память, выделенная под массив, освобождается при выходе переменной из области видимости или явным вызовом drop()
, так же как и в C++.
Неизменяемость переменных по умолчанию мотивирует использование функционального подхода, и в большинстве случаев это действительно удобно, но если требуется, то можно использовать и императивный подход с изменением массивов in-place и т.п.
Повторю, никакой магии под капотом в Rust нет: если структура описана как два четырёхбайтовых целых числа, то в памяти будут лежать два четырёхбайтовых целых числа, никаких ссылок на предыдущее неизменяемое значение там не будет, поэтому и не требуется сборщик мусора для очистки неиспользуемых предыдущих значений.
Разве копирование не жрёт память? А в случае огромного массива разве затраты времени на копирование нулевые?
Иногда нулевые, иногда нет. Без бенчмарков ничего нельзя сказать, поэтому совет по-умолчанию: пишите максимально просто и понятно, а затем разворачивайте узкие места в более производительные конструкции.
Есть статья, где человек рассказывает, как он с тяжело оптимизированного сишного кода переписал на раст, и получил буст производительности в полтора раза. При этом он писал, насколько я знаю, максимально идеоматично, с неизменяемыми данными (по-возможности) и т.п., а тяжело оптимизированная сишка имела дело с кучей мутабельных буферов (для производительности). Он сам не ожидал таких результатов, но вот так вот вышло. Не забывайте: «Premature optimization is the root of all evil.» © Кнут, без бенчмарков нельзя что-то оптимизировать. Об этом и Фаулер писал, и еще кто-то. В чистом коде, по крайней мере, про это есть, насколько я помню.
И что со сборкой мусора в Rust? Как я понимаю, она имеется по причине наличия неизменяемых переменных, или нет?
И если она есть, то её можно отключать?
В расте нет GC, да и наличие GC ортогонально неизменяемым переменным. Возможно, когда-то в будущем добавят опционально для упрощения интеграции с браузерами, в частности JS, но это не скоро и полностью кастомизируемо.
с тяжело оптимизированного сишного кода переписал на раст, и получил буст производительности в полтора раза
Круто!
А в Rust можно использовать кириллические имена переменных и функций ?
Скоро можно будет. RFC о юникодных идентификаторах приняли не так давно, срачей вокруг него куча была.
Только работать с такими переменными будет неудобно.
Далеко не каждый терминал позволяет удобно вводить их с клавы — значит нужно будет каждый раз Copy+Paste делать. Еще хуже ситуация — вы на маке, коллега на винде. У вас чуть разное отображение шрифтов и разный ввод символов. Брррр.
Не понимаю, зачем из нормального языка делать 1С.
Стандартная библиотека языка остаётся ASCII-only и на английском. Авторы RFC советуют другим библиотекам поступать так же, если у них нет других соображений. Так что я бы не назвал это превращением Rust в 1С.
Другое дело, что сейчас хотят сделать юникодные идентификаторы по-дефолту (то есть не-юникодные символы надо будет явно запрещать, а не разрешать), с чем я не согласен, но видимо люди не знают, что кто-то может пользоваться клавиатурами, где нельзя набрать лямбду самостоятельно.
Не профессиональным программистам так удобнее. Вообще была попытка русифицировать Lua для торгового терминала OUIK — чтобы можно было писать код и на русском и латиницей https://тхаб.рф/wiki/LuaRu
Есть ещё очень профессионально сделанный 1Script http://oscript.io — активно развивается, есть сообщество поддержка MS Visual Studio Code
Ну а то что его на StackOverflow любят — ну так любить не значит жениться.
Короче, тяжело ему будет у работадателей зацепиться.Вакансий реально мало.
А С++ никуда не денется ближайшее время, потому что то, что можно было у него откусить, уже давно откушено Java и C#, а оставшаяся публика слишком консервативна. В нее никто не целится.
P.S. Не идеален, но работает сносно (у нас проект на ~100 KLOC строк кода). Некоторые функции работают очень медленно (например, «найди мне все реализации этого типажа»).
Есть плагин для IntelliJ IDEA :D Кстати сказать, официальный, от JetBrains (что намекает на перспективы языка). Конечно, до уровня поддержки Java пока далеко, но в целом — отлично работает, умеет почти все, что требуется в повседневной разработке (кроме дебага — для этого потребуется лицензионный CLion или старый-добрый gdb)
Также есть плагин для VS Code — он использует Language Server Protocol. Работает вроде неплохо, но пока что нестабильно и для по-настоящему больших проектов, пожалуй, не подходит.
Разумеется, есть и плагины под другие редакторы — статус поддержки можно посмотреть на https://areweideyet.com
И еще прикол-после 6 летней спячки проснулся создатель node.js, покаялся в ошибках(хотя кто их не делает ) и уже начал пиарить новый проект deno, а в нем он использует Rust (а хотел Go ), ну и до кучи там C и
Интересно, удастся ли полноценно заменить связку Python + numpy + matplotlib на Rust + что-то.
Для 2D и 3D графиков хороших нативных решений пока нет (и вероятно ещё достаточно долго не будет, т.к. это весьма нетривиальная задача). Большинство либо использует gnuplot, либо дёргает matplotlib через крейты вроде pyo3.
И видео соответствующее, рекомендую посмотреть.
https://youtu.be/n-txrCMvdms
Пример показывает, что тесты писать просто — нашлепнул на функцию #[test]
и готово, остальное встроено в стандартный cargo.
До JUnit в Java ещё пока не дотягивает, на мой взгляд. Например, не хватает возможности удобно писать data-driven тесты. Так, чтобы 1 файл с данными = 1 тест, например; с возможностью запуска индивидуальных тестов.
Какие-то движения вокруг custom test frameworks вроде происходят, но, к сожалению, нет времени, чтобы во всём этом разобраться.
Пока выкручиваюсь с proc macro, который сканирует файлы и создаёт #[test] функцию на каждый файл, выглядит примерно так (второй параметр — регулярное выражение для матча на «основной» файл теста, остальные параметры — шаблоны-подстановки по этому регулярному выражению):
// запустится с A.input.txt / A.output.txt, B.input.txt / B.output.txt, и.т.д.
#[datatest("tests/test-cases", r"^(.*)\.input\.txt", r"$1.output.txt")]
fn sample(input: String, output: String) {
assert_eq!(format!("Hello, {}!", input), output);
}
Можно, конечно, сделать так, чтобы все эти тесты в одной #[test] функции выполнялись, но тогда очень неудобно с этими тестами работать. Например, если делается рефакторинг и половина тестов поломана — хочется возможности запустить один тест, чтобы начать с чего-то мелкого. Плюс, хочется видеть все поломки, а не первую. И так далее.
Может быть, можно как-то через кишки модуля test выкрутиться?..
P.S. Похоже, можно попробовать через «harness = false» в Cargo.toml. Пора снова переписывать фреймворк :D
Согласен, очень не хватает параметризованных тестов. Приходится извращаться в стиле
#[cfg(test)]
mod tests {
use super::*;
macro_rules! generate_test {
($name:ident, $c:expr, $a:expr, $b:expr, $result:expr) => {
#[test]
fn $name() {
assert_eq!(are_concatable_strings($a, $b, $c), $result);
}
}
}
generate_test!(a, "javascript", "javpt", "ascri", true);
generate_test!(b, "javascript", "jasrit", "vacp", true);
generate_test!(c, "javascript", "java", "scripts", false);
generate_test!(d, "javascript", "jav", "script", false);
generate_test!(e, "java not script", "java ", "not script", true);
generate_test!(f, "javascript", "java ", "scritp", false);
generate_test!(g, "Java1Java3Java2Java4", "Java2Java4", "Java1Java3", true);
generate_test!(h, "jjaaamaaat", "jaaam", "jaaat", false);
}
В таком простом случае не ясно, зачем вам несколько тестовых функций, когда можно обойтись всего одной. Тут ведь проверяется корректность всего одной функции are_concatable_strings(), принимающей короткие значения:
#[test]
fn strings_concat_fine() {
assert!(are_concatable_strings("javascript", "javpt", "ascri"));
assert!(!are_concatable_strings("javascript", "java", "scripts"));
// ...
}
Потому что в таком случае если тесты не пройдут я сразу пойму, какие условия не выполнены
running 9 tests
test tests::a ... ok
test tests::b ... ok
test tests::c ... ok
test tests::d ... ok
test tests::e ... ok
test tests::f ... ok
test tests::g ... ok
test tests::h ... FAILED
test tests::m ... FAILED
failures:
---- tests::h stdout ----
thread 'tests::h' panicked at 'assertion failed: `(left == right)`
left: `false`,
right: `true`', src/lib.rs:61:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
---- tests::m stdout ----
thread 'tests::m' panicked at 'assertion failed: `(left == right)`
left: `false`,
right: `true`', src/lib.rs:62:5
failures:
tests::h
tests::m
test result: FAILED. 7 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out
А в этом случае просто будет "Отвалилось на 10 строчке" и ищи свищи. Причем непонятно, 11 строчка и дальше — работают или нет.
"Отвалилось на 10 строчке" и ищи свищи
Не только. Еще будет распечатано содержимое assert'а (если использовался обычный assert).
Не только. Еще будет распечатано содержимое assert'а (если использовался обычный assert).
Ага, как в примере выше :)
thread 'tests::h' panicked at 'assertion failed: `(left == right)`
left: `false`,
right: `true`', src/lib.rs:61:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
Ну и это не отменяет того, что я все еще остановлюсь на первой же ошибке. тогда как множество всех отвалившихся тестов помогает понять, что пошло не так.
Но за это я раст и люблю, что в нем есть макросы, которые позволяют решить все раздражающие моменты. Нужен сахар, вот тебе макрос, пиши какой хочешь)))
10 неочевидных преимуществ использования Rust