An Example Program Using Structs
To understand when we might want to use structs, let’s write a program that calculates the area of a rectangle. We’ll start by using single variables, and then refactor the program until we’re using structs instead.
Let’s make a new project with Scarb called rectangles that will take the width and height of a rectangle specified in pixels and calculate the area of the rectangle. Listing 5-8 shows a short program with one way of doing exactly that in our project’s src/lib.cairo.
Filename: src/lib.cairo
#[executable]
fn main() {
let width = 30;
let height = 10;
let area = area(width, height);
println!("Area is {}", area);
}
fn area(width: u64, height: u64) -> u64 {
width * height
}
the area of a rectangle specified by separate width and height variables.
Now run the program with scarb execute:
$ scarb execute
Compiling listing_04_06_no_struct v0.1.0 (listings/ch05-using-structs-to-structure-related-data/listing_03_no_struct/Scarb.toml)
Finished `dev` profile target(s) in 1 second
Executing listing_04_06_no_struct
Area is 300
This code succeeds in figuring out the area of the rectangle by calling the
area function with each dimension, but we can do more to make this code clear
and readable.
The issue with this code is evident in the signature of area:
fn area(width: u64, height: u64) -> u64 {
The area function is supposed to calculate the area of one rectangle, but the
function we wrote has two parameters, and it’s not clear anywhere in our program
that the parameters are related. It would be more readable and more manageable
to group width and height together. We’ve already discussed one way we might do
that in the
Tuple Section of Chapter 2.
Refactoring with Tuples
Listing 5-9 shows another version of our program that uses tuples.
Filename: src/lib.cairo
#[executable]
fn main() {
let rectangle = (30, 10);
let area = area(rectangle);
println!("Area is {}", area);
}
fn area(dimension: (u64, u64)) -> u64 {
let (x, y) = dimension;
x * y
}
5-9: Specifying the width and height of the rectangle with a tuple.
In one way, this program is better. Tuples let us add a bit of structure, and we’re now passing just one argument. But in another way, this version is less clear: tuples don’t name their elements, so we have to index into the parts of the tuple, making our calculation less obvious.
Mixing up the width and height wouldn’t matter for the area calculation, but if
we want to calculate the difference, it would matter! We would have to keep in
mind that width is the tuple index 0 and height is the tuple index 1.
This would be even harder for someone else to figure out and keep in mind if
they were to use our code. Because we haven’t conveyed the meaning of our data
in our code, it’s now easier to introduce errors.
Refactoring with Structs: Adding More Meaning
We use structs to add meaning by labeling the data. We can transform the tuple we’re using into a struct with a name for the whole as well as names for the parts.
Filename: src/lib.cairo
struct Rectangle {
width: u64,
height: u64,
}
#[executable]
fn main() {
let rectangle = Rectangle { width: 30, height: 10 };
let area = area(rectangle);
println!("Area is {}", area);
}
fn area(rectangle: Rectangle) -> u64 {
rectangle.width * rectangle.height
}
5-10: Defining a Rectangle struct.
Here we’ve defined a struct and named it Rectangle. Inside the curly brackets,
we defined the fields as width and height, both of which have type u64.
Then, in main, we created a particular instance of Rectangle that has a
width of 30 and a height of 10. Our area function is now defined with one
parameter, which we’ve named rectangle which is of type Rectangle struct. We
can then access the fields of the instance with dot notation, and it gives
descriptive names to the values rather than using the tuple index values of 0
and 1.
Conversions of Custom Types
We've already described how to perform type conversion on in-built types, see Data Types > Type Conversion. In this section, we will see how to define conversions for custom types.
Note: conversion can be defined for compound types, e.g. tuples, too.
Into
Defining a conversion for a custom type using the Into trait will typically
require specification of the type to convert into, as the compiler is unable to
determine this most of the time. However this is a small trade-off considering
we get the functionality for free.
#[derive(Drop, PartialEq)]
struct Rectangle {
width: u64,
height: u64,
}
#[derive(Drop)]
struct Square {
side_length: u64,
}
impl SquareIntoRectangle of Into<Square, Rectangle> {
fn into(self: Square) -> Rectangle {
Rectangle { width: self.side_length, height: self.side_length }
}
}
#[executable]
fn main() {
let square = Square { side_length: 5 };
// Compiler will complain if you remove the type annotation
let result: Rectangle = square.into();
let expected = Rectangle { width: 5, height: 5 };
assert!(
result == expected,
"A square is always convertible to a rectangle with the same width and height!",
);
}
TryInto
Defining a conversion for TryInto is similar to defining it for Into.
#[derive(Drop)]
struct Rectangle {
width: u64,
height: u64,
}
#[derive(Drop, PartialEq)]
struct Square {
side_length: u64,
}
impl RectangleIntoSquare of TryInto<Rectangle, Square> {
fn try_into(self: Rectangle) -> Option<Square> {
if self.height == self.width {
Some(Square { side_length: self.height })
} else {
None
}
}
}
#[executable]
fn main() {
let rectangle = Rectangle { width: 8, height: 8 };
let result: Square = rectangle.try_into().unwrap();
let expected = Square { side_length: 8 };
assert!(
result == expected,
"Rectangle with equal width and height should be convertible to a square.",
);
let rectangle = Rectangle { width: 5, height: 8 };
let result: Option<Square> = rectangle.try_into();
assert!(
result.is_none(),
"Rectangle with different width and height should not be convertible to a square.",
);
}