So, you are learning a programming language… you know what that means?
fn main() {
println!("Hello World!");
}
Yes, it is that simple. As you can see, functions are declared using the generic fn
keyword and for the C habitués, the return type – if any – is specified as in ML, after an ASCII arrow, e.g., fn random() -> int { ... }
.
To try it out on your own, save this code snippet and call rustc
!
Now let’s try something funny:
fn main() {
let out = "Hello World!";
for c in out.chars() {
/* spawn concurrent tasks! */
spawn(proc() {
print!("{}", c);
});
}
}
First, notice the loop syntax: for (each) element
in container
, as in Python.
Here, we make use of Rust’s iterator type with .chars()
to successively host each char of the string (it used to be .iter()
) so we can print those directly.
What if we wanted to simply iterate a said number of times? Well, for that we have a special container, the range()
iterator.
// Yields an iterator that will do `n` times, from 0 to n-1
// Note: using just `range(n)` is not possible just yet
for i in range(0, n) {}
// This one will go from 1 to n
for _ in range(1, n+1)
As you can see, range()
has exclusive upper bound, to be in line with 0-based indexing.
Notice the wildcard _
in the second example, which is used if we don’t want to reference the iterator for use as a local variable in the loop.
BTW, let’s look at the conditional while
loop right now:
let mut cake = have_my_cake();
while cake > 0 {
cake -= 1; // eat it
}
Note that for infinite loops, the loop
keyword is to be preferred to while true { }
(which is essentially a hack). Also, Rust doesn’t have the do { } while;
syntax; you can just use a loop
with a breaking condition instead – break
will exit a loop while continue
will jump to the next iteration.
Now, before looking at the type syntax itself, let’s look at the output:
Hllo World!
e
Well, we did spawn
a few concurrent tasks. Things are done asynchronously, which means that one of the tasks may unpredictably end up before the other. Anyways, spawning threads to print out the letters of a sentence is pretty silly!
Note: proc()
is a one-shot closure, the latter being a nested function that captures its surrounding environment (i.e. one can access the variables of the containing namespace).
As you can see, variables are declared via the let
keyword and type specification is optional (~ML) because Rust has a type inference mechanism, which means that the variable type is determined at compile time – not to be confused with dynamic typing, as in Python, where the variable type is managed at run-time and can be dynamically modified.
Note: the convention is that variables and function names are_lowercase while type names are CamelCase.
Let’s have a look at some of the primitive types:
let a = 3; // considered as `int` by default
let b = "code"; // that would be an `str` - string
let c = [1, 2, 3]; // and that's a `vec` - array (of fixed-length)
let d = true; // bool
Rust variables are immutable by default in order to avoid some common errors, so you need to append the mutable mut
keyword to let
in order to reassign a value – the compiler will warn you if there are unused mut
s in your code.
Note: unlike C, for example, Rust does not let you edit an immutable variable via its pointer.
Speaking of type assignment, you can force the use of a particular type:
let a: uint = 3; // unsigned int
let a = 3u; // that's the same, using a short variable suffix for convenience
let mut n = 3; // `n` is supposedly an `int` here...
n = 5u; // but the compiler will infer it as an `uint` from here
let mut h = ~[]; // unknown-type `vec` - won't compile as such
h = ~[1u, 2, 3]; // a `vec` of `uint`s
There are other suffixes like i32
(32-bit integer), i64
, f32
(32-bit float), f64
… the empty type is called nil and annotated ()
. But remember, you cannot change the type of a variable during it’s lifetime – static typing!
Functions will need explicit parameters and return types through. Let’s look at one:
fn square(x: int) -> int {
x*x
}
fn main() {
let x = 3u;
/* Now, calculate the square. */
let y = square(x);
println!("So this gives us {}.", y);
}
Ok, but this code will not typecheck because we are passing an unsigned int to a function that takes an int as parameter; if we were in a situation where we cannot change the type annotation, we would have to just cast the value of x
.
Rust has various ways to change variable types, and while they all share different characteristics, they are also not all possible with all type combinations. Here is the general convention:
cast | .as_T() |
ref change-no allocation ⇒ read-only | &'a T -> &'a U |
---|---|---|---|
conversion | .to_T() .into_T() |
allocates new memory “in-place” conversion, without copying | &T -> ~U ~T -> ~U |
As you can see, .as_T()
is normally just casting a reference so that it is treating the original data as if it were a different type (for example, the .as_bytes
method allows the contents of a &str to be viewed as a sequence of bytes, &[u8]). A research on the libstd documentation will tell you which types implement which of these methods – in the present case, we would just use a x as y
to cast between primitive types, in this case x as uint
.
In Rust, instructions that do not end with semicolons are expressions (not statements), meaning that they will return their value to the higher-level block.
This allows you not only to return from a function but also – for example – to allocate a value based on a test’s result. This is valid Rust code:
let hype =
if cake == "KitKat" {
10
} else if cake == "Jelly Bean" {
7
} else {
5
};
Let’s go back on a similar example as with the spawn
before, this time using a vec
of chars:
fn main() {
let out = ['H', 'e', 'l', 'l', 'o'];
for c in out.iter() { // iterate over `out`
print!("{}", *c);
}
}
Rust supports UTF-8 (Unicode) natively through the u8
type (8-bit unsigned type); strings are essentially just ~[u8]
arrays with a few specific methods on top of it.
FYI, there is a blog article that talks about types in Rust and has some interesting facts, check it out!
So Rust uses the print!
and println!
macros (also called syntax extensions, ending with !
) in order to format-print your variables marked as {}
and passed as arguments into strings.
Note that you can specify the type manually, e.g. {:u}
means unsigned integer literal; this will permit compiler type-checking.
Also, {:?}
will conveniently raw-print any kind of variable (in particular, arrays).
Macros are defined as syntax extensions in the sense that they are like scripts, but are evaluated at compile-time. So they serve only the developer by simplifying/automating things.
Think about #define
in C, but macros are much more powerful. Macros have a few syntax elements of their own; for example, variables get referenced with $var
, so that you can differenciate macro variables – which serve “internally” – from actual, real Rust code.
Not elaborating further on it for now but you can have a look at the mainline macro tutorial for some code examples.
Iterators have methods such as .map()
, .zip()
and .filter()
– a touch of functional programming which can combine and/or reuse iterator’s values in various ways. So one can write something like iterator.map(...).zip(...)
which manages to apply a function to each of the iterator’s element (map) and append another iterator to it as a pair of elements (zip).
Here is an example using fold, which can combine an iterator’s values:
let xs = [14, 1, 5, 3, 12];
// this yields -35
let result = xs.iter().fold(0, |accumulator, item| accumulator - *item);
This approach is very practical as it keeps code concise and manages to do various kind of operations on variables quite easily.
Check out the iterators tutorial to find out more.
You will probably start quickly adding comments to your code (well, you should anyways!) so let’s speak about that now: Rust uses C-style comments and also has a doc comments system (librustdoc) that’s quite similar to Javadoc, for example.
Code comments | Global doc comments | Block-level doc comments | |
---|---|---|---|
inline multiline | // yuck! /* yuck! */ |
//! foo /*! foo */ |
/// bar /** bar */ |
The built-in doc generator (librustdoc) will make pretty HTML documentation out of these: check out how it looks with the libstd documentation!
All comments accept the Markdown syntax and code snippets inside backticks, like so:
/*
* This file is part of my software,
* licensed under CC0 public domain license.
*/
//! Okay, this file is about providing an
//! interface to the serial device I'm using.
//! All relevant functions are here.
/// This functions returns the current status.
/// true: ok
/// false: err
///
/// # Example
///
/// ```
/// if !reachable() {
/// fail!("Device not responding!");
/// }
/// ```
fn reachable() -> bool {
foo
}
You can learn more about rustdoc intrinsics here.
Hmm, do you see that fail!
macro in the example above?
It is part of a stack of debugging macros, we will have a look at that in the next chapter.