Martin Sulzmann
Rust the better C?
Statically typed/Simple form of type inference
Higher-order functions
Deterministic memory mangagement
Stack allocated by default
Heap allocations must be be explicit
Static ownership type system to guarantee there are no memory errors
fn example1(x : i32) -> i32 {
let y = x;
// y = 1; // If commented out yields an error
return x+y;
}
fn example2(x : i32) -> i32 {
let mut y = x;
let z = y;
y = 1;
return x+y+z;
}
Local type infernce, see let y = x
. Instead we could also write let y : i32 = x
Variables are immutable by default
Mutable variables must be declared explicitly
The following program is rejected
fn example3(x : String) -> String {
let y = x;
println!("{}",y);
return x;
}
because
22 | fn example3(x : String) -> String {
| - move occurs because `x` has type `String`, which does not implement the `Copy` trait
23 | let y = x;
| - value moved here
...
27 | return x;
| ^ value used here after move
Rust applies by default the move semantics we know from C++. Unlike C++, Rust statically checks that there is no access to any variable that has been moved.
Like in C++, we can explicitely apply the copy semantics.
fn example3(x : String) -> String {
let y = x.clone();
println!("{}",y);
return x;
}
In general, we could simply always perform a (deep) copy (build a clone).
Can this be down automatically? See C++ where we can overload the assignment operator and copy constructor.
Yes! Rust uses overloading based on traits (more on traits further below).
#[derive(Clone,Copy)]
struct Rectangle {
x : i32,
y : i32
}
fn example4() {
let p = Rectangle{x : 1, y : 2};
let q = p;
println!("{} {}",q.x, p.x);
}
Automatically derive the trait instances Clone and Copy (assignment) for Rectangle.
Implies a "copy" assignment operator for Rectangle
fn example6() {
let p = Square{x : 1};
let q = &p;
println!("{} {}",q.x, p.x);
}
Borrow access to the value stored in p
via a reference!
Borrowing in Rust must obey a number of rules.
fn example7() {
let mut p = Square{x : 1};
let q = &p;
p.x = 2;
println!("{} {}",q.x, p.x);
}
yields
cannot assign to `p.x` because it is borrowed
--> overview.rs:68:5
|
66 | let q = &p;
| -- borrow of `p.x` occurs here
67 |
68 | p.x = 2;
| ^^^^^^^ assignment to borrowed `p.x` occurs here
69 | println!("{} {}",q.x, p.x);
| --- borrow later used here
The following works however.
fn example8() {
let mut p = Square{x : 1};
{
let q = &p;
println!("{} {}",q.x, p.x);
}
p.x = 2;
println!("{}",p.x);
}
Traits describe a collection of methods that share the same type.
trait Shape {
fn area(s : &Self) -> i32;
}
impl Shape for Rectangle {
fn area(r : &Rectangle) -> i32 {
return r.x * r.y;
}
}
impl Shape for Square {
fn area(s : &Square) -> i32 {
return s.x * s.x;
}
}
Self
refers to the shared type
Self
is an implicit parameter of the trait Shape
In essence, the trait Shape
represents a predicate Shape(Self)
We use borrowing here to avoid copying
Summing up the area of two geometric objects.
fn sum_area<A:Shape,B:Shape>(x : &A, y : &B) -> i32 {
return Shape::area(x) + Shape::area(y);
}
fn example9() {
let r = Rectangle{x : 1, y : 2};
let s = Square{x : 3};
println!("{}",sum_area(&r,&s));
}
A:Shape
is a type constraint and states that x
implements the trait Shape
In standard logic notation written Shape(A)
Method calls are written Shape::area(x)
Rust introduces some syntactic sugar to make look method calls "nicer".
Here's the above example written using Rust's "method" notation.
trait Shape2 {
fn area2(&self) -> i32;
}
impl Shape2 for Rectangle {
fn area2(&self) -> i32 {
return self.x * self.y;
}
}
impl Shape2 for Square {
fn area2(&self) -> i32 {
return self.x * self.x;
}
}
fn sum_area2<A:Shape2,B:Shape2>(x : &A, y : &B) -> i32 {
return x.area2() + y.area2();
}
fn example10() {
let r = Rectangle{x : 1, y : 2};
let s = Square{x : 3};
println!("{}",sum_area2(&r,&s));
}
Traits may seem like interfaces but they are not.
Interfaces
Interfaces are types
A value of an interface type implements the methods described by the interface
Traits
Traits describe a collection of methods that share the same type
Traits are type constraints
In Rust, we can represent interfaces via dynamic traits.
fn sum_area3(x : Box<dyn Shape2>, y : Box<dyn Shape2>) -> i32 {
return x.area2() + y.area2();
}
fn example11() {
let r = Box::new(Rectangle{x : 1, y : 2});
let s = Box::new(Square{x : 3});
println!("{}",sum_area3(r,s));
}
dyn Shape2
effectively represents an interface
In logical notation, dyn Shape2
corresponds to exists A. Shape2(A) => A
What does Box
mean?
The memory space of an interface cannot be statically computed
It can either be a Rectangle or a Square (the size of the memory space occupied is different)
Hence, we need to allocate the (interface) value on the heap.
In Rust, this is reflected in the type via Box
pub enum Exp {
Int {
val: i32
},
Plus {
left: Box<Exp>,
right: Box<Exp>
},
Mult{
left: Box<Exp>,
right: Box<Exp>
},
}
Box
required
Otherwise, recursive definition (without indirection)
fn eval(e : &Exp) -> i32 {
match e {
Exp::Int { val } => return *val,
Exp::Plus { left, right } => return eval(left) + eval(right),
Exp::Mult { left, right } => return eval(left) * eval(right),
}
}
fn example12() {
{
let e = Exp::Int { val : 1 };
println!("{}", eval(&e));
}
{
let e = Exp::Plus{left : Box::new(Exp::Int { val : 1 }), right : Box::new(Exp::Int { val : 2})};
println!("{}", eval(&e));
}
}
We have seen:
Ownership and borrowing
Overloading via traits
Data types and pattern matching
Learn Haskell!
Makes it easier to comprehend Rust (and other languages such as Go, ...).
///////////////////////////////
// Ownership + Borrowing
fn example1(x : i32) -> i32 {
let y = x; // Local type inference.
// y = 1; // Variables are immutable by default.
return x+y;
}
fn example2(x : i32) -> i32 {
let mut y = x; // Mutable variables must be declared explicitly.
let z = y;
y = 1;
return x+y+z;
}
fn example3(x : String) -> String {
let y = x.clone();
println!("{}",y);
return x;
}
#[derive(Clone,Copy)]
struct Rectangle {
x : i32,
y : i32
}
fn example4() {
let p = Rectangle{x : 1, y : 2};
let q = p;
println!("{} {}",q.x, p.x);
}
struct Square {
x : i32
}
fn example5() {
let p = Square{x : 1};
let q = &p;
println!("{} {}",q.x, p.x);
}
fn example6() {
let p = Square{x : 1};
let q = &p;
println!("{} {}",q.x, p.x);
}
/*
fn example7() {
let mut p = Square{x : 1};
let q = &p;
p.x = 2;
println!("{} {}",q.x, p.x);
}
*/
fn example8() {
let mut p = Square{x : 1};
{
let q = &p;
println!("{} {}",q.x, p.x);
}
p.x = 2;
println!("{}",p.x);
}
//////////////////////////
// Traits
trait Shape {
fn area(s : &Self) -> i32;
}
impl Shape for Rectangle {
fn area(r : &Rectangle) -> i32 {
return r.x * r.y;
}
}
impl Shape for Square {
fn area(s : &Square) -> i32 {
return s.x * s.x;
}
}
fn sum_area<A:Shape,B:Shape>(x : &A, y : &B) -> i32 {
return Shape::area(x) + Shape::area(y);
}
fn example9() {
let r = Rectangle{x : 1, y : 2};
let s = Square{x : 3};
println!("{}",sum_area(&r,&s));
}
// Method notation known from OO.
trait Shape2 {
fn area2(&self) -> i32;
}
impl Shape2 for Rectangle {
fn area2(&self) -> i32 {
return self.x * self.y;
}
}
impl Shape2 for Square {
fn area2(&self) -> i32 {
return self.x * self.x;
}
}
fn sum_area2<A:Shape2,B:Shape2>(x : &A, y : &B) -> i32 {
return x.area2() + y.area2();
}
fn example10() {
let r = Rectangle{x : 1, y : 2};
let s = Square{x : 3};
println!("{}",sum_area2(&r,&s));
}
fn sum_area3(x : Box<dyn Shape2>, y : Box<dyn Shape2>) -> i32 {
return x.area2() + y.area2();
}
fn example11() {
let r = Box::new(Rectangle{x : 1, y : 2});
let s = Box::new(Square{x : 3});
println!("{}",sum_area3(r,s));
}
////////////////////////////////////////////////
// Data types and pattern matching in Rust
pub enum Exp {
Int {
val: i32
},
Plus {
left: Box<Exp>,
right: Box<Exp>
},
Mult{
left: Box<Exp>,
right: Box<Exp>
},
}
fn eval(e : &Exp) -> i32 {
match e {
Exp::Int { val } => return *val,
Exp::Plus { left, right } => return eval(left) + eval(right),
Exp::Mult { left, right } => return eval(left) * eval(right),
}
}
fn example12() {
{
let e = Exp::Int { val : 1 };
println!("{}", eval(&e));
}
{
let e = Exp::Plus{left : Box::new(Exp::Int { val : 1 }), right : Box::new(Exp::Int { val : 2})};
println!("{}", eval(&e));
}
}
fn main() {
println!("Hello {} {}", 5, "World!"); // Type of placeholders inferred.
println!("{}", example1(5));
println!("{}", example2(5));
println!("{}", example3(String::from("Hallo")));
example4();
example5();
example6();
example8();
example9();
example10();
example11();
example12();
}