Просьба не воспринимать эту статью слишком серьёзно, переходить с 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.

Ну и работа со случайными значениями более многословная, напоминает как это сделано в С++, правда это претензия к библиотеке.

К сожалению, play.rust-lang не поддерживает внешние пакеты, так что с примером поэкспериментировать не получится.


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);
}

Распаковка выглядит в точности как объявление, так как let — это полноценное сопоставление с образцом.


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
module main;

import std.stdio;

void main() 
{
    int i = 1 , j = 2;
    auto k = 3;
    auto c = true , python = false , java = "no!";

    writeln( i , j , k , c , python , java ); // 123truefalseno!
}

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

Комментарии (53)


  1. ufm
    01.04.2016 04:15

    На D можно написать так:
    auto add( X, Y )( X x, Y y ) if (__traits(compiles, x + y)) {
    (это не самый правильный вариант, но самый простой) и тогда ошибка будет вполне внятной.
    a.d(13): Error: template main.add cannot deduce function from argument types !()(int, string), candidates are:
    a.d(5): main.add(X, Y)(X x, Y y) if (__traits(compiles, x + y))

    P.S. Вобще, сравнение двух языков, да еще с примерами без _отличного_ знания обоих языков напоминает мне «перевожу с японского на сомалийский. Знание обоих языков — читаю со словарём». Извините.


    1. DarkEld3r
      01.04.2016 12:13
      +3

      Знание обоих языков — читаю со словарём

      А можно какие-то аргументы? Хотелось бы думать, что Rust я знаю не так уж плохо.

      Для D я брал готовый код и, разумеется, мог упустить какие-то нюансы. Впрочем, подозреваю, что гоферы точно так же не были довольны оригинальной статьёй. (:
      На D можно написать так:

      В курсе. Речь шла о том, что на Rust нельзя написать без явного указания требований к типам. То есть, в D мы можем написать "быстро и коротко", а можем "правильно". В первом случае, перекладываем проблемы на пользователей. При этом, я признаю, что далеко не всегда нужен такой сильный контроль над типами — шаблон может быть приватным, например. Но опять же, для Rust это последовательное решение в духе языка.


      1. rafuck
        11.04.2016 00:50

        Да хотя бы вот (листнул на первый попавшийся пример)
        Вы пишете «В Rust компилятор запрещает обращениe к не инициализированным переменным.». И все. Это как-то выделяет Rust? А D что, разрешает?


        1. DarkEld3r
          11.04.2016 01:19
          +2

          Это как-то выделяет Rust? А D что, разрешает?

          D, по умолчанию, просто инициализирует все переменные:


          int val;
          writeln(val); // выведем int.init

          Rust этого не делает и требует нас самих инициализировать:


          let val: i32;
          println!("{}", val); // use of possibly uninitialized variable

          Согласитесь, что есть разница. Да, в плане "безопасности" она не особо существенна, но подход-то другой.

          Ну и если честно, не совсем понял аргумент. Как процитированное относится к знанию языка? Я разве что-то неправильно сказал?


          1. rafuck
            11.04.2016 08:51

            Вот сейчас вы раскрыли разницу. А в статье — нет.


  1. JIghtuse
    01.04.2016 08:21

    Для языка с таким небольшим сообществом D поражает своими возможностями. Многое, что только ползёт в С++ (или напротив, ещё и не планирует), здесь уже есть: ranges, modules, template constraints (~concepts), contracts, unit tests, scope guards*. При этом сочетается всё как-то более гармонично, чем в C++. Меньше базовых принципов что ли.
    Жаль, что работодателей мало интересуют D и Rust. Они разные, но выглядят очень интересно оба.
    * Про последние не так давно узнал, к слову. На первый взгляд, удобнейшие возможности. Есть выступление Александреску на тему поддержки C++:

    Скрытый текст

    И описание на сайте D: dlang.org/exception-safe.html


    1. DarkEld3r
      01.04.2016 12:17

      scope guards

      Справедливости ради, в плюсах оно есть в виде библиотечных решений. Например, BOOST_SCOPE_EXIT. Хотя в D оно и несколько более навороченное.


  1. vintage
    01.04.2016 08:53
    -3

    Точку с запятой в этом примере можно и опустить — она используeтся, чтобы превратить выражения (expressions) в инструкции (statements), а так как и main и println! ничего не возвращают (на самом деле, возвращается специальный пустой тип ()), то разницы нет.
    А как всё же реализовать несколько точек выхода? Типа такого:

    ```d
    int getHash( string str )
    {
    if( string.length == 0 ) return 0;
    // считаем хеш по хитрому алгоритму
    return hash;
    }

    ```

    Явно указывать название модуля не нужно, если мы не хотим объявить вложенный модуль, так как оно зависит от имени файла или директории.
    В D на самом деле аналогично.

    мы жертвуем лаконичностью и, отчасти, гибкостью ради «явности» и более удобных сообщений об ошибках — из-за необходимости указывать ограничения типам, проблема не может просочиться через много уровней, порождая кучу сообщений.
    Но в примере как раз наоборот, сообщение в Rust не говорит какой именно параметр мы не так написали (trait Add is not implemented for the type), а вот в D нам не просто сказали, что нет реализации такой сигнатуры (Error: template instance main.add!(int, string) error instantiating), но и подсказали почему (Error: incompatible types for ((x) + (y)): 'int' and 'string').

    В Rust компилятор запрещает обращениe к не инициализированным переменным.
    В D просто нет такого понятия как «неинициированная переменная», так как с ними много проблем. От проблем с безопасностью, до сложностей с побитовым сравнением. Например, структуры в D сравниваются наиболее быстрым способом — через сравнение участков памяти.

    Оба языка умеют выводить типы, но в Rust тип может выводиться не только из объявления, но и использования:
    Очень опасный приём, особенно в свете множественной диспетчеризации.

    a as i64
    Вот, кстати, что это, что `cast(long) a` — одинаково не удобны в выражениях, в отличие от `a.to!long`


    1. Googolplex
      01.04.2016 09:56
      +7

      А как всё же реализовать несколько точек выхода?

      Конечно же, в Rust есть return. Но его использование идиоматично только для ранних возвратов.
      Но в примере как раз наоборот, сообщение в Rust не говорит какой именно параметр мы не так написали

      На самом деле здесь важны не сообщения об ошибках, а типобезопасность. В отличие от шаблонов в C++ или в D, дженерики в Rust позволяют применять к типам-параметрам только те операции, которые разрешаются ограничениями на эти параметры. В этом плане Rust ближе к языкам типа Java или к концептам (так и не вошедшим в стандарт, емнип) в C++. Ну и сообщение об ошибке на самом деле говорит какой конкретно типовый параметр неправильный, просто автор статьи не привёл это сообщение полностью. Вообще у rustc сообщения об ошибках просто феноменальные.
      так как с ними много проблем

      Ну в Rust с ними нет проблем, потому что переменные, которым ничего не присвоено, использовать нельзя, компилятор это проверяет статически и способа обойти это нет. То, как в Rust сравниваются объекты, зависит от реализации трейта PartialEq. В случаях, когда этот трейт автоматически выведен через derive (99%), то если структура состоит целиком из примитивных типов, то всё сведётся к побайтовому сравнению.
      Очень опасный приём, особенно в свете множественной диспетчеризации

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


      1. vintage
        01.04.2016 10:27
        -4

        Конечно же, в Rust есть return. Но его использование идиоматично только для ранних возвратов.

        Тогда не очень понятно, что они сэкономили сделав return опциональным. Неконсистентный синтаксис получился.
        В отличие от шаблонов в C++ или в D, дженерики в Rust позволяют применять к типам-параметрам только те операции, которые разрешаются ограничениями на эти параметры.
        В D можно делать и так и так. Можно использовать структурную типизацию через шаблоны, а можно номинативную, через интерфейсы.

        Ну и сообщение об ошибке на самом деле говорит какой конкретно типовый параметр неправильный, просто автор статьи не привёл это сообщение полностью.
        Вот полностью из песочницы на сайте:

        ```rust
        error: the trait `core::ops::Add<&str>` is not implemented for the type `_` [E0277]
        :2:25: 2:56 note: in this expansion of format_args!
        :3:1: 3:54 note: in this expansion of print! (defined in )
        ```

        Как-то не очень понятно.

        Ну в Rust с ними нет проблем, потому что переменные, которым ничего не присвоено, использовать нельзя, компилятор это проверяет статически и способа обойти это нет.
        Но в 99% случаев инициализируются они всё равно дефолтными значениями. Наверняка в процессе хитрых кастов можно случайно обойти ограничение на неиспользование неинициированной памяти.

        Вообще говоря, вывод типов — это безумно удобная вещь.
        Речь не о выводе типов, которая есть и в D, а изменение типа в зависимости от того, какая функция была вызвана первой. Редактирование такого кода, как хождение по минному полю. Ну или покажите пример, где оно действительно полезно.


        1. Googolplex
          01.04.2016 11:13
          +5

          Тогда не очень понятно, что они сэкономили сделав return опциональным. Неконсистентный синтаксис получился

          В большинстве случаев early returns не используются. Кроме того, Rust — это expression-based язык, как Scala. Например, вместо тернарного оператора в нём if:
          let y = if x > 0 { ... } else { ... };

          Отсутствие необходимости писать return для возврата значения, если это значение — последнее выражение в функции, вытекает из этого совершенно естественно.
          номинативную, через интерфейсы

          Интерфейсы — это всё-таки немного не то. Если я не ошибаюсь, в D, как и в C++ и в Java, интерфейсы подразумевают динамическую диспетчеризацию. В Rust динамическая диспетчеризация используется только если вы сами её попросите (через трейт-объекты).
          Вот полностью из песочницы на сайте

          Такое сообщение появляется, если в качестве параметра add() используется нетипизированный литерал (42). Для удобства в Rust, как и во многих языках, числовые литералы могут автоматически принимать любой тип. Но в данном контексте он используется в качестве аргумента дженериковой функции, поэтому компилятор не может вывести тип литерала так просто. Вот такая ошибка возникает, если тип литерала указать явно (42i32):
          <anon>:9:20: 9:23 error: the trait `core::ops::Add<&str>` is not implemented for the type `i32` [E0277]
          <anon>:9     println!("{}", add(42i32, "eee"));
                                      ^~~

          На мой взгляд, понятнее некуда.
          Но в 99% случаев инициализируются они всё равно дефолтными значениями. Наверняка в процессе хитрых кастов можно случайно обойти ограничение на неиспользование неинициированной памяти.

          Неинициализированные переменные ничем не инициализируются, если этого не указать явно, и нет, случайно обойти это ограничение нельзя в принципе. Одна из целей Rust — это гарантия полной безопасности работы с памятью; это включает в себя полный запрет на работу с неинициализированной памятью вне unsafe.
          изменение типа в зависимости от того, какая функция была вызвана первой

          Вообще-то это и есть вывод типов. Компилятор выводит тип переменной в зависимости от того, как она используется. Просто в Rust вывод типов действует внутри функции целиком, а не только при присваивании значения, как с auto в C++.
          Редактирование такого кода, как хождение по минному полю. Ну или покажите пример, где оно действительно полезно.

          Вам это так кажется. Редактировать и рефакторить такой код ничуть не сложнее, чем код без вывода типов, даже в чём-то проще — меньше печатать нужно. Почти не бывает такого, чтобы при рефакторинге тип переменной внезапно менялся, хотя бы потому, что объявлять отдельностоящие неинициализированные переменные неидиоматично. Кроме того, объявления типов обязательны в сигнатурах функций и констант/статических переменных, даже если они объявляются внутри другой функции.
          Пример "действительной полезности" привести сложно — это всё-таки не фича уровня borrow checker'а, без которой невозможно жить, а элементарное удобство. Обойтись без этого можно всегда. Но я вот совершенно не могу придумать несинтетического примера, где это бы помешало. Специально сейчас пять минут посвятил просмотру кода своих библиотек, и не нашёл ни одного проблемного места.


          1. vintage
            01.04.2016 11:46
            -3

            Отсутствие необходимости писать return для возврата значения, если это значение — последнее выражение в функции, вытекает из этого совершенно естественно.
            Кажется я понял. Не «точка с запятой не обазательна» или «точка с запятой превращает выражение в утверждение», а просто всё есть выражения, и функция возвращает значение последнего, просто последнее выражение может быть пустым и возвращать соответственно пустоту.

            Интерфейсы — это всё-таки немного не то. Если я не ошибаюсь, в D, как и в C++ и в Java, интерфейсы подразумевают динамическую диспетчеризацию.
            Нет, не подразумевают. И речь не только про интерфейсы классов.

            На мой взгляд, понятнее некуда.
            Ну то есть, приходится отказывать себе в удобстве, чтобы был понятный вывод.

            дна из целей Rust — это гарантия полной безопасности работы с памятью; это включает в себя полный запрет на работу с неинициализированной памятью вне unsafe.
            Ну а в unsafe вы получаете все эти проблемы, что и требовалось доказать :-)

            Редактировать и рефакторить такой код ничуть не сложнее, чем код без вывода типов, даже в чём-то проще — меньше печатать нужно.
            Я всеми руками за выведение типов и каждый день его использую. Речь исключительно о: «в Rust вывод типов действует внутри функции целиком, а не только при присваивании значения».

            Пример «действительной полезности» привести сложно — это всё-таки не фича уровня borrow checker'а, без которой невозможно жить, а элементарное удобство.
            Так покажите это удобство.


            1. Googolplex
              01.04.2016 11:59
              +4

              Нет, не подразумевают

              Что же это за такие интерфейсы?
              отказывать себе в удобстве

              Всё ещё не понимаю, про какое удобство вы говорите. На мой взгляд, шаблоны по типу C++, хоть и мощнее, значительно сложнее в правильном использовании. Дженерики с ограничениями трейтов проще и практичнее. И удобнее, потому что я точно знаю, когда пишу дженериковую функцию, что она будет работать везде и всегда, какие бы типы в неё не передали.
              Ну а в unsafe вы получаете все эти проблемы, что и требовалось доказать :-)

              Простите, но это настолько избитая тема, что я уже не хочу на неё отвечать подробно. Кратко — нет, это не то что требовалось доказать, и да, гарантия того, что подобные вещи возможны исключительно в unsafe, это game breaker. Единственный способ получить непроинициализированную переменную — это вызвать unsafe-функцию mem::uninitialized():
              let x: i32 = unsafe { mem::uninitialized() };

              И хотя бы поэтому случайно обратиться к неинициализированной памяти сложно. Особенно если у вас проекте указано #![forbid(unsafe)].
              Так покажите это удобство.

              Я не могу этого сделать, потому что вывод типов используется везде, и в каждом конкретном участке кода вывода типов не очень много, и примеры будут неубедительными. Если же писать код много, удобство становится очень заметным.


          1. potan
            01.04.2016 17:41

            Речь не о выводе типов, которая есть и в D, а изменение типа в зависимости от того, какая функция была вызвана первой.

            От этого зависит сообщение об ошибке. Если программа компилируется, то тип от перемены порядка вызовов меняться не будет.


        1. DarkEld3r
          01.04.2016 13:40

          Редактирование такого кода, как хождение по минному полю. Ну или покажите пример, где оно действительно полезно.

          Лучше покажите как при помощи этой фичи можно в ногу неожиданно выстрелить. (:

          Впрочем, в Rust есть ещё одна "спорная фича":
          let a = 10;
          let a = a.to_string();
          let a = Some(a);

          На всякий случай, уточняю: переменная a неизменяемая, просто тут три разных а. Когда впервые увидел, то показалось, что это ужасное решение и источник непонятных ошибок. А на деле оказывается удобно.


          1. vintage
            01.04.2016 14:23

            Я пацифист.
            А три разных а — и правда ужасное решение ибо вносит неразбериху. Не стоит оно того сомнительного удобства.


            1. DarkEld3r
              01.04.2016 14:57
              +3

              и правда ужасное решение ибо вносит неразбериху. Не стоит оно того сомнительного удобства.

              А ведь кто-то так думает про шаблоны...


              1. vintage
                01.04.2016 15:18
                -1

                В D порядок объявления шаблонов ни на что не влияет. А тут одна переменная втихую перекрывает другую.


                1. DarkEld3r
                  01.04.2016 16:02
                  +5

                  Я не про порядок, а про точку зрения. Или про "парадокс Блаба", если угодно. Гоферы живут без шаблонов и не (особо) жалуются, а вам кажется, что языки без обобщённого программирования заведомо убоги. Так и тут — вы видите источник проблем, а кто-то удобную возможность. Стоит её распробовать перед тем как критиковать.

                  Собственно, не в расте эту штуку придумали. И ограниченное "скрытие" есть даже в С++.


                  1. vintage
                    01.04.2016 16:59

                    В JS вполне частая ситуация, когда кому-то лень было придумывать новое имя и он воспользовался существующей переменной. В итоге во второй половине функции нет доступа к затёртому значению. А чтобы его получить нужно разделить имена. Вручную.


                    1. DarkEld3r
                      01.04.2016 17:15
                      +1

                      А чтобы его получить нужно разделить имена. Вручную.

                      Дык, если нам нужен доступ к переменным, то код мы, очевидно, и так модифицируем. В чём проблема переименовать?

                      А если код был бы вообще в виде цепочки вызовов, за которую вы так ратуете, записан? Тогда потребовалось бы ещё больше изменений.


                      1. vintage
                        01.04.2016 17:58
                        -1

                        Если сразу именовать по разному, то не будет проблемы переименовать вообще.
                        Чтобы разорвать цепочку не надо перелопачивать весь код — место разрыва локализуется в нескольких строках.


                1. ozkriff
                  06.04.2016 12:01
                  +3

                  Оно досталось внаследство от функциональщины, того же OCaml, насколько я помню.
                  Плюсы:

                  • помогает убрать изменяемость объекта без лишнего блока, иногда очень удобно.
                  • хорошо сочетается с семантикой перемещения — let foo = foo.unwrap();.
                  • если что-то пошло не так, то ты почти всегда получишь предупреждение о неиспользованной затененной переменной.
                  • ну и уж если совсем не нравится, то для своего проекта можно clippy подтянуть, в нем три проверки есть (по умолчанию все выключены):
                    shadow_reuse — rebinding a name to an expression that re-uses the original value, e.g. let x = x + 1
                    shadow_same — rebinding a name to itself, e.g. let mut x = &mut x
                    shadow_unrelated — The name is re-bound without even using the original value


          1. potan
            01.04.2016 17:49
            +2

            В некоторых языках явно различают let и let req. Отсутствие этого разделения в Haskell меня расстраивает: достаточно типичная ошибка в нем — в блоке let n = 5 in let n1 = n+1 in… можно перепутать переменные n и n1.
            Реализация let req в неленивом языке нетривиальна, а в C-подобных языках требуется он достаточно редко. По этому нерекурсивный let с возможностью переопределить переменную зависимым от нее значением очень полезна.


        1. potan
          01.04.2016 17:37
          +3

          Тогда не очень понятно, что они сэкономили сделав return опциональным.

          Короткие функции выглядят гораздо приятнее. return в 1-2-строчных функциях меня очень раздражает. А в C++ до недавнего времени константные функции могли быть только однострочными.
          Кроме того, в Rust принято использовать return для нештатного завершения, обычно при ошибке. Так как исключения не поддерживаются, такая двойственность синтаксиса делает код более читабельным.


    1. JIghtuse
      01.04.2016 09:57
      +1

      А как всё же реализовать несколько точек выхода?

      Точно так же: http://is.gd/WQfIrK
      Суть в том, что можно опускать return в конце любого scope, получая значение последнего выражения в блоке: http://is.gd/UhSmzV


    1. DarkEld3r
      01.04.2016 13:24

      В D на самом деле аналогично.

      Да, знаю, просто не решился переделывать оригинальные примеры кода. Признаю, сравнение из-за этого может выглядеть не совсем объективно…
      Хотя некоторая разница всё-таки есть: в расте невозможно дать имя модулю изнутри, так что аналога конструкции module main; нет.
      В D просто нет такого понятия как «неинициированная переменная», так как с ними много проблем.

      Хм… а вот это что такое?
      int a = void;
      writeln(a); // ???

      Вот, кстати, что это, что cast(long) a — одинаково не удобны в выражениях, в отличие от a.to!long

      Почему?


      1. vintage
        01.04.2016 14:25

        Ок, есть, но по умолчанию, все переменные инициализированы. :-)
        Потому что, когда цепочки пишешь приходится в дополнительные скобки брать.


        1. DarkEld3r
          01.04.2016 15:01

          Потому что, когда цепочки пишешь приходится в дополнительные скобки брать.

          Да, про это не подумал.


    1. ozkriff
      01.04.2016 15:12
      +1

      a as i64

      Вот, кстати, что это, что cast(long) a — одинаково не удобны в выражениях, в отличие от a.to!long

      Они и должны быть громоздкими и обращающими на себя внимание, как по мне. Это ж преобразования типов — в этом месте легко вносятся логические ошибки.


      1. vintage
        01.04.2016 15:20

        В любом коде легко вносятся логические ошибки. Но в преобразовании типов нет ничего особо страшного.


      1. Sirikid
        02.04.2016 13:38

        А вот в Go преобразование хорошо сделано: type(value)
        Если бы ещё были конструкторы то вызов конструктора от преобразования типа было бы не отличить.


  1. vintage
    01.04.2016 09:13

    Да, и у вас заголовок статьи не компилируется :-)


    1. ozkriff
      01.04.2016 10:07
      +8

      Пойдет?

      struct Man {
          basics: &'static str,
      }
      
      macro_rules! man {
          ( $x:expr => $y:expr ) => {
              Man{basics: "https://habrahabr.ru/post/280642"}
          }
      }
      
      fn main() {
          println!("{}", man!(D => Rust).basics);
      }

      https://play.rust-lang.org/?gist=1df33e28a746408d0905c9c71a74a5fe


      1. vintage
        01.04.2016 10:29
        -1

        Зачёт! :-)


      1. ozkriff
        01.04.2016 10:31
        +3

        Вот даже 2.0, а то чего-то я параметры макроса не использовал:

        use std::collections::HashMap;
        
        #[derive(PartialEq, Eq, Hash)]
        enum Lang {
            Go,
            D,
            Rust,
        }
        
        struct Info {
            basics: &'static str,
            concurrency: &'static str,
        }
        
        macro_rules! man {
            ($lang1:ident => $lang2:ident) => {{
                let mut db = HashMap::new();
                db.insert((Lang::Go, Lang::D), Info {
                    basics: "https://habrahabr.ru/post/279657",
                    concurrency: "https://habrahabr.ru/post/280378",
                });
                db.insert((Lang::D, Lang::Rust), Info {
                    basics: "https://habrahabr.ru/post/280642",
                    concurrency: "TODO: DarkEld3r еще не написал",
                });
                db.remove(&(Lang::$lang1, Lang::$lang2)).unwrap()
            }}
        }
        
        fn main() {
            println!("Go => D basics: {}", man!(Go => D).basics);
            println!("Go => D concurrency: {}", man!(Go => D).concurrency);
            println!("D => Rust basics: {}", man!(D => Rust).basics);
            println!("D => Rust concurrency: {}", man!(D => Rust).concurrency);
        }

        play.rust-lang
        Эх, вечно я над всякой ерундой залипаю.


        1. qthree
          01.04.2016 13:53

          У Вас каждый man! новый хэшмап создает и заполняет каждый раз.
          enum Lang {
          Go,
          D,
          Rust,
          }
          use Lang::*;

          struct Info {
          basics: &'static str,
          concurrency: &'static str,
          }

          macro_rules! man {
          ($lang1:ident => $lang2:ident) => {{
          match ($lang1, $lang2) {
          (Go, D) => Info {
          basics: «habrahabr.ru/post/279657»,
          concurrency: «habrahabr.ru/post/280378»,
          },
          (D, Rust) => Info {
          basics: «habrahabr.ru/post/280642»,
          concurrency: «TODO: DarkEld3r еще не написал»,
          },
          _ => unreachable!()
          }
          }}
          }

          fn main() {
          println!(«Go => D basics: {}», man!(Go => D).basics);
          println!(«Go => D concurrency: {}», man!(Go => D).concurrency);
          println!(«D => Rust basics: {}», man!(D => Rust).basics);
          println!(«D => Rust concurrency: {}», man!(D => Rust).concurrency);
          }


          1. ozkriff
            01.04.2016 14:02

            Ага, я об этом подумал уже когда запостил и даже накидал аналогичную версию 3.0, но потом решил что не очень клево превращать комментарии к статье в git)


        1. vintage
          01.04.2016 14:20
          +2

          Аналог на D:

          import std.traits;
          
          alias Lang = int;
          
          enum : int {
              Go ,
              D ,
              Rust ,
          };
          
          struct Info {
              string basics;
              string concurrency;
          }
          
          enum articles = [
              [ Go , D ] : Info( "https://habrahabr.ru/post/279657" , "https://habrahabr.ru/post/280378" ) ,
              [ D , Rust ] : Info( "https://habrahabr.ru/post/280642" , "TODO: DarkEld3r еще не написал, скорее бы" ) ,
          ];
          
          template man( Lang function( Lang ) axis )
          {
              static if( is( FunctionTypeOf!axis args == __parameters ) )
              {
                  enum from = mixin( __traits( identifier , args ) );
                  enum to = axis( from );
                  enum man = articles[[ from , to ]];
              }
          }
          
          unittest {
              static assert( man!(Go => D).basics == "https://habrahabr.ru/post/279657" );
              static assert( man!(Go => D).concurrency == "https://habrahabr.ru/post/280378" );
              static assert( man!(D => Rust).basics == "https://habrahabr.ru/post/280642" );
              static assert( man!(D => Rust).concurrency == "TODO: DarkEld3r еще не написал, скорее бы" );
              static assert( !is( man!(Go => Rust).concurrency ) );
          }
          
          void main( ) { }


  1. ingrysty
    01.04.2016 11:45
    +2

    Скоро еще будет сравнение D с Pony ;)


  1. Sirikid
    01.04.2016 13:52
    +1

    Мелкие неточности из сравнения типов D и Go просочились сюда, например в Go byte это алиас для uint8, а rune для int32.

    В Go есть void, но синтаксис построен так что писать его не надо.
    А можно ли считать void типом? В C нет, в D я думаю тоже.
    Пустой кортеж не эквивалентен отсутствию типа, поэтому в Rust есть кортежи вообще и пустой в частности, а в D и Go нету.
    Насчет 2 и 3 можно поспорить, чем я, надеюсь, и займусь под соседнем постом о кортежах.


    1. DarkEld3r
      01.04.2016 14:08

      Мелкие неточности из сравнения типов D и Go просочились сюда, например в Go byte это алиас для uint8, а rune для int32.

      Согласен, но решил не переделывать оригинал. Меня ещё и int смущает, который имеет фиксированный размер в D, но зачем-то приведён как аналог платформозависимого int из Go.

      Кстати, в Go есть возможность помечать функции как никогда не возвращающие управление? Насколько я вижу, в D были предложения ввести @noreturn атрибут. В Rust такие функции имеются и "результат" можно присвоить любому типу. Применяется, например, так:
      let a: String = match 10 {
          10 => "normal value".to_owned(),
          _  => panic!(),
      };


      1. vintage
        01.04.2016 14:43

        Потому, что если вы пишете кроссплатформенный софт, то вынуждены писать его предполагая, что размер инта минимальный из возможных.


        1. DarkEld3r
          01.04.2016 15:06

          Дык, если нам надо, то будем использовать типы фиксированного размера. В Go int — это аналог ptrdiff_t, а не int из D.


          1. vintage
            01.04.2016 15:33

            Наоборот, аналогом ptrdiff_t в го является int. Но сам int имеет более широкую область применения, чем смещение указателей. Согласитесь, при портировании программы будет странно менять общий тип int на зачастую не относящийся к делу ptrdiff_t.


            1. DarkEld3r
              01.04.2016 16:10

              Наоборот, аналогом ptrdiff_t в го является int.

              А в чём разница?
              Собственно, я могу согласиться с тем, что название int для платформозависимого типа не самое удачное. В этом плане мне нравится решение раста: isize/usize длиннее типов с фиксированным размером, да и название говорящее.
              Но сам int имеет более широкую область применения, чем смещение указателей.

              В Go или вообще? И неужели в D не возникает желания использовать "знаковый size_t" не только для разницы указателей?

              А при портировании всё равно какие-то изменения вносить придётся, иначе код получится убогим. Заодно будет поводом пересмотреть действительно ли там необходим ptrdiff_t.


              1. vintage
                01.04.2016 17:04

                Разница в направлении портирования. В D больше типов, в Go меньше. Так что в одном направлении приходится сводить несколько типов к одному (что относительно просто), а в другом — выбирать нужный тип (и тут конечно есть сложности, ибо в коде не хватает информации для выбора правильного типа — нужно анализировать алгоритм).
                И в Го и вообще. А почему должно быть желание использовать типы не по назначению?


                1. DarkEld3r
                  01.04.2016 17:17

                  Я всё-таки продолжаю думать, что в 99% случаев, при портировании проблемы с типами будут в последнюю очередь.

                  А почему должно быть желание использовать типы не по назначению?

                  В смысле?


      1. Sirikid
        02.04.2016 13:29

        Но panic! это макро (не знаю во что он раскрывается), unreachable! кстати тоже. Думаю там нет таких функций просто потому что пришлось бы делать другой компилятор и рантайм тем или иным образом.


        1. DarkEld3r
          02.04.2016 14:21
          +3

          Да, panic — это макрос, который раскрывается в вызов как раз такой специальной функции:

          fn begin_unwind<M: Any + Send>(msg: M, file_line: &(&'static str, u32)) -> ! { ... }

          Для наглядности мой пример можно и переписать заменив панику на собственную функцию:
          fn my_panic() -> ! {
              loop {}
          }
          
          fn fake_panic() {}
          
          fn main() {
              let _a: String = match 10 {
                  10 => "normal value".to_owned(),
                  //_ => fake_panic(), // Error.
                  _ => my_panic(),
              };
          }


    1. vintage
      01.04.2016 14:36

      Да нет, всё правильно. Не важно чему оно там алиас. Область применения у них эквивалентна.


  1. Dicebot
    03.04.2016 11:08

    Самое забавное тут то, что после изучения Rust отношение к D несколько изменилось — в плане лаконичности и выразительности последний сильно выигрывает. Впрочем, «явность» Rust-сообщество наоборот считает преимуществом. По моим ощущениям, в Rust чаще руководствуются «академической правильностью», а в D более практичный подход.

    У меня после продолжительных экспериментов с Rust осталось очень похожее впечатление. Его строгость очень привлекает в больших проектах с критической важностью корректности, но затраты времени на написание простых вещей очень уж непрактичны. Вероятнее всего, даже если я решу начинать какой-либо новый проект на Rust, прототип всё равно будет написан на D.


    1. DarkEld3r
      03.04.2016 19:21
      +2

      но затраты времени на написание простых вещей очень уж непрактичны

      Всё-таки думаю, что ситуация не настолько плоха. Если мы пишем что-то действительно простое, то и мест, где придётся "воевать" с бороу чекером будет мало. Сильнее всего это будет проявляться, если кода в общем мало и при этом активно изобретаются какие-то свои структуры данных — вот тут придётся вникать в тонкости языка и использовать unsafe. Если же пользоваться готовым, то всё куда проще. Конечно, у D есть сборка мусора, что должно давать выигрыш для быстрого прототипирования… хотя тут любители динамическиx языков предпочтут что-нибудь другое.

      Впрочем, у меня маловато практического опыта с Rust (а с D вообще нет), чтобы делать окончательные выводы.