From 59d230da3d67036f55c2aacdcd29f4d89f193077 Mon Sep 17 00:00:00 2001 From: Martin Sulzmann Date: Tue, 17 Dec 2024 12:54:41 +0100 Subject: [PATCH] Add files via upload --- lec-rust.html | 688 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 688 insertions(+) create mode 100644 lec-rust.html diff --git a/lec-rust.html b/lec-rust.html new file mode 100644 index 0000000..f4726d9 --- /dev/null +++ b/lec-rust.html @@ -0,0 +1,688 @@ + + + + + + + + + The Rust programming language + + + + + +
+

The Rust programming language

+

+Martin Sulzmann +

+
+
+

The Rust programming +language

+ +
+
+

Variables, functions and type inference

+

Functions

+
fn inc(x : i32) -> i32 {
+    return x+1;
+}
+

Local type inference

+
fn inc2(x : i32) -> i32 {
+    let y : i32 = x;       // Local variables
+    return y+1;
+}
+
+fn inc3(x : i32) -> i32 {
+    let y = x;             // Type inference for local variables
+    return y+1;
+}
+

Variables are immutable by +default

+
fn inc4(x : i32) -> i32 {
+    let y = x;             // Variables are immutable by default
+    y = y + 1;             // Yields compiler error "cannot assign twice to immutable variable `y`"
+    return y;
+}
+

Mutable variables +must be declared explicitly

+
fn inc5(x : i32) -> i32 {
+    let mut y = x;
+    y = y + 1;             // Mutable variables must be declared explicitly
+    return y;
+}
+

“Lambda” functions (aka +closures)

+
fn lambda1() {
+    let one = 1;
+    let inc = |x : i32| -> i32 { let y = x+one; return y; };
+    println!("\n{}",inc(1));
+}
+

Closures take ownership

+
fn lambda2() {
+    let add = |x : i32, y : i32| -> i32 { return x + y; };
+    println!("\n{}",add(1,2));
+
+    // Multiple arguments. Type inference for return types.
+    let plus = |x : i32|  { return move |y : i32|  { return x + y; }; };
+
+    println!("\n{}",plus(1)(2));
+}
+ +

Runnable code

+

Link +to Rust playground

+
+
+

Ownership

+

Rust has call-by-value semantics like C++. For managing heap +allocated memory, Rust uses a C++ style copy/move semantics. The +difference to C++ is that the Rust compiler checks that there cannot be +any memory violations.

+

Strings are allocated on the +heap

+
fn str(x : String)  {
+    println!("{}",x);
+}
+
+fn test1() {
+    let s = String::from("12345");
+    str(s);
+}
+ +

Ownership transfer

+
fn test2() {
+    let s = String::from("12345");
+    str(s);
+    str(s);  // Compiler error "use of moved value: `s`"
+}
+ +

Ownership types

+

Rust uses a refined type system to carry out ownership +checks.

+
fn test2() {
+    let s = String::from("12345");   // s : String_Owner
+    str(s);                          // s : String_Not_Owner
+    str(s);                          // Type error invalid access of s
+}
+

Cloning and borrowing

+

Like in C++, we can explicitly apply the copy semantics by using the +clone method.

+
fn test3() {
+    let s = String::from("12345");  // s : String_Owner
+    str(s.clone());
+    let s2 = s.clone();             // s2 : String_Owner
+    str(s2);
+    println!("{}",s);
+}
+

Always cloning an object seems overkill. In particular, if only +require read access like in case of function +str.

+

There is a way out here.

+
fn strB(x : &String)  {             // "borrow" Annotation
+    println!("{}",x);
+}
+
+fn test4() {
+    let s = String::from("12345");
+    strB(&s);                       // "borrow" Annotation
+    strB(&s);
+}
+ +

Borrow annotations guarantee that the object can only be read.

+
fn strB2(x : &String)  {
+    x.push('a');           // Yields compiler error
+    println!("{}",x);
+}
+
+fn str3(x : String)  {
+    let mut y = x;
+    y.push('a');          // Okay
+    println!("{}",y);
+}
+

Runnable code

+

Link +to Rust playground

+

Short summary

+ +
+
+

Data types and pattern matching in Rust

+

We show how to implement a stack-based virtual +machine (VM) to support a simple expression language.

+

VM codes

+

We first consider the virtual machine. We make use of Rust enums to +represent VM code.

+
pub enum OpCode {
+    PUSH {
+        val : i32
+    },
+    PLUS,
+    MULT
+}
+

Rust enums can take arguments (identified via a label/attribute +name).

+

VM interpreter

+

We write an interpreter where we make use of Rust vectors to (a) +represent a sequence of VM instructions (codes) and (b) represent the +stack to carry out the computation of VM instructions.

+
fn is_some<T>(x : Option<T>) -> T {
+    match x {
+        Some(v) => { return v; }
+        _ => { panic!(); }
+    }
+}
+
+fn run(op_codes : &Vec<OpCode>) -> i32 {
+
+    let mut st = Vec::new();
+
+    for op in op_codes.iter() {
+        match op {
+            OpCode::PUSH { val } => { st.push(*val); }
+            OpCode::PLUS => {
+                match st.pop() {
+                    Some(v_right) => {
+                        match st.pop() {
+                            Some(v_left) => { st.push(v_left+v_right); }
+                            _ => { panic!(); }
+                        }
+                    }
+                    _ => { panic!(); }
+
+                }
+            }
+            OpCode::MULT => {
+                // Shorter code using the helper function.
+                let v_right = is_some(st.pop());
+                let v_left = is_some(st.pop());
+                st.push(v_left*v_right);
+            }
+        }
+    }
+
+    let res = is_some(st.pop());
+    return res;
+}
+

Optional data type

+

The vector data type acts like a stack by using operations +push and pop.

+

A pop operation either yields some result or none.

+

Pattern matching

+

We can observe the various shapes a data type can have by using a +form of pattern +matching.

+

Consider the helper function

+
fn is_some<T>(x : Option<T>) -> T {
+    match x {
+        Some(v) => { return v; }
+        _ => { panic!(); }
+    }
+}
+

where the option type is defined as follows

+
pub enum Option<T> {
+    Some { val : T},
+    None
+}
+

The pattern cases connected to a match expression allows +us to observe the various shapes of values.

+

For example, the pattern case

+
        Some(v) => { return v; }
+

applies if there is “some” value. Pattern variable v +refers to the underlying value.

+

The pattern case _ refers to a “don’t care” pattern and +always applies.

+

Keep in mind that pattern cases are tried in textual order (from top +to bottom). Hence, the don’t care case always comes last.

+

Examples

+
fn test_vm() {
+    let mut os = Vec::new();
+    os.push(OpCode::PUSH{ val : 1 });
+    os.push(OpCode::PUSH{ val : 2 });
+    os.push(OpCode::PUSH{ val : 3 });
+    os.push(OpCode::MULT);
+    os.push(OpCode::PLUS);
+
+    println!("{}", run(&os));
+}
+

Expression data types

+

Next, we consider expressions that are composed of integers, addition +and multiplication. We again make use of Rust enums to represent the +abstract syntax of our expression language. What abstract means +will be explained shortly.

+
pub enum Exp {
+    Int {
+        val: i32
+    },
+    Plus {
+        left: Box<Exp>,
+        right: Box<Exp>
+    },
+    Mult{
+        left: Box<Exp>,
+        right: Box<Exp>
+    },
+}
+ +

Rust enums are a well-known concept and commonly referred to as algebraic data +types.

+

For example, the expression 1 * (2 + 3) can be +represented as follows

+
    let e2 = Exp::Mult{left : Box::new(Exp::Int { val : 1 }),
+                      right : Box::new(Exp::Plus{left : Box::new(Exp::Int { val : 2 }),
+                                                 right : Box::new(Exp::Int { val : 3})})};
+

In terms of some concrete syntax +written 1 * (2 + 3) where where parentheses are necessary +to capture the precedence among (sub)expressions.

+

In the abstract syntax +representation we use Rust enum constructors Int, +Plus and Mult and parentheses are omitted.

+

Expression +evaluation via pattern matching

+

We make use of pattern matching to implement an evaluator for our +expressions.

+
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),
+    }
+}
+

The second case Exp::Plus { left, right } refers to an +expression that is composed using + where (pattern) +variables left and right refer to the +operands.

+

Examples

+
fn test_exp() {
+
+    // 1 + (2 * 3)
+    let e = Exp::Plus{left : Box::new(Exp::Int { val : 1 }),
+                      right : Box::new(Exp::Mult{left : Box::new(Exp::Int { val : 2 }),
+                                                 right : Box::new(Exp::Int { val : 3})})};
+
+    println!("{}", eval(&e));
+
+
+    // 1 * (2 + 3)
+    let e2 = Exp::Mult{left : Box::new(Exp::Int { val : 1 }),
+                      right : Box::new(Exp::Plus{left : Box::new(Exp::Int { val : 2 }),
+                                                 right : Box::new(Exp::Int { val : 3})})};
+
+    println!("{}", eval(&e2));
+
+}
+

Compiler

+

We write a compiler from Exp to VM instructions +(Vec<OpCode>).

+
fn compile(e : &Exp) -> Vec<OpCode> {
+    match e {
+        Exp::Int { val } => {
+            let mut os = Vec::new();
+            os.push(OpCode::PUSH { val : *val });
+            return os;
+        }
+        Exp::Plus { left, right } => {
+            let mut o1 = compile(left);
+            let mut o2 = compile(right);
+            o1.append(&mut o2);
+            let mut o3 = Vec::new();
+            o3.push(OpCode::PLUS);
+            o1.append(&mut o3);
+            return o1;
+        }
+      Exp::Mult { left, right } => {
+            let mut o1 = compile(left);
+            let mut o2 = compile(right);
+            o1.append(&mut o2);
+            let mut o3 = Vec::new();
+            o3.push(OpCode::MULT);
+            o1.append(&mut o3);
+            return o1;
+        }
+    }
+}
+ +

Complete source code

+

The complete source can be found here.

+

Permalink +to the playground

+

Direct +link to the gist

+
+
+

Traits

+ +

Traits for cloning

+
#[derive(Clone)]
+struct Square {
+    x : i32
+}
+
+fn area_sq(s : Square) -> i32 {
+    return s.x * s.x;
+}
+
+fn sq_test() {
+  let s = Square{ x : 2 };
+
+  let s2 = s.clone();
+  println!("{}", area_sq(s2));
+  println!("{}", area_sq(s));
+}
+ +

Traits for cloning and +copying

+
#[derive(Clone,Copy)]
+struct Rectangle {
+    x : i32,
+    y : i32
+}
+
+fn area_rec(r : Rectangle) -> i32 {
+    return r.x * r.y;
+}
+
+fn rec_test() {
+  let r = Rectangle{ x : 2, y : 4 };
+
+  let r2 = r;
+  println!("{}", area_rec(r2));
+  println!("{}", area_rec(r));
+  }
+ +

More on traits

+

Traits describe a collection of methods that share the same type.

+

Via some clever sugared syntax, Rust makes them look like +“objects”.

+

Geometric objects examples

+
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;
+    }
+}
+ +

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 test_area() {
+    let r = Rectangle{x : 1, y : 2};
+    let s = Square{x : 3};
+
+    println!("{}",sum_area(&r,&s));
+
+}
+ +

Traits - syntactic +sugar for method calls

+

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 test_area2() {
+    let r = Rectangle{x : 1, y : 2};
+    let s = Square{x : 3};
+
+    println!("{}",sum_area2(&r,&s));
+
+}
+

Traits are not types

+

Traits may seem like interfaces but they are not.

+ +

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 test_area3() {
+    let r = Box::new(Rectangle{x : 1, y : 2});
+    let s = Box::new(Square{x : 3});
+
+    println!("{}",sum_area3(r,s));
+}
+ +

Complete source code

+

The complete source can be found here.

+

Link +to Rust playground

+
+
+

Conclusion

+

We have seen:

+ +

How to describe Rust?

+

Rust = Haskell with deterministic memory management

+

Learn Haskell!

+

Makes it easier to comprehend Rust (and other languages such as Go, +…).

+
+ +