Русский
Русский
English
Статистика
Реклама

Размышления о Rust

Всем привет. Не сразу, но я полюбил Rust. И эта любовь привела меня в бескрайние моря лоулевельного кода. О том, что мне удалось найти - под катом.

Секретный тип данных

Если вы читали Rust Book, то наверняка помните похожий код-сниппет:

fn unwrap<T>(option: Option<T>) -> T{    let unwrapped = match option{        Some(val) => val,        None => panic!("This cannot be None!")    };    return unwrapped;}fn main() {    let unwrapped = unwrap(Some(0));}

Проверить

Конечно, здесь нет ничего необычного. Возвращаем значение внутри Option, если оно есть, либо вызываем завершение процесса с помощью макроса panic!. Но задумывались ли вы, почему этот код компилируется? Как компилятор понимает, что функция, возвращающая T, может вернуть... это?

На самом деле, всё очень просто - макрос panic возвращает тип данных "!". Документация

Тип данных "!" просит выйти из текущего блока кода. Как это использовать? Сами разработчики языка предлагают такой вариант:

#![feature(never_type)]use std::convert::TryInto;#[derive(Debug)]enum ConnectionError{    BrokenPipe,    BadId,    Other}struct Client;struct Request;struct Response;impl Request{    pub fn build_response(&self) -> Response{        Response    }}fn get_request(id: i32) -> Result<(Client, Request), ConnectionError>{    match id % 2 == 0{        true => {            Ok((Client, Request))        },        false => {            Err(ConnectionError::BadId)        }    }}fn init_server() -> Result<!, ConnectionError>{    loop {        let (client, request) = get_request(5i32)?;        let resp = request.build_response();    };}fn main() {    let x: ! = init_server().unwrap();}

Проверить

Однако, эта конструкция, работающая только в nightly билдах, спокойно превращается в обычный код заменой "!" на пустой тип данных "()":

fn init_server() -> Result<(), ConnectionError>{    loop {        let (client, request) = get_request(5i32)?;        let resp = request.build_response();    };}fn main() {    let x = init_server().unwrap();}

В чём же разница? Всё очень просто, в первом примере мы не сможем полностью выполнить код:

fn main() {    match init_server(){        Ok(v) => { println!("unreachable? {:?}", v); },        Err(_) => {}    }; }

Компилятор любезно сообщит, что ветка Ok(v) - недостижима. Разумеется, это не помешает ему запустить программу, однако мне хотелось бы обозначить такую интересную особенность. Понятно, что она была бы недостижима и во втором примере, однако если его скомпилировать, то сообщения о недостижимом коде не будет.

Почему так происходит? Потому что то, что примет значение v в данном сниппете буквально означает "выход". "!" возвращается, когда вы пишете break, continue или std::process::exit.

И, внимание, вопрос. Зачем нужна #![feature(never_type)] ? С тех пор, как я узнал об этом типе данных, я думал, где его можно применить. И такого места, кажется, нет. Во всех случаях вы будете использовать panic, expect, todo или unimplemented. К чему нужен "!"?

Странные интерфейсы

Сразу скажу, что этот вопрос скорее дискуссионный. Здесь я не буду рассказывать о малоизвестной вещи, речь пойдет о странно реализованном полиморфизме.

В Rust есть такой интерфейс (трейт - скорее абстрактный класс, но лично мне удобнее называть его интерфейсом) Fn. И вроде бы с ним всё просто - все функции и лямбда-выражения ("closures" или "замыкания", если угодно), принимающие иммутабельные входные значения, его реализуют. В чём тут подвох?

Дело в том, что, как мы знаем, нельзя создать переменную типа данных impl Trait. Но проблема в том, что такой тип данных можно вернуть из функции, и это вызывает вопросы...

use std::any::type_name;fn type_of<T>(x: T) -> &'static str {    type_name::<T>()}fn callback() -> impl Fn(f32) -> f32{    |a| {      a*2.      }}fn main() {    let x = callback();    println!("{}", type_of(x));}

Вывод будет такой: playground::callback::{{closure}} . И вот, казалось бы, переменная х имеет тип данных impl Fn(f32) -> f32, вот только если мы об этом явно напишем, то код не скомпилируется. Как мы знаем, чтобы хранить trait object, нужно использовать ключевое слово dyn. Но вот незадача - компилятор не знает, сколько памяти будет занимать этот trait object, поэтому необходимо такие вещи класть в кучу с помощью Box:

fn main() {    let x: Box<dyn Fn(f32) -> f32> = Box::new(callback());    println!("{}", type_of(x));}

Но тогда и вывод поменяется:

alloc::boxed::Box&lt;dyn core::ops::function::Fn&lt;(f32,)>+Output = f32>

Я это понимаю так: компилятор знает, что это за имплементация, поэтому может рассчитать количество необходимой памяти.

Однако мне до сих пор не даёт покоя мысль, что такой код работает:

use tokio; // 1.0.2use tokio::task::JoinError;use futures::prelude::*; // 0.3.12async fn job1(){}async fn job2(){for i in 0..5{}}async fn job() -> Vec<impl Future<Output = Result<(), JoinError>>>{    vec![    tokio::spawn(async move{        job1().await;    }),    tokio::spawn(async move{        job2().await;    })]}#[tokio::main]async fn main() {    let mut v = job();}

С точки зрения логики компилятора, тут нет проблемы - tokio::spawn создаёт структуруtokio::task::JoinHandle. Да, JoinHandle - это одна и та же структура, она принимает футуру, которая создаётся блоком async{} , однако почему таски, содержащие разные async-блоки, интерпретируются компилятором как одна и та же реализация? Почему код

let v = vec![        Box::new(async{}),        Box::new(async{            let cb = |x| x*2.;            let val = cb(1f32);        })    ];

не выполнится, а тот, что выше, выполнится? Ведь мы передаём разные реализации футур. Поделитесь в комментариях, если у вас есть мысли по этому поводу. Я честно с умным видом полчаса изучал исходники, но так и не понял.

Заключение

Rust, каким бы хорошим ни был, порой заставляет крепко задуматься. Почему retain не меняет capacity? Почему функциональные исчисления сделали ленивыми? Почему cargo создаёт странные папки с хеш-суммами на каждый случай жизни, вместо того, чтобы собрать одни и те же библиотеки один раз (хотя, справедливости ради, это не проблема самого языка)? Как бы то ни было, если писать на плюсах - это стрелять себе в ногу, то писать на расте - это пытаться стрелять себе в ногу (и не дай бог в проекте вы используете ffi, тогда попытки могут оказаться вполне успешными).

Цель этой статьи - попытаться углубиться внутрь языка, понять, как он работает изнутри, ведь, как известно, любить можно только того, кого понимаешь.

Источник: habr.com
К списку статей
Опубликовано: 06.02.2021 10:17:52
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Программирование

Rust

Tokio/futures

Компиляторы

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru