man!(D => Rust).basics

в 0:14, , рубрики: D, Go, Rust, Программирование

Просьба не воспринимать эту статью слишком серьёзно, переходить с 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, при переписывании таких простыв примеров с другого языка, не всегда есть возможность полностью раскрыть преимущества/особенности. Скажем, за кадром остались алгебраически типы данных, сравнение с образцом и макросы.

Автор: DarkEld3r

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js