-
Notifications
You must be signed in to change notification settings - Fork 4
Rust for ActionScript developers
This article is an introduction of the Rust language for ActionScript familiar people.
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.
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()
.
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>
.
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
)
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
.
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()
}
}
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() {
_ => {},
}
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
}
}
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
.
Traits may be automatically implemented according to a structure or enumeration through the #[derive(...)]
attribute.
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
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
}
let mut x: Option<f64> = None;
x = Some(10.0);
if let Some(x) = x {
println!("{x}");
}
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
}
}
Everything is internal to the enclosing module by default, unless the pub
qualifier is used.
There is pub
, and pub(module_path)
.
crate // The main module of your Rust crate
self // The enclosing module
super // The parent of the enclosing module
use foo::bar::*;
pub use foo::qux::*;
use super::Something;
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 static
s are single memory locations.
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);
}
There are different function types in Rust:
-
Fn(...)
orFn(...) -> T
-
FnMut(...)
orFnMut(...) -> T
-
FnOnce(...)
orFnOnce(...) -> T
-
fn(...)
orfn(...) -> 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.
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>
.
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");
}
}
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 modulex
and either a submoduley
or an enumeration's varianty
.
One common thing too is that the Option
s 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 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 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 anOption
.
These techniques operate in Option
and Result
values.