What is the design philosophy behind


#1

What is the design philosophy behind Rust declaration and assignment syntax and what were the problems with the traditional C/C++/C#/Java syntax that inspired them to change ?

I actually see a lot of newer languages choosing this same syntax, and I was very curious WHY ?

// Rust, Golang, Kotlin, Jai, Zig:
let number := 5;
let x: i32 = 10;

// C/C++/C#/Java
int number = 5;
int x = 10;

Notice that in the C-variants, the type is explicit (except when using auto or var keywords).
In Rust, the type declaration is optional.

So… I am guessing that on the scale of verbosity-VS-conciseness, Rust choose to be more concise and easier to WRITE rather than choosing verbosity and easier to READ ?

My curiosity also extends to function parameter declarations: Whereas, with C/C++/C#/Java i am used to

// Type goes before parameterName
void example(int param) { }

with Rust:

// parametersName goes before Type
fn example(param: i32) { }

Here I see that Rust is being consistent (which is refreshing). But god, I’ve read through so much Java code that I am used to the Type being “front and center” which means that the Rust equivalent is very difficult to read (as quick as I’m used to)… indeed, I don’t really care about what the parameter’s name is… To me, that’s an implementation detail that I don’t even need to know about… For functions, methods, & constructors… I am more concerned with what the types are… so that I can fill out the signature correctly… Then coding merely becomes an easy exercise akin to Legos where you merely fit all the building blocks together… and the compiler enforces that you’ve done it correctly.

Anyways, Rust seems like an awesome language… and I am curious what people coming from other statically-typed languages think about some of Rusts design philosophy and any problems with the traditional C/C++/C#/Java that you believe inspired Rust to go against the curve.


#2

Here’s a more interesting example. Many times, the variable isn’t a nice, short, three-letter example like i32

let x: i32 = 0;

but rather, some monstrosity like this:

let w: Window<OpenGLBuffer<GL4, f64>, 3dContext<f64>> = construct_window();

this isn’t really easier to read or write. However, modern development tooling should make it easy to view the type when you need to.


#3

This goes way back; Rust takes this from functional languages.

One reason why is that it works nicer with type inference; you drop the annotation only, rather than needing to change int -> var or whatever.

Another is that let is more complex than just that:

let (x, y) = (1, "hello");

this sets x to 1 and y to “hello”, with type annotations it’s

let (x, y): (i32, &str) = (1, "hello");

the types still mirror the declaration. You can even do

let (x, y): (_, &str) = (1, "hello");

to annotate just one part. This is harder with the other form…


#4

C-style type declarations tend to fall apart in more complex cases as well. Functions returning function pointers is the classic example: http://c-faq.com/decl/spiral.anderson.html


#5

At the beginning of every statement in C++, the compiler needs to figure out the answer to one incredibly difficult question:

Is this a declaration?

If it is a declaration, then the thing it is parsing at the very beginning of the statement is a type. If it is not a declaration, then the thing it is parsing is an expression. Both of these are complicated, recursive syntactical forms, and honestly, I have no clue how C++ can even manage to make extensions to the language possible when any statement can start with just about literally anything. (somehow they manage)

C++ has it even worse than most, with troubles like everybody’s favorite “most perplexing parse:”

Type foo(a, b); // calls 2-arg constructor of Type to initialize foo
Type foo(a); // calls 1-arg constructor of Type to initialize foo
Type foo(); // uhm... defines foo as a function pointer type.

…but that’s really specific to C++, where declarations use some bizarro hybrid syntax that embeds the identifier somewhere inside the type. I doubt that languages like Go (which are truly “type-first”) fall into this trap.

(Edit: Oops, Go is name-first)


In Rust, the type declaration is optional.

So… I am guessing that on the scale of verbosity-VS-conciseness, Rust choose to be more concise and easier to WRITE rather than choosing verbosity and easier to READ ?

Well… in some ways. Perhaps not all. Rust borrows the idea of type inference from functional languages, because:

  • it makes adding new variables and closures cheap (good because these let you avoid mutability in more places)
  • it makes adding new types to your code cheap (good because more types means more help from the type checker)

The whole process of “fitting together the building blocks” still takes place in Rust, it’s just done with the assistance of the type checker. If I decide to change some i32 to an Option<i32>, I usually just change its type in one place and let the type errors guide me through the rest, until I know the change has been accounted for everywhere that it needs to be.


#6

Keyword prefix or name always in the beginning avoids parsing ambiguity. In C it’s not possible to know just from the syntax whether:

foo * bar;

is a declaration of a variable bar of foo* type, or multiplication of foo by bar.


#7

It’s really, really convenient to have a keyword at the beginning that tells you what’s coming next. (See struct, let, fn, where, …) That makes it easier to write the parser (less guessing), makes it easier to give good error messages (because if they said struct they probably meant it), and even helps humans (by making it easy to visually scan for those usually-highlighted keywords).


#8

Go isn’t type-first (it’s name-first but without a colon between name and type). Java and C# are truly type-first though, among others.


#9

The Clockwise/Spiral Rule is very helpful is understanding the why a change was needed. Thanks!


#10

Sorry, I should have clarified that I wasn’t questioning the need for the let keyword… as I merely speculated that it made parsing easier… as you pointed out.

My question was primarily concerned with the difference & reason for having the adjective (Type annotation) before the noun (identifier) VERSES having the adjective after the noun


#11

Ah, I see.

I think it’s because it puts the introduced name in the most predictable position. When you have a huge blob of

std::vector<std::unique_ptr<Task>> v = { std::make_unique<Task>(3) };
std::unordered_map<std::string, std::vector<std::unique_ptr<Task>>> foo { { std::string("a"), v } };

it becomes much harder to even find the names of the variables, which are far more important than their types. Compare with

auto v = std::vector<std::unique_ptr<Task>>{ std::make_unique<Task>(3) };
auto foo = std::unordered_map<std::string, std::vector<std::unique_ptr<Task>>>{ { std::string("a"), v } };

Note that there’s fairly popular guidance even in C++ to always declare like that:

And C++ now offers

using GoodContainer = std::vector<int>;

following the keyword-name-rest pattern, despite having the previous order available as

typedef std::vector<int> GoodContainer;

FWIW, I’ve started doing that in C# as well, even to the point of

var text = (string)null;

#12

Putting the type after the identifier is basically a prerequisite for concise type inference.

If you put the type first, then the language parser will expect a type, and you will need a “this is not a type” disambiguation keyword like C++'s auto. This looks redundant and unnecessary when type inference is the norm, as in Rust.

In contrast, with the “IDENT: TYPE = VALUE” syntax, if you remove the type the parser will easily tell the difference because it will see an equal sign where a colon is expected. So no extra syntax is needed.

As for why type inference, as you say in the introductory post… let’s just say that there are types which we do not want to spell out (Iterators, Futures, any kind of complex generic struct really…) and types which we cannot spell out at all (closures…).


#13

I would only like to add a comment on this traditional/newer distinction.
If you take a look eg. at the Rosetta Code site, you will see the impressive amount of various syntaxes used by languages old and new.

In particular Ada, Eiffel, Modula, Pascal - not to mention PL/I - aren’t especially new languages.


#14

I think that most people misunderstand the syntax of pointers, arrays, and function pointers in C. The fundemental rule to C’s variable syntax is that a variable is declared exactly as it is used to get its value.

Suppose the following example.

int a = 12;
int *b = &a;

If you wanted to get the value of b, you would just copy how it is declared.

int c = *b;

In a slightly more complex array:

int *array[15] = { ... };
int d = *array[5];

In the infamous example of function pointers in C, this fact is mirrored.

int (*getFunc())(int, int) { 
    … 
}

int result = (*getFunc())(1, 2);

It is slightly more complex to store the function pointer, but it’s mostly just replacing the getFunc() in the type with the variable’s name.

int (*a_function_pointer)(int, int) = getFunc();

int result = (*a_function_pointer)(1, 2);

The spiral rule is correct in demonstrating that types are hard to interpret. I have no clue if int *array[15] is a pointer to an array or if it is an array of pointers.