Rust хорош

0e3895f5b12e152cf7ace714822a7bef

Как известно, 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;

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

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

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

use rayon::prelude::*;

fn par(input: Vec) {
    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 { /* omitted */ }

unsafe impl Send for Tree {}
unsafe impl Sync for Tree {}

type Link = Option>>;

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

#[derive(Default)]
pub struct Cursor<'a, T> {
    current: Link,
    _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"),
};

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

© Habrahabr.ru