Skip to content

Rust for ActionScript developers

Matheus Dias de Souza edited this page May 11, 2024 · 19 revisions

This article is an introduction of the Rust language for ActionScript familiar people.

Cargo

Cargo is the package manager integrated in the Rust installation.

When you publish your Cargo package to the public (the crates.io website), documentation is automatically generated by the docs.rs service.

"Package" and "crate" are interchangeable terms in Rust. Crate often refers to the topmost module in your Rust application or library.

Ownership

Rust features a type model that includes ownership rules.

The types &str and String are used to represent UTF-8 encoded text. The difference between them is:

  • &str is a slice of an existing string somewhere in memory. It is a "borrow", or a reference.
  • String is a string you own.

Rust implicitly converts &String to &str, where &String is a borrow of a String.

You may convert a &str to a String with .into() or .to_owned().

Move Semantics

In Rust, a variable whose type does not implement Copy is moved out of scope once it is passed to another function or variable.

Types that do not implement Copy include String, shared pointers such as Rc<T> and Box<T> and in general "owned" types.

To avoid moving unintentionally, you either borrow (&x) or clone (x.clone()) the variable.

let x = String::new();
let y = x;
let z = x; // ERROR

When matching a pattern in, for example, a Rc<T> value, you may want to invoke .as_ref(), which returns &T rather than &Rc<T>.

Borrowing and References

Borrow types, or reference "ref" types, are interchangeable terms in Rust. The borrow type &'my_lifetime T, or simply &T, represents a reference to data T at the lifetime 'my_lifetime.

Lifetimes are inferred or explicitly created in most places of Rust programs, thus omitted.

Note that the 'static lifetime is reserved and has a special meaning: the entire execution of the program.

fn m1<'my_lifetime>(argument: &'my_lifetime str) {}

// Equivalent
fn m2(argument: &str) {} 

There are "immutable" borrows and "mutable" borrows (&'my_lifetime mut T, or simply &mut T)

Mutability

Variables are immutable by default. mut is used to indicate they are immutable.

let x = 0;
let mut x = 0;

Based on explanations from the Rust community:

  • When a variable is immutable, it is transitively immutable.
  • When a variable is mutable, it is transitively mutable.

A mutable variable may be freely used as immutable variable. For example, &mut T implicitly converts to &T.

Cells

There are cases where the Rust's mutability concept is limited. You may sometimes use Cell<T> or RefCell<T> instead to contain your T data.

  • Use Cell for stack resources.
  • Use RefCell for heap resources.
use std::cell::Cell;

struct ExampleA {
    x: Cell<f64>,
}

impl ExampleA {
    pub fn new() -> Self {
        Self {
            x: Cell::new(0.0),
        }
    }

    pub fn x(&self) -> f64 {
        self.x.get()
    }
}

Destructuring and Pattern Matching

There are several ways to do pattern matching.

let group: (f64, f64) = (0.0, 0.0);
let (x, y) = group;

#[derive(Copy, Clone)]
enum E1 {
    M1(f64),
    M2,
}

let m1 = E1::M1(0.0);
let E1::M1(x) = m1 else {
    panic!();
};
if let E1::M1(x) = m1 {
    println!("{x}");
}
match m1 {
    E1::M1(x) => {
    },
    E1::M2 => {
    },
}
let q: bool = matches!(m1, E1::M1(_));

When you need to match a pattern in a Rc<T> type (that is, a reference counted type), you need to call .as_ref() in the Rc value.

// m1: Rc<E1>
match m1.as_ref() {
    _ => {},
}

Inheritance

Rust favours composition over inheritance. Further, there is no equivalent of the Java classes in Rust.

The programming patterns to use instead of inheritance where it is required vary:

  • Use a structure nested in another structure
struct ExampleA {
    example_b: Option<Box<ExampleA>>,
}
struct ExampleB(f64);
  • Use an enumeration whose variants may contain additional data
enum Example {
    A(f64),
    B {
        x: f64,
        y: f64,
    },
}
  • Use a trait. Traits are non opaque data types that may be implemented by opaque types (struct, enum, and primitive types). They are similiar to Java interfaces in certain ways.
trait ExampleTrait {
    fn common(&self) -> f64;

    fn always_provided(&self) {
        println!("Foo");
    }
}
struct Example1;
impl ExampleTrait for Example1 {
    fn common(&self) -> f64 {
        0.0
    }
}

Shared Pointers

Rc<T> (use std::rc::Rc;) represents a reference to T managed by reference counting.

Box<T> represents a heap reference to T, similiar to C++'s unique_ptr.

Deriving Traits

Traits may be automatically implemented according to a structure or enumeration through the #[derive(...)] attribute.

Implementations

struct ExampleStruct(f64);

impl ExampleStruct {
    pub fn method(&self) {
        println!("example_struct.0 = {}", self.0);
    }
}

let o = ExampleStruct(10.0);
o.method(); // example_struct.0 = 10.0

Error Handling

Unchecked exceptions that you arbitrarily throw are not a thing in Rust.

In Rust, there are "panics" (fatal exceptions or crashes) and functions that return Result<T, E>, where E is the error type and T is the result data type.

fn function_that_panics() {
    panic!("Panic message");
}

enum MyError {
    Common,
}
fn function_that_throws() -> Result<f64, MyError> {
    if true {
        Ok(10.0)
    } else {
        Err(MyError::Common)
    }
}

Propagate the error from another function by using the ? operator:

fn another_function() -> Result<f64, MyError> {
    function_that_throws()? * 2
}

Nullability

let mut x: Option<f64> = None;
x = Some(10.0);
if let Some(x) = x {
    println!("{x}");
}

Modules

lib.rs

// crate

mod ns1;

ns1.rs

mod foo;

ns1/foo.rs

pub static BAR: &'static str = "Bar";

pub mod qux {
    pub fn m() -> f64 {
        10.0
    }
}

Visibility

Everything is internal to the enclosing module by default, unless the pub qualifier is used.

There is pub, and pub(module_path).

Module paths

crate // The main module of your Rust crate
self // The enclosing module
super // The parent of the enclosing module

Importing Modules

use foo::bar::*;
pub use foo::qux::*;
use super::Something;

Constants

const versus static variables: there are certain differences between them. Also, const may appear in impl blocks, which allows for extra readability.

const EXAMPLE_1: f64 = 1_024.0 * 4;

struct ExampleStruct;
impl ExampleStruct {
    pub const EXAMPLE_CONST: &'static str = "Example string";
}

fn main() {
    println!("{}", ExampleStruct::EXAMPLE_CONST);
}

Constants are expanded ("inlined") wherever used, while statics are single memory locations.

Lambdas

Example 1:

type F<'a> = &'a dyn Fn(f64) -> f64;

let callback: F = & |a| a * 2.0;

Example 2:

fn m(callback: impl FnOnce(f64) -> f64) {
    println!("Produced: {}", callback(10.0));
}

fn main() {
    m(|a| a * 2.0);
}

Function types

There are different function types in Rust:

  • Fn(...) or Fn(...) -> T
  • FnMut(...) or FnMut(...) -> T
  • FnOnce(...) or FnOnce(...) -> T
  • fn(...) or fn(...) -> T

The first three of these are "traits". As you can see in the lambda examples, for a callback you often need a type parameter that implements FnOnce(...) -> T, or you receive the callback as a trait object (&dyn T, Box<dyn T>, or Rc<dyn T>).

The fn(...) type is ultra rare for us: it is a function pointer, which I myself never used.

Trait objects

A trait object is a value that performs "type erasure", allowing to use dynamic dispatches in any value that implements a series of traits.

The type of trait objects is indicated by the dyn keyword, allowed at certain contexts (such as &dyn T and Rc<dyn T>).

type T1<'a> = &'a dyn Trait1 + Trait2 + TraitN;

Note that not all traits are "object safe"; that is, not all Rust traits may be used as trait objects.

If for example you need an Any that may be stringified, you will want to define a trait like:

pub trait Trait1: Any + ToString + 'static {
}

The 'static bound makes it so implementors of the Trait1 are not allowed to contain non 'static references, such as non 'static borrows, making the Trait1 trait "object safe", for use such as in Rc<dyn Trait1>.

Dynamic typing

The Any trait emulates dynamic typing in a certain way. All types, except borrows in general, implement the Any type.

use std::any::Any;

fn main() {
    let o: Box<dyn Any> = Box::new(10.0);
    if let Ok(_) = o.downcast::<f64>() {
        println!("o is f64");
    }
}

AS3 packages versus Rust modules

Totally different:

  • Rust modules do not have the same "shadowing rules" as AS3 packages.
  • Items are looked up in different ways. For example:
    • A macro invokation only looks for macros
    • A x::y statement only looks for a module x and either a submodule y or an enumeration's variant y.

One common thing too is that the Options variants (Some and None) as well as Result variants (Ok and Err) are imported lexically in the Rust's standard library (std::prelude).

// Equivalent
Option::Some(v)
Some(v)
Option::None
None

Result::Ok(v)
Ok(v)
Result::Err(error)
Err(error)

Macros

Macros appear in different flavours:

  • Function call macros (your_macro!(...), your_macro! [...], your_macro! {...})
  • Attribute macros
  • Derive macros

Macros expand to different Rust code depending in the token sequence they receive.

Optional chaining

Optional chaining may be emulated in Rust in different ways:

  • Using the nightly try { ...} block and the error propagation operator inside (option?.m()?.x).
  • Using methods such as .map(|v| v * 2) or .and_then(|v| v.m()) in an Option.

These techniques operate in Option and Result values.