C/CPS 506
Comparative Programming Languages Prof. Alex Ufkes
Topic 11: Structs, enums, generic types, traits
Course Administration
© Alex Ufkes, 2020, 2021
2
© Alex Ufkes, 2020, 2021 3
Reminder: Types & Literals
4 Scalar types:
Integer – u8, u16, u32, u64, usize, i8, i16, i32, i64, isize Floating Point – f32, f64
Boolean – bool (true, false)
Character – Unicode: ‘Z’, ‘a’, ‘&’, ‘\u{00C5}’, etc
2 Compound types:
Tuple – heterogeneous Arrays – homogeneous
© Alex Ufkes, 2020, 2021 4
© Alex Ufkes, 2020, 2021 5
Structures: Similar to C/C++ Can contain heterogeneous data, just like tuples:
• Struct fields separated by commas. Even the last item ends in a comma.
• Must indicate type
• Declare struct as follows:
• Indicate values for each field
• Again, separate by commas.
• Need not declare in same order!
© Alex Ufkes, 2020, 2021
6
Structures
When declaring, must assign values to all fields:
© Alex Ufkes, 2020, 2021
7
Structures: Accessing Fields
• If we want to change struct values, must declare using mut
• Entire struct must be mutable
• Use the dot operator to access
© Alex Ufkes, 2020, 2021
8
Structures: As Return Values
• Return type is the struct name
• Function ends in an expression,
returns a Person struct.
© Alex Ufkes, 2020, 2021
9
Structures: Parameter Shorthand
If the name of the parameter is the same as the struct field, we can create the struct as seen here:
© Alex Ufkes, 2020, 2021
10
Tuple Structures
Notice:
• Fields are not named.
• We access them using numeric
index, just like a normal tuple.
© Alex Ufkes, 2020, 2021
11
Structures, References, Lifetimes
Notice:
• name is declared as a string slice (&str)
• We should be able to
initialize it with a literal
© Alex Ufkes, 2020, 2021 12
Structures, References, Lifetimes
Danger:
• This reference could point to data owned by something else
• Similar to the dangling pointer problem
• Struct might still be in scope, while the
data referenced has gone out of scope.
• We can annotate lifetimes here
© Alex Ufkes, 2020, 2021 13
Structures, References, Lifetimes
All we’re saying here is that an instance of Person won’t outlive the reference it holds in its name field.
In other words, user1 cannot outlive “Tim”
© Alex Ufkes, 2020, 2021 14
Methods VS Functions
Like functions, methods can accept parameters, return a value, and contain code that is run when called.
Unlike functions, methods are defined within the context of a struct (or trait, or enum, as we’ll also see).
© Alex Ufkes, 2020, 2021 15
Rect struct containing two fields, width and height
• Method area implemented for Rect structs. Use impl block.
• &self refers to the calling struct • self.widthandself.height
reference struct fields. • Similar to this in Java.
• Call area method for r1.
• No args since we’re just
going to reference self
© Alex Ufkes, 2020, 2021 16
Why Methods?
• Keep behavior specific to a struct organized.
• Simplify arguments through self keyword.
Function VS Method:
• Strictly speaking, we can have associated functions as well
• The difference syntactically is that a function doesn’t use the &self parameter, while a method does.
© Alex Ufkes, 2020, 2021 17
There’s more to methods, but we’ll leave it here for now until we talk about traits.
© Alex Ufkes, 2020, 2021 18
Enums
• enum is used to define a custom type and the range of values it can take.
• In this case, IpType can take the values of V4 and V6.
• In our IpAddr struct, we have a field whose type is IpType.
• Use :: to access enum values
© Alex Ufkes, 2020, 2021
19
More Concisely
• Instead of using a struct…
• We can attach data directly to
each enum variant.
• Enum values are tuple structs
© Alex Ufkes, 2020, 2021
20
© Alex Ufkes, 2020, 2021
21
Pattern Matching
(with Enums!)
Pattern Matching with match
IpType enum from before
Declare two IpTypes, one V4 variant and one V6 variant.
© Alex Ufkes, 2020, 2021
22
Pattern Matching with match
• Call a function for printing IP addresses.
• Problem: How we print depends on version
© Alex Ufkes, 2020, 2021
23
Pattern Matching with match
IpType arg, no return value
Pattern match input arg
© Alex Ufkes, 2020, 2021
24
Pattern Matching with match
V4 case. Notice four values for members of V4
V6 case. Single value for String
© Alex Ufkes, 2020, 2021
25
Rules: match
Like Haskell, match structures must be exhaustive:
© Alex Ufkes, 2020, 2021
26
Rules: match
Also like Haskell, underscores are wild:
© Alex Ufkes, 2020, 2021
27
Nested Clauses
• If/else inside a match case
• Result of if/else is an expression
• Will result in a String
© Alex Ufkes, 2020, 2021
28
Enum: Option
Rust defines an Option enum. Used to test if a value is something or nothing Just like Maybe in Haskell!
What’s wrong with NULL?
• It’s easy to accidentally use NULL as a non-NULL value
• Dereference NULL pointers, use NULL value in computation
• May or may not cause run-time errors
• The concept of NULL is pervasive. Easy mistake to make.
© Alex Ufkes, 2020, 2021 29
Option
• NULL is a very useful concept.
• The problem is its implementation
• Default integer value instead of unique type.
• Haskell implements Maybe, Rust implements Option
data Maybe a = Just a
| Nothing
enum Option
None,
}
T is the Rust equivalent, a generic type parameter
© Alex Ufkes, 2020, 2021
30
a is a type variable
Option
It’s built into Rust, we can just use it
Notice:
• When using None, we specify a type.
• Rust can’t infer a type from None.
© Alex Ufkes, 2020, 2021
31
• This highlights the difference between using NULL vs Option or Maybe:
• We can’t implicitly convert Option for arithmetic or other operations.
• Thus, we get a compile error
• With NULL, code compiles perfectly fine because it’s just a value.
• Causes runtime errors that are more dangerous, and trickier to track down.
• The burden is still on the programmer to identify when Option should be used.
© Alex Ufkes, 2020, 2021 32
Pattern Matching with match T from Some(T)
• Very similar to Haskell
• Extract value x from Some(x)
• Decide on some default value for None
• In this case, we say 0. Sensible because
we’re just using v1_val in an addition.
• Notice we’re treating the match
structure as an expression
• Just like we saw with if/else-if/else
© Alex Ufkes, 2020, 2021
33
© Alex Ufkes, 2020, 2021 34
Generic Types
Consider a function that finds the largest item in an array:
• Rust is strongly, statically typed.
• This function will only work for
arrays of type i32.
• What about: i8, i16, i64, isize, u8,
u16, u32, u64, usize, f32, f64, or
even char?
• Do we really need a different function for each type?
With what we know of Rust, you’d be forgiven for thinking that we did.
© Alex Ufkes, 2020, 2021 35
Generic Types
It can be done!
• T is a generic data type.
• We’re not done yet though:
© Alex Ufkes, 2020, 2021
36
• Function won’t work on all possible types T could take.
• We’re doing comparison, T must be a type that can be ordered. • std::cmp::PartialOrd is a trait.
• Recall: Traits in Rust are directly inspired by type classes in Haskell
© Alex Ufkes, 2020, 2021 37
Let’s see some more about generic types and traits, then we’ll come back to this max_val function.
© Alex Ufkes, 2020, 2021 38
Generic Types: Structs
• Here, it doesn’t matter what type T is
• We’re not operating on it in any way
© Alex Ufkes, 2020, 2021
39
Points declared using int, float, and even Strings
However…
© Alex Ufkes, 2020, 2021
40
Type of x and y must match
Generic Types: Struct Methods
• Here, we’re moving ownership (no &)
• That’s OK, we’re sending a new Point
back to main.
• It doesn’t matter what type T is,
we’re allowed to create a new Point
© Alex Ufkes, 2020, 2021
41
Generic Types: Struct Methods
• Here, we implement a vec_len method for Point.
• However, it is only implemented when T is f64.
• If we left it generic, we’d get a compile error.
• sqrt() and * aren’t defined for all possible T
• Speaking of sqrt(), that’s weird…
• We’re calling it as a method, rather than a function.
• sqrt() is implemented over type f64
© Alex Ufkes, 2020, 2021
42
Method vec_len implemented for Point, but only when T is f64
Method swap_coords implemented for any type T. Old point is destroyed, new point with swapped coords is returned
• Add as many as we want
• They can be implemented for
specific types, or generic types.
• Whatever the case, operations
on the type must be defined.
© Alex Ufkes, 2020, 2021 43
Struct with two values of generic type T
Generic Types: Enums
• Here’s an example with two generic types.
• Ok can have a different type from Err
• Simply add more type variables in the definition
© Alex Ufkes, 2020, 2021
44
• •
Problem: These types can’t stay generic forever.
Rust is statically typed – has to know concrete type at compile time if we’re using an instance of Result.
Generic Types: Enums
• By providing 5 with Ok, that tells rust the type of T.
• It says nothing about E.
© Alex Ufkes, 2020, 2021
45
Problem? Can Rust infer types?
Generic Types: Enums
But… Why should it matter what type E is, if _r1 is Ok(T)?
© Alex Ufkes, 2020, 2021 46
Now, for each Result variable, we’ve specified the type of T and E.
mut
• If _r1 is mutable, it can take the value of Ok(T) or Err(E)
• Thus, Rust must know the type
of each at compile time.
© Alex Ufkes, 2020, 2021
47
© Alex Ufkes, 2020, 2021
48
Traits
Traits
A trait tells the Rust compiler about functionality a particular type has
Let’s revisit our max_val function:
© Alex Ufkes, 2020, 2021
49
• In max_val, we compare two values of type T.
• This behavior isn’t defined for all possible values of T.
• Comparison is defined for types with the PartialOrd trait.
• Just like Haskell’s Ord type class
© Alex Ufkes, 2020, 2021 50
• Tell Rust we want max_val to accept any type T that implements the PartialOrd trait.
• We’re almost there…
© Alex Ufkes, 2020, 2021 51
Roughly speaking:
• Not all types implementing PartialOrd are stored on the stack
• There is still potential here for T being a type stored on the heap (i.e. String)
• This error comes from us trying to (potentially) copy a heap variable.
• Recall, of Strings:
© Alex Ufkes, 2020, 2021 52
• Ownership moved from s to word!
• s is now invalid!
• This is very different from any other
language we’re used to.
• This doesn’t happen with primitives
because they will simply be copied.
• The String is not copied! Its
ownership is moved!
© Alex Ufkes, 2020, 2021 53
We can fix this! We have the technology!
Now, max_val will work on any type T that implements Copy and PartialOrd traits.
© Alex Ufkes, 2020, 2021
54
© Alex Ufkes, 2020, 2021 55
• Interesting, I thought Copy wasn’t implemented for String?
• Except, these are not Strings. They’re literals, which means they’re &str • CopyandPartialOrdareimplementedfor&str
© Alex Ufkes, 2020, 2021 56
?
© Alex Ufkes, 2020, 2021 57
Traits and Methods
Clunky. Let’s create a method we can call that will print our Points.
© Alex Ufkes, 2020, 2021
58
• Makes sense right? Except…
• We’re assuming that Rust knows
how to print every type T.
• Turns out this isn’t the case.
Not to worry! Display is a trait. We know how to use traits.
© Alex Ufkes, 2020, 2021 59
First time seeing:
• Similar to a “using namespace” statement in C++
• Include the Display trait in the impl definition.
• Now, the print method will work for any type T that implements Display
© Alex Ufkes, 2020, 2021 60
Implement Traits For Custom Types?
Everything we’ve seen regarding traits so far has been about restricting functions or methods to types with specific traits.
What if we want to create a new a trait for a certain type?
© Alex Ufkes, 2020, 2021 61
• Create a new trait (using trait keyword) called PointOps.
• This trait will contain operations on our Pt3D data type
• Initially, we’ll start with a simple method for addition.
• This method is invoked from some type, accepts that type as an argument, and returns that type
• T = T.plus(T)
• This is just a method signature!
• What about the implementation?
• &self is a reference to the calling variable
• • •
Self is the type of self Keeps this trait generic! Any type can implement it
© Alex Ufkes, 2020, 2021
62
• We’re implementing the PointOps trait for our Pt3D type
• Inside the impl we find our method definition(s)
• We create and return a new Pt3D whose elements are the sum of the Pt3D that invoked the method, and the Pt3D passed in as an argument.
• Notice this implementation is specific to Pt3D
• Sensible, since we’d need different behavior for different types
© Alex Ufkes, 2020, 2021
63
• •
If we implement a trait for a particular data type, we are required to implement all methods Compile error otherwise:
© Alex Ufkes, 2020, 2021
64
• Let’s test our plus method
• Declare two points, call the plus
method on p1, pass in p2 as
argument.
• Store the result in a new point, p3.
• Print p3.
• Unacceptable. Let’s create and implement a print method in our PointOps trait
© Alex Ufkes, 2020, 2021
65
© Alex Ufkes, 2020, 2021 66
By the way:
• If we want formatted output a la printf, we can use format! Instead of println!
• Feel free to Google it, usage is fairly straightforward.
© Alex Ufkes, 2020, 2021 67
We’ve implemented our custom PointOps trait for Pt3D Can we implement it for another type? How about Pt2D?
© Alex Ufkes, 2020, 2021 68
• • •
Our trait can stay the same.
It’s already generic enough for Pt2D.
Self can be anything!
© Alex Ufkes, 2020, 2021 69
• This time, impl PointOps for Pt2D
• Functionality is similar
• We’re dealing with a 2D point
instead of 3D.
© Alex Ufkes, 2020, 2021 70
© Alex Ufkes, 2020, 2021 71
Implement Existing Trait For Custom Types?
We can restrict functions or methods to types with specific traits. We can create new traits.
What if we want to implement an existing trait for a certain type? For example, the Copy or Display trait for Pt
© Alex Ufkes, 2020, 2021
72
Two Options: #derive Recall:
• p1 is moved to p2
• We can no longer use p1!
© Alex Ufkes, 2020, 2021
73
• p1 is moved to p2
• We can no longer use p1!
Ownership – Three Rules:
1. Each value in Rust has a variable that’s called its owner. 3. When the owner goes out of scope, the value is dropped.
2. There can only be one owner at a time.
© Alex Ufkes, 2020, 2021 74
• p1 is moved to p2
• We can no longer use p1!
• Move occurs because Pt2D doesn’t implement Copy.
• Variables are moved by default, unless they implement Copy
• Can we implement Copy?
© Alex Ufkes, 2020, 2021 75
Implementing Copy
Rule: A struct can implement Copy if its components implement copy.
• The elements of Pt2D are just f64.
• They certainly implement copy.
• We should be OK
© Alex Ufkes, 2020, 2021 76
#derive
• Huh? We can’t implement Copy without implementing Clone?
• Clone is a supertrait of Copy
© Alex Ufkes, 2020, 2021
77
#derive
• We’re now copying instead of moving
• p1, p2 are different values in memory
• Can still make use of p1!
© Alex Ufkes, 2020, 2021
78
Rule: A struct can implement Copy if its components implement copy.
• We already know that String doesn’t implement Copy.
• What happens if we try?
© Alex Ufkes, 2020, 2021 79
#derive Display?
© Alex Ufkes, 2020, 2021
80
Nope. But we can implement it ourselves!
Implement Display?
• Doing so requires us to know what methods the Display trait requires.
• The method signature for displaying a type using { } in a println! Is as follows:
Format your output as desired here
© Alex Ufkes, 2020, 2021 81
© Alex Ufkes, 2020, 2021 82
Traits are a powerful mechanism for achieving type polymorphism and custom type behavior in Rust.
Traits are directly inspired by Haskell’s type classes, and it shows.
Haskell type classes
• Must (should) implement minimal set of operations
• Can derive existing type classes
• Can implement existing type class
Rust traits
• Must implement methods described in trait definition
• Can derive existing traits
• Can implement existing traits
© Alex Ufkes, 2020, 2021
83
Fantastic Rust Reference:
https://doc.rust-lang.org/book/second-edition/
© Alex Ufkes, 2020, 2021 84
© Alex Ufkes, 2020, 2021 85