I'll start with a simple feature: type inference. Along with structural typing, this allows Rust to express some pretty powerful concepts without the boilerplate seen in other languages. Rust features what I call feed-forward type inference, which is summed up by this rule: Variables have no type until they are assigned a value, at which point their type can't change.
Let's take a look at some simple examples to see how this works.
auto x = 5;
auto y = "foo";
Here, we see the auto keyword, which declares a variable and leaves the compiler to figure out the type. In this form, it's similar to var in C# or Go. Because a value is immediately assigned to the variable, Rust infers that x has the type int and that y has the type string.
Now for a more interesting example:
auto x;
if (localtime().hours >= 8) {
x = "awake!"
} else {
x = "asleep, go away."
}
log "I'm " + x;
Here, we didn't initialize x in its declaration, but this code still compiles and runs properly. This is because Rust is able to wait until x is assigned to determine its type. Unlike, say, C# (and, of course, languages like ML in which variables must be assigned a value at declaration time), Rust doesn't require that auto declarations be initially assigned a value to determine a type for them. At the same time, unlike C, the compiler will emit an error if you ever forget to assign a value to the variable in any branch of your code. For instance, if you omit the else branch above, the code won't compile.
Where type inference becomes especially interesting is in combination with structural record types. Take this very simple example (note that a couple of these APIs are currently missing, so this won't work if you try it right now):
log "Enter two vectors in the format 'x1 y1 x2 y2' and I'll give you the dot product:"
auto fields = _str.split(input.read_line(), ' ');
auto ints = _vec.map(fields, _int.of_str); // convert to ints
auto a = rec(x=ints.(0), y=ints.(1));
auto b = rec(x=ints.(2), y=ints.(3));
auto ans = a.x * b.x + a.y * b.y;
log ans;
Take a look at the two lines that begin with auto a = and auto b =. They initialize a record that wasn't defined anywhere else in the program. The C99 equivalent of this would be:
struct point {
int x;
int y;
};
...
point a = { ints[0], ints[1] };
point b = { ints[2], ints[3] };C doesn't have structural typing, so we're forced to declare the point type up front. In contrast, Rust allows you to effectively pull new record types out of thin air at any time, by simply listing their keys and values inside a rec() constructor. Combined with type inference, this affords you much of the convenience of the (roughly-)equivalent idiomatic Python declaration:
a = { 'x': ints[0], 'y': ints[1] }
b = { 'x': ints[2], 'y': ints[3] }But you gain the type safety of Rust and performance comparable to the C version. (You do lose the dynamism that Python dictionaries provide, however; for example, a["x"] isn't allowed. For that, you'll want the hashmap type available in the standard library.)
Hopefully, these examples give you a feel for the type inference in Rust and what it can provide you. Although Rust isn't a scripting language, it's designed to bring much of the convenience and readability of scripting languages (dare I say "agile development"?) to large-scale projects.