Please do not take this article too seriously, I don’t urge to switch from D to Rust, just after reading a series of translations written by Dmitry aka
vintage , I was curious to rewrite the code examples on Rust, especially since the author added this language to the vote. My main working tool is C ++, although I have been actively interested in Rust lately. For D several times tried to take, but every time something repelled. I don’t want to say in any case that this is a bad language, it is just “too radical” in places for a “killer pluses”, for example, there is a GC (even if it is disabled), and in other places it’s too close to C ++ with all non-obvious nuances.
The funny thing is that after studying Rust, the attitude towards D has changed somewhat - in terms of conciseness and expressiveness, the latter greatly benefits. However, the “explicitness” of the Rust-community, on the contrary, considers it an advantage. According to my feelings, in Rust more often they are guided by "academic correctness", and in D a more practical approach. What is better is a difficult question, personally, I myself can not always decide.
However, this is all very subjective, so let's look at the code together. I will not give the code on Go, if you wish, you can look at the
original article .
')
Hello world
Dmodule main; import std.stdio; void main() {
Rusty fn main() { println!("Hello, 世界") }
Rust
implicitly imports (although you can disable) the most frequently used items, so you don’t need to import anything else.
In this example, the semicolon can be omitted - it is used to turn expressions into statements (statements), as well as main and println! returns nothing (in fact, a special empty type is returned
()
), then there is no difference.
Explicitly specifying the name of the module is not necessary if we do not want to declare a nested module, since it depends on the name of the file or directory. Read more about the
modules (
translation ).
Packages
D module main; import std.stdio; import std.random; void main() { writeln( "My favorite number is ", uniform( 0 , 10 ) ); }
Rusty 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)); }
This example was not quite equivalent, since in Rust, the standard library extensions are approached with caution, as a result, for the sake of many “simple” things, you have to turn to third-party libraries. The decision is ambiguous, but it has its advantages. In any case, it is quite easy to connect libraries thanks to the convenient Cargo package manager.
Well, working with random values is more verbose, recalls how it was done in C ++, though this is a pretense to the library.
Unfortunately, play.rust-lang does not support external packages, so it’s impossible to experiment with an example.
Imports
D module main; import std.stdio, std.math; void main() { import std.conv;
Rusty use std::{path, env}; fn main() { use std::convert;
Rust will allow grouping imports with a common root. Unfortunately, relative imports cannot be used in such imports:
use std::{path, env::args};
Imports can also be not only at the beginning of the file, but they should be located at the very beginning of the block.
Exported names
I recall that in D everything, by default, is considered public (except for imports of the module itself), if you wish, you can specify private. Rust also prefers explicitness in this matter: only entities marked with the keyword pub are exported.
Example :
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};
Functions
D module main; import std.stdio; int add( int x , int y ) { return x + y; } void main() {
Rusty fn add(x: i32, y: i32) -> i32 { x + y } fn main() { println!("{}", add(42, 13)); }
Rust does not allow using functions as methods, although the opposite is possible. Plus, the methods are implemented from the outside, so that they (as well as the implementation of the traits) can be added to existing types. Generalized programming available:
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 }
Rusty 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));
Here we literally say: the function add takes two parameters T1 and T2 and returns the type Result, where for the type T1 an addition with the type T2 is implemented, which returns Result. In this example, the difference in approaches is best seen: we sacrifice conciseness and, in part, flexibility for the sake of “clarity” and more convenient error messages — because of the need to specify restrictions on types, the problem cannot leak through many levels, generating a bunch of messages.
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 );
Rusty 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); }
Unpacking looks exactly like an ad, since
let
is a complete pattern match.
Named return values
In Rust, as in D, there are no named return values. There are also no tuples with named arguments. However, the latter seem to me a strange thing - why, in that case, not to use structures? ..
By the way, in both languages there are no named parameters of functions. It's funny that they can appear in both
D and
Rust .
Variable
D module main; import std.stdio; void main() { bool c; bool python; bool java; int i; }
Rusty fn main() { let c: bool; let python: bool; let java: bool; let i: i32; }
In Rust, the compiler prohibits calls to non-initialized variables.
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 );
Rusty fn main() { let (i, j) = (1, 2); let k = 3; let (c, python, java) = (true, false, "no!"); println!("{}, {}, {}, {}, {}, {}", i, j, k, c, python, java);
Both languages are able to infer types, but in Rust the type can be inferred not only from the declaration, but also from its
use :
fn take_i8(_: i8) {} fn take_i32(_: i32) {} fn main() { let a = 10; let b = 20; take_i8(a);
Such an example looks somewhat contrived, but the possibility is convenient if we pass to a function (or return from) an incompletely specified type.
Basic types
Type mapping table:
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
In Rust base types, again, the most necessary minimum, for more exotic things you have to go to the library. Properties associated with types in Rust are defined as constants (
for example, f64 ).
In comparison, the "more cunning" types do not participate, there is a
funny table where you can look at them.
and be terrified .
Zero values
D module main; import std.stdio; void main() { writefln( "%s %s %s \"%s\"" , int.init , double.init , bool.init , string.init );
Rusty fn main() { println!("{} {} {} '{}'", i32::default(), f64::default(), bool::default(), String::default());
In Rust, types can implement the
Default trait if they have a meaningful default value. As already mentioned, the compiler monitors access to non-initialized variables, so there is no special point in automatically initializing them.
Type conversions
There are no implicit type conversions in Rust. Even those that can be done safely - without loss of accuracy. Recently, I just had a dispute with a friend and he argued that coercion of integer types to floating-point numbers should be explicit, as well as coercion to platform-dependent types (such as size_t). I completely agree with the latter - it is not very convenient when a warning only pops up with different compilation settings. As a result, than to make entangled rules it is always better to demand an explicit indication of the type - the decision is quite in the spirit of the philosophy of language.
let a: i32 = 10; let b: i64 = a as i64;
Numeric constants
D enum Big = 1L << 100;
Rusty let a = 1 << 100;
By the way, Rust in the debug build keeps track of overflows during arithmetic operations. In release, for the sake of performance, checks are disabled, although there is a way to explicitly enable / disable them, regardless of the type of assembly.
Of course, as in the case of D, when rewriting such simple examples from another language, it is not always possible to fully reveal the advantages / features. For example, algebraic data types, pattern comparison, and macros are left out of frame.