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<T1, T2, Result>(x: T1, y: T2) -> Result      where T1: Add<T2, Output = Result> {     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, при переписывании таких простыв примеров с другого языка, не всегда есть возможность полностью раскрыть преимущества/особенности. Скажем, за кадром остались алгебраически типы данных, сравнение с образцом и макросы.

ссылка на оригинал статьи https://habrahabr.ru/post/280642/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *