Как известно, html-разработчики скучают по многопоточке. Читая на днях шикарную книгу Mara Bos oб "атомиках и локах", наткнулся на упоминание одного интересного языка программирования: rust lang. Согласно интернетам, learning curve раста стремится к бесконечности, но для многопоточного программирования выбор кажется нормуль. Тут вопрос - можно и, главное, удобно ли юзать раст для бизнес-логики (ну то есть для продакшна).

Краткое содержание: макросы, компилятор-враг, компилятор-друг, unsafe и miri, многопоточка, options, iters, match.

Будем честны, фичи тестировались на учебном проекте, но как бывает со всеми пет прожектами (учебными проектами), переписывались они десятки раз.

Макросы

Проектирование какой-никакой бизнес-логики обычно делается с небольшим заделом на прекрасное будущее. Добавление новых фичей происходит, если сильно утрировать, путем наследования, копипасты, расширением глобальной хеш-таблицы или "добавь вот тут еще" парочку if-elif конструкций. В целом, тут как бы про баланс между переиспользованием и копипастой. Eсли вам нужно от чего-то отнаследоваться, наверно, лучше заюзать емкую dsl конструкцию. Функциональный смысл такой же, но из-за локальности и несвязности выглядит лучше. Например, макрос tree!

let expr = tree![
    | BracketLeft, Expr, BracketRight, Term
    | Number, Term
    | Never
];

let term = tree![
    | Operator, Expr
];

На выходе два дерева, которые матчат, например, строку инпута 5+5+5 или (2+2)*2. Движок чекает первый столбец макроса, и если матч - продолжает по выбранному ряду. Составные токены (например, Expr или Term) раскрываются в еще одно дерево. Чуть сложнее использование макроса:

// grammar for javascript object literals

let map = HashMap::from([
	(Expr, tree![
	    | CurlyBracketLeft, Object, CurlyBracketRight
	    | SquareBracketLeft, Array, SquareBracketRight
	    | OtherExpr
	]),
	(Object, tree![
	    | Variable, Value
	    | String, Value
	    | SquareBracketLeft, Variable, SquareBracketRight, Value
	]),
	(Value, tree![
	    | Colon, Expr, Term
	    | Never
	]),
	(Term, tree![
	    | Comma, Object
	]),
]);

Код макросов кажется запутанным, но потом привыкаешь :). Тут матчимся на vertical bar и наполняем дерево токенами ряд за рядом.

macro_rules! tree {
    ($(| $($x:ident),+ $(,)?)+) => {
        {
            let mut tt = TokenTree::new();
            $(
                let v = vec![$(Token::$x,)*];
                tt.add_right(v[0]);
                for x in v.into_iter().skip(1) {
                    tt.add_left(x, Token::Never);
                }
            )*
            tt
        }
    }
}

Еще один пример compile time макроса

macro_rules! _reverse_vec {
    ([$first:ident $($rest:ident)*] $($acc:ident)*) => {
        _reverse_vec!([$($rest)*] $first $($acc)*)
    };
    ([] $($x:ident)*) => {
        vec![$(Token::$x,)*]
    }
}

Добрый компилятор, злой компилятор

При использовании entry апи для хеш-таблиц компилятор поначалу любит поругаться, особенно на js разработчиков, которые юзают обьекты (они же мапы) почти везде:

// javascript code for if-elif construction

const x = ({
	plus: func,
	minus: func2,
}[operator] || defFunc)(operand, operand2);

Entry апи принимает лямбду на модификацию объекта:

map.entry("key")
   .and_modify(|e| { *e += 1 })
   .or_insert(42); 

В лямбду выше хочется запихнуть побольше кода, как в питоне или джаваскрипте, но в расте компилятор скорее всего будет не доволен и очень сильно порежет диапазон возможных действий, так как аргумент передается по мутабельной ссылке. На самом деле, компилятор помогает, ну или хочет помочь. Сообщения об ошибках очень хороши. Немного времени, и вы уже будете скучать по компилятору раста в других языках программирования. В интернетах кто-то сказал, что pair programming в расте бесплатно - так и есть. Affine types (система типов) позволяет (скорее заставляет, но это тонкости) взглянуть на структуру кода немного под другим углом, пока, честно говоря, не ясно, это хорошо или плохо, но что-то новое это верное направление.

Rc & Arc

На всякий случай, стандартная библиотека раста имеет шикарный набор примитивов для работы в многопоточной среде.

Наш тестовый проектик парсит файлы джаваскрипта, в принципе, используя только следующую структурку:

type Choice = Option<Word>;

#[derive(Default, PartialEq, PartialOrd, Debug, Clone)]
struct Word(Token, Rc<Choice>, Rc<Choice>);

Логика, в двух словах, - мы двигаемся влево (выбираем второй элемент структурки Word), если токен инпута совпадает с ожидаемым токеном, в противном - двигаемся вправо (третий элемент структурки Word). Тип Rc (reference сounted) здесь нужен для хранения нашей структурки Word сразу в нескольких местах (например, для итерации по дереву и матчинга в хеш-таблице).

Скорее всего, мы захотим парсить файлики параллельно. В расте есть прекрасная библиотека Rayon, которая как бы создана для этих целей. То есть у нас есть список файлов, и вместо input.iter() мы пишем par_iter() -> профит (почти).

use rayon::prelude::*;

fn par(input: Vec<PathBuf>) {
    let tt = token_tree();
    input
        .par_iter()
        .map(|path| {
            let str = fs::read_to_string(path).expect("Unable to read file");
            let data = str.bytes().peekable();
            let v = tokens(data).into_iter().rev().collect();
            parse(v, &tt)
        })
        .collect()
}

Не забываем заменить тип Rc на Arc (Atomically Reference Counted) в структурке выше, и система работает в многопоточной среде. Профит.

unsafe и miri

Юзать умные указатели (rc, arc) для иммутабельных структурок, наверно, оверхед, иногда обычных указателей достаточно. В расте чтение указателя - unsafe операция, и лучше, честно говоря, держаться подальше от unsafe блоков. В интернетах существует замечательная книга "Learn Rust With Entirely Too Many Linked Lists" которая поможет сделать жизнь с unsafe немного проще. В итоге, мы заменяем Arc на обычные указатели, не теряя возможности парсить файлики параллельно:

use std::ptr::NonNull;
use std::marker::PhantomData;

pub struct Tree<T> { /* omitted */ }

unsafe impl<T: Send> Send for Tree<T> {}
unsafe impl<T: Sync> Sync for Tree<T> {}

type Link<T> = Option<NonNull<Node<T>>>;

struct Node<T> {
    elem: T,
    left: Link<T>,
    right: Link<T>,
}

#[derive(Default)]
pub struct Cursor<'a, T> {
    current: Link<T>,
    _foo: PhantomData<&'a T>, 
}

impl<'a, T> Cursor<'a, T> {
    pub fn get(&self) -> Option<&'a T> {
        unsafe {
            self.current.map(|node| &(*node.as_ptr()).elem)
        }
    }

    pub fn left(&mut self) {
        unsafe {
            if let Some(node) = self.current.take() {
                self.current = (*node.as_ptr()).left;
            }
        }
    }

    pub fn right(&mut self) {
        unsafe {
            if let Some(node) = self.current.take() {
                self.current = (*node.as_ptr()).right;
            }
        }
    }
}

Тут структурка Tree (представляет дерево грамматик) шарится между потоками, а Cursor (наш указатель) не шарится. Другими словами, Cursor создается в скоупе потока исполнения, а вот дерево одно на все потоки и должно быть иммутабельным. PhantomData - это так называемый маркер, который интуитивно показывает, каким типом данных владеет данная структурка (полезно при работе с указателями). Вроде работает, но unsafe код как бы не проверяется компилятором, а мы уже решили, что компилятор друг. Тут на помощь приходит проект Miri (An experimental interpreter for Rust's mid-level intermediate representation). Чуваки как бы пытаются проверить unsafe код, и вроде как у них что-то получается. Miri говорит, все ок с unsafe кодом, но ругается на библиотеку Rayon (параллельные вычисления). Автор в интернетах говорит, что все ок, но уговорить Miri не знает как. Что ж, оставим данное на совести автора библиотеки и перейдем к итераторам.

iters, options, match

Итераторы пришли из хаскеля, если даже не из хаскеля - они все равно молодцы, очень хороший апи. Например, вспомните в питоне, пожалуйста, без консоли позицию индекса при enumerate(list)? В расте, по-моему, ответ очевиден:

for (x,i) in v.iter().zip(0..) {
	println!("val {x} - index {i}");
}

Или тип Option - который неявно импортирован в скоуп каждого файла, - просто прекрасен. В следующих раз, когда вы захотите -1 поставить как отсутствие значения, вспомните, плиз, об Option. Ну и напоследок match:

let token = match ch as char {
    '0'..='9' => Token::Number,
	'A'..='Z' | 'a'..='z' | '_' => Token::Variable,
    '*' | '+' | '-' | '/' | '&' | '|' | '%' | '^' => Token::Operator,
    _ => panic!("no match"),
};

Кажется, что создатели раста захотели хаскель в продакшне, но так, чтобы, типа, никто и не догадался. Идея ок.

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


  1. ababo
    04.01.2024 18:44
    +16

    Что это было?


    1. gmtd
      04.01.2024 18:44
      +16

      Пацан замолвил слово за Rust


    1. ParaMara
      04.01.2024 18:44
      +14

      Это был ценнейший материал о восприятии программирования в общем и языка Rust в частности представителем некоторой группы программистов. На сколько большой - вопрос удачи при попадании в социальное окружение, как по мне - весьма большой.

      Человек пишет что думает, а что о нём подумают - не думает. Это не только искренне чем само по себе заслуживает уважения, это ценно. Да, у меня про Rust сложилось совершенно другое мнение и даже в этом стиле я писал бы о другом. Да, скорее всего с подавляющим большинством читателей дело обстоит так же, ну и что? Это не статья про Rust, это статья про впечатление от него.

      Конечно, если все статьи будут этого типа, чтобы не сказать уровня, то это будет катастрофа (для Хабра). Но если таких статей не будет вовсе, то это будет недостаток.


      1. xyli0o Автор
        04.01.2024 18:44

        Спасиб)


      1. arezvov
        04.01.2024 18:44
        +1

        Комментарий хорош.

        Добавлю, что ваша точка зрения мне очень симпатична, ее бы куда-нибудь в принципе сообщества вывести, лаконично и емко выражена.


    1. Tesserust
      04.01.2024 18:44

      Вольный пересказ книги "The Rust programming language".


    1. numenorix
      04.01.2024 18:44
      -2

      А чё? нормуль же разложено все.


  1. feelamee
    04.01.2024 18:44
    -1

    А за что заминусили то. Хоть бы объяснили.
    Вроде все по делу. Осталось только добавить тег Opinion)
    Хотя толку, я, как вижу, тут и за Opinion людей минусуют


    1. ptr128
      04.01.2024 18:44
      +1

      Я тоже не понимаю смысла ставить минус без комментария, раскрывающего его причину. Никакой пользы для автора и сообщества такие минусы не несут. Лично для себя я воспринимаю такие минусы, как "морда на аватарке не понравилась". Всё же это технический профессиональный сайт. А эмоциональная или догматическая оценка - не профессионально и лженаучно. Отрицательная рецензия без аргументов ничего положительного об её авторе не говорит.


  1. nameisBegemot
    04.01.2024 18:44
    +6

    Питон хорош. Джава хорош. Или хороша. Си хороши. Го хорош. Все они хороши


    1. hVostt
      04.01.2024 18:44
      +1

      Без критики нет развития. Нет совершенства, лишь путь к нему. Если добавить к языку уже нечего, значит язык умер, и смысла его развивать нет. Уж слишком он хорош :)


    1. breninsul
      04.01.2024 18:44

      Kotlin - нормальная Java :)

      После него Java не то чтоб плоха, но в ней не много смысла самой по себе. То, что JVM развивается вместе с ней - другой вопрос)


  1. semenInRussia
    04.01.2024 18:44

    Про enumerate не понял, в расте тоже enumerate, и что индекс первый стоит тоже не очень понятно, а zip и в питоне можно использовать.

    Но а так норм, не понимаю почему так задизили