man!(D => Rust).basics
Просьба не воспринимать эту статью слишком серьёзно, переходить с D на Rust не призываю, просто после прочтения серии переводов за авторством Дмитрия aka vintage, мне стало любопытно переписать примеры кода на Rust, тем более, что автор добавил этот язык в голосование. Мой основной рабочий инструмент — С++, хотя в последнее время активно интересуюсь Rust. За D несколько раз пытался взяться, но каждый раз что-то отталкивало. Ни в коем случае не хочу сказать, что это плохой язык, просто местами он «слишком радикален» для «убийцы плюсов», например, имеется GC (пусть и отключаемый), а в других местах наоборот слишком близок к С++ со всеми его неочевидными нюансами.
Самое забавное тут то, что после изучения Rust отношение к D несколько изменилось — в плане лаконичности и выразительности последний явно выигрывает. Впрочем, «явность» Rust-сообщество наоборот считает преимуществом. По моим ощущениям, в Rust чаще руководствуются «академической правильностью», а в D более практичный подход. Что лучше — сложный вопрос, лично я и сам не всегда могу определиться.
Впрочем, это всё очень субъективно, так что давайте вместе посмотрим на код. Код на Go приводить не буду, при желании, можно посмотреть в оригинальной статье.
Hello World
D
module main;
import std.stdio;
void main()
{
// stdout.writeln( "Hello, 世界" );
writeln( "Hello, 世界" );
}
Rust
fn main() {
println!("Hello, 世界")
}
Rust неявно импортирует (хотя можно отключить) наиболее часто используемые вещи, так что дополнительно импортировать ничего не нужно.
Точку с запятой в этом примере можно и опустить — она используeтся, чтобы превратить выражения (expressions) в инструкции (statements), а так как и main и println! ничего не возвращают (на самом деле, возвращается специальный пустой тип ()
), то разницы нет.
Явно указывать название модуля не нужно, если мы не хотим объявить вложенный модуль, так как оно зависит от имени файла или директории. Подробнее про модули (перевод).
Packages
D
module main;
import std.stdio;
import std.random;
void main()
{
writeln( "My favorite number is ", uniform( 0 , 10 ) );
}
Rust
extern crate rand;
use rand::distributions::{IndependentSample, Range};
fn main() {
let between = Range::new(0, 10);
let mut rng = rand::thread_rng();
println!("My favorite number is {}", between.ind_sample(&mut rng));
}
Этот пример получился не совсем эквивалентым, так как в Rust к расширению стандартной библиотеки подходят осторожно, в итоге ради многих «простых» вещей приходится обращаться к сторонним библиотекам. Решение неоднозначное, но имеет свои преимущества. В любом случае, подключать библиотеки весьма просто благодаря удобному пакетному менеджеру Cargo.
Ну и работа со случайными значениями более многословная, напоминает как это сделано в С++, правда это претензия к библиотеке.
Imports
D
module main;
import
std.stdio,
std.math;
void main()
{
import std.conv;
// ...
}
Rust
use std::{path, env};
fn main() {
use std::convert;
// ...
}
Rust позволят группировать импорты с общим корнем. К сожалению, в таких импортах нельзя использовать относительные пути:
use std::{path, env::args}; // Error
Импорты так же можно могут быть не только в начале файла, но они должны располагаться в самом начале блока.
Exported names
Напомню, что в D всё, по умолчанию, считается public (кроме импортов самого модуля), при желании можно указать private. Rust и в этом вопросе предпочитает явность: экспортируются только помеченные ключевым словом pub сущности. Пример:
mod test {
pub struct PublicStruct {
pub a: i32,
}
pub struct NoSoPublicStruct {
pub a: i32,
b: i32,
}
struct PrivateStruct {
a: i32,
}
pub struct PublicTupleStruct(pub i32, pub i32);
pub struct TupleStruct(pub i32, i32);
struct PrivateTupleStruct(i32, i32, i32);
pub fn create() -> NoSoPublicStruct {
NoSoPublicStruct { a: 10, b: 20 }
}
fn create_private() -> PublicTupleStruct {
PublicTupleStruct(1, 2)
}
}
use test::{PublicStruct, NoSoPublicStruct, PublicTupleStruct, create};
// Ошибка: невозможно импортировать приватные типы/функции.
// use test::{PrivateStruct, create_private}; // Error.
fn main() {
let _a = PublicStruct { a: 10 };
// Ошибка: невозможно извне создать структуру с приватными полями.
// let _b = NoSoPublicStruct { a: 10, b: 20 }; // Error.
let _c = create();
// Ошибка: обращение к приватным данным.
// _c.b;
let _d = PublicTupleStruct(1, 2);
}
Functions
D
module main;
import std.stdio;
int add( int x , int y )
{
return x + y;
}
void main()
{
// writeln( add( 42 , 13 ) );
writeln( 42.add( 13 ) );
}
Rust
fn add(x: i32, y: i32) -> i32 {
x + y
}
fn main() {
println!("{}", add(42, 13));
}
Rust не позволяет использовать функции как методы, хотя обратное и возможно. Плюс методы реализуются извне, так что их (а так же реализации трейтов) можно добавлять существующим типам. Обобщённое программирование в наличии:
D
module main;
import std.stdio;
auto add( X , Y )( X x , Y y ) {
return x + y; // Error: incompatible types for ((x) + (y)): 'int' and 'string'
}
void main()
{
// writeln( 42.add!( int , float )( 13.3 ) );
writeln( 42.add( 13.3 ) ); // 55.3
writeln( 42.add( "WTF?" ) ); // Error: template instance main.add!(int, string) error instantiating
}
Rust
use std::ops::Add;
fn add(x: T1, y: T2) -> Result
where T1: Add {
x + y
}
fn main() {
println!("{}", add(42, 13));
//println!("{}", add(42, "eee")); // trait Add is not implemented for the type
}
Тут мы буквально говорим: функция add принимает два параметра Т1 и Т2 и возвращает тип Result, где для типа Т1 реализовано сложения с типом Т2, возвращающее Result. На этом примере лучше всего видно различие в подходах: мы жертвуем лаконичностью и, отчасти, гибкостью ради «явности» и более удобных сообщений об ошибках — из-за необходимости указывать ограничения типам, проблема не может просочиться через много уровней, порождая кучу сообщений.
Multiple results
D
module main;
import std.stdio;
import std.meta;
import std.typecons;
auto swap( Item )( Item[2] arg... )
{
return tuple( arg[1] , arg[0] );
}
void main()
{
string a , b;
AliasSeq!( a , b ) = swap( "hello" , "world" );
writeln( a , b ); // worldhello
}
Rust
fn swap(a: i32, b: i32) -> (i32, i32) {
(b, a)
}
fn main() {
let (a, b) = swap(1, 2);
println!("a is {} and b is {}", a, b);
}
Распаковка выглядит в точности как объявление.
Named return values
В Rust, как и в D, нет именованных возвращаемых значений. Кортежей с именованными аргументами так же нет. Впрочем, последние мне кажутся странной штукой — почему бы, в таком случае, не использовать структуры?…
Кстати, в обоих языках нет и именованных параметров функций. Забавно, что и в D и в Rust они могут появиться.
Variables
D
module main;
import std.stdio;
void main()
{
bool c;
bool python;
bool java;
int i;
}
Rust
fn main() {
let c: bool;
let python: bool;
let java: bool;
let i: i32;
}
В Rust компилятор запрещает обращениe к не инициализированным переменным.
Short variable declarations
D
Rust
fn main() {
let (i, j) = (1, 2);
let k = 3;
let (c, python, java) = (true, false, "no!");
println!("{}, {}, {}, {}, {}, {}", i, j, k, c, python, java); // 1, 2, 3, true, false, no!
}
Оба языка умеют выводить типы, но в Rust тип может выводиться не только из объявления, но и использования:
fn take_i8(_: i8) {}
fn take_i32(_: i32) {}
fn main() {
let a = 10;
let b = 20;
take_i8(a);
//take_i32(a); // error: mismatched types: expected `i32`, found `i8`
take_i32(b);
//take_i8(b); // error: mismatched types: expected `i8`, found `i32`
}
Такой пример выглядит несколько надуманно, но возможность бывает удобной, если мы передаём в функцию (или возвращаем из) не полностью уточнённый тип.
Basic types
Таблица соответствия типов:
Go D Rust
---------------------------------
void ()
bool bool bool
string string String
&str
int int i32
byte byte i8
int8 byte i8
int16 short i16
int32 int i32
int64 long i64
uint unint u32
uint8 ubyte u8
uint16 ushort u16
uint32 uint u32
uint64 ulong u64
uintptr size_t usize
ptrdiff_t isize
float32 float f32
float64 double f64
real
ifloat
idouble
ireal
complex64 cfloat
complex128 cdouble
creal
char
wchar
rune dchar char
В Rust базовых типов, опять же, самый необходимый минимум, за более экзотическими вещами надо идти в библиотеки. Свойства связанные с типами в Rust определенны в виде констант (на примере f64).
В сравнении не участвуют «более хитрые» типы, есть забавная таблица, где на них можно посмотреть и ужаснуться.
Zero values
D
module main;
import std.stdio;
void main()
{
writefln( "%s %s %s \"%s\"" , int.init , double.init , bool.init , string.init ); // 0 nan false ""
}
Rust
fn main() {
println!("{} {} {} '{}'", i32::default(), f64::default(), bool::default(), String::default()); // 0 0 false ''
}
В Rust типы могут реализовывать трейт Default, если у них есть осмысленное значение по умолчанию. Как уже говорилось, компилятор следит за доступом к не инициализированным переменным, поэтому особого смысла в том, чтобы автоматически их инициализировать нет.
Type conversions
В Rust отсутствуют неявные приведения типов. Даже те, которые можно сделать безопасно — без потери точности. Недавно у меня как раз состоялся спор с другом и он аргументировал тем, что приведение целочисленных типов к числам с плавающей запятой должно быть явным, как и приведение к платформозависимым типам (таким как size_t). С последним я вполне согласен — не слишком удобно когда предупреждение вылазит только при других настройках компиляции. В итоге, чем делать запутанные правила лучше всегда требовать явного указания типа — решение вполне в духе философии языка.
let a: i32 = 10;
let b: i64 = a as i64;
Numeric Constants
D
enum Big = 1L << 100; // Error: shift by 100 is outside the range 0..63
Rust
let a = 1 << 100; // error: bitshift exceeds the type's number of bits, #[deny(exceeding_bitshifts)] on by default
Кстати, Rust в дебажной сборке следит за переполнениями при арифметических операциях. В релизe, ради производительности, проверки отключаются, хотя и есть способ явно включить/отключить их, независимо от типа сборки.
Разумеется, как и в случае с D, при переписывании таких простыв примеров с другого языка, не всегда есть возможность полностью раскрыть преимущества/особенности. Скажем, за кадром остались алгебраически типы данных, сравнение с образцом и макросы.