Misc: Syntax, Crates, std

CIS 198 Lecture 7


const

  1. const PI: f32 = 3.1419;
  • Defines constants that live for the duration of the program.
  • Must annotate the type!
  • Constants “live” for the duration of the program.
    • Think of them as being inlined every time they’re used.
    • No guarantee that multiple references to the same constant are the same.

static

  1. static PI: f32 = 3.1419;
  • As above: must annotate type.
  • Typical global variable with fixed memory address.
  • All references to static variables has the 'static lifetime, because statics live as long as the program.
  • unsafe to mutate.
  1. let life_of_pi: &'static f32 = Π
  • String literals are references (with lifetime 'static) to static strs.

static

  1. static mut counter: i32 = 0;
  • You can create mutable static variables, but you can only mutate them inside unsafe blocks.
    • Rust forces you to declare when you’re doing things that are… morally questionable potentially going to crash your program.

Modules & Crates


Modules

  • We’ve seen these in the homework, but not talked about them.
  • Everything in Rust is module-scoped: if it’s not pub, it’s only accessible from within the same module.
  • Modules can be defined within one file:
  1. mod english {
  2. pub mod greetings {
  3. }
  4. pub mod farewells {
  5. }
  6. }
  7. mod japanese {
  8. pub mod greetings {
  9. }
  10. pub mod farewells {
  11. }
  12. }

Reference: TRPL 4.25


Modules

  1. mod english {
  2. pub mod greetings { /* ... */ }
  3. }
  • Modules can be defined as files instead:
  • lib.rs:
    1. mod english;
  • english.rs:
    1. pub mod greetings { /* ... */ }

Modules

  1. mod english {
  2. pub mod greetings { /* ... */ }
  3. }
  • Modules can also be defined as directories:
  • lib.rs:
    1. mod english;
  • english/
    • mod.rs:
      1. pub mod greetings;
    • greetings.rs:
      1. /* ... */

Namespacing

  • When accessing a member of a module, by default, namespaces are relative to the current module:
  1. mod one {
  2. mod two { pub fn foo() {} }
  3. fn bar() {
  4. two::foo()
  5. }
  6. }
  • But it can be made absolute with a leading :: operator:
  1. mod one {
  2. mod two { pub fn foo() {} }
  3. fn bar() {
  4. ::one::two::foo()
  5. }
  6. }

useing Modules

  • use has the opposite rules.
  • use directives are absolute by default:
  1. use english::greetings;
  • But can be relative to the current module:
  1. // english/mod.rs
  2. use self::greetings;
  3. use super::japanese;
  • pub use can be used to re-export other items:
  1. // default_language.rs
  2. #[cfg(english)]
  3. pub use english::*;
  4. #[cfg(japanese)]
  5. pub use japanese::*;

Using External Crates

  • For external crates, use extern crate instead of mod.
  1. extern crate rand;
  2. use rand::Rng;

Making Your Own Crate

  • We’ve been writing lib crates - but how do we export from them?
  • Anything marked pub in the root module (lib.rs) is exported:
  1. pub mod english;
  • Easy!

Using Your Own Crate

  • Now, you can use your own crate from Cargo:
  1. [dependencies]
  2. myfoo = { git = "https://github.com/me/foo-rs" }
  3. mybar = { path = "../rust-bar" }
  • Or:
  1. [dependencies.myfoo]
  2. git = "https://github.com/me/foo-rs"
  • And use them:
  1. extern crate myfoo;
  2. use myfoo::english;

Cargo: you got your bins in my lib

  • We’ve seen both lib and bin (executable) crates in homework
    • Executable-only crates don’t export any importable crates.
    • But this isn’t really a distinction!
  • Cargo allows both :/src/lib.rs and :/src/main.rs.
    • Cargo will also build :/src/bin/*.rs as executables.
  • Examples go in :/examples/*.rs.
    • Built by cargo test (to ensure examples always build).
    • Can be called with cargo run --example foo.
  • Integration (non-unit) tests go in :/tests/*.rs.
  • Benchmarks go in :/benches/*.rs.

Cargo: Features

  • Features of a crate can be toggled at build time:
    • cargo build --features using-html9
  1. [package]
  2. name = "myfacebumblr"
  3. [features]
  4. # Enable default dependencies: require web-vortal *feature*
  5. default = ["web-vortal"]
  6. # Extra feature; now we can use #[cfg(feature = "web-vortal")]
  7. web-vortal = []
  8. # Also require h9rbs-js *crate* with its commodore64 feature.
  9. using-html9 = ["h9rbs-js/commodore64"]
  10. [dependencies]
  11. # Optional dependency can be enabled by either:
  12. # (a) feature dependencies or (b) extern crate h9rbs_js.
  13. h9rbs-js = { optional = "true" }

Cargo: Build Scripts

  • Sometimes, you need more than what Cargo can provide.
  • For this, we have build scripts!
    • Of course, they’re written in Rust.
  1. [package]
  2. build = "build.rs"
  • Now, cargo build will compile and run :/build.rs first.

Cargo: The Rabbit Hole

  • Cargo has a lot of features. If you’re interested, check them out in the Cargo manifest format documentation.

Attributes

  • Ways to pass information to the compiler.
  • #[test] is an attribute that annotates a function as a test.
  • #[test] annotates the next block; #![test] annotates the surrounding block.
  1. #[test]
  2. fn midterm1() {
  3. // ...
  4. }
  5. fn midterm2() {
  6. #![test]
  7. // ...
  8. }

Attributes

  • Use attributes to…
    • #![no_std] disable the standard library.
    • #[derive(Debug)] auto-derive traits.
    • #[inline(always)] give compiler behavior hints.
    • #[allow(missing_docs)] disable compiler warnings for certain lints.
    • #![crate_type = "lib"] provide crate metadata.
    • #![feature(box_syntax)] enable unstable syntax.
    • #[cfg(target_os = "linux")] define conditional compilation.
    • And many more!

Rust Code Style


Rust Code Style

  • A style guide is being drafted as part of the Rust docs.
  • The main reason for many of the rules is to prevent pointless arguments about things like spaces and braces.
    • If you contribute to an open-source Rust project, it will probably be expected that you follow these rules.
  • The rustfmt project is an automatic code formatter.

Spaces

  • Lines must not exceed 99 characters.
  • Use 4 spaces for indentation, not tabs.
  • No trailing whitespace at the end of lines or files.
  • Use spaces around binary operators: x + y.
  • Put spaces after, but not before, commas and colons: x: i32.
  • When line-wrapping function parameters, they should align.
    1. fn frobnicate(a: Bar, b: Bar,
    2. c: Bar, d: Bar)
    3. -> Bar {
    4. }

Braces

  • Opening braces always go on the same line.
  • Match arms get braces, except for single-line expressions.
  • return statements get semicolons.
  • Trailing commas (in structs, matches, etc.) should be included if the closing delimiter is on a separate line.

Capitalization & Naming

  • You may have seen built-in lints on how to spell identifiers.
    • CamelCase: types, traits.
    • lowerCamelCase: not used.
    • snake_case: crates, modules, functions, methods, variables.
    • SCREAMING_SNAKE_CASE: static variables and constants.
    • T (single capital letter): type parameters.
    • 'a (tick + short lowercase name): lifetime parameters.
  • Constructors and conversions should be worded:
    • new, new_with_stuff: constructors.
    • from_foo: conversion constructors.
    • as_foo: free non-consuming conversion.
    • to_foo: expensive non-consuming conversion.
    • into_foo: consuming conversion.

Advanced format!ing

  • The ? means debug-print. But what goes before the : part?
    • A positional parameter! An index into the argument list.
  1. println!("{2} {} {} {0} {} {}", 0, 1, 2, 3) // ==> "2 0 1 0 2 3"
  • Among the specifiers with no positional parameter, they implicitly count up: {0} {1} {2} ....

  • There are also named parameters:

  1. format!("{name} {}", 1, name = 2); // ==> "2 1"

format! Specifiers

  • We’ve been printing stuff out with println!("{:?}", bst);
  • There are more format specifiers than just {} and {:?}.
    • These all call traits in std::fmt:
Spec. Trait Spec. Trait Spec. Trait
{} Display {:?} Debug {:o} Octal
{:x} LowerHex {:X} UpperHex {:p} Pointer
{:b} Binary {:e} LowerExp {:E} UpperExp

format! Specifiers

  • There are tons of options for each of these format specifiers.
  • Examples:
    • {:04} -> 0010: padding
    • '{:^4}' -> ' 10 ': alignment (centering)
    • # indicates an “alternate” print format:
    • {:#X} -> 0xA: including 0x
    • {:#?}: Pretty-prints objects:
  1. A {
  2. x: 5,
  3. b: B {
  4. y: 4
  5. }
  6. }

Operators

  • Operators are evaluated left-to-right, in the following order:
    • Unary operators: ! - * & &mut
    • as casting
    • * / % multiplicative arithmetic
    • + - additive arithmetic
    • << >> shift arithmetic
    • & bitwise and
    • ^ bitwise xor
    • | bitwise or
    • == != < > <= >= logical comparison
    • && logical and
    • || logical or
    • = .. assignment and ranges
  • Also: call(), index[]

Operator Overloading

  • Okay, same old, same old. We can customize these!
  • Rust defines these - surprise! - using traits, in std::ops.
    • Neg, Not, Deref, DerefMut
    • Mul, Div, Mod
    • Add, Sub
    • Shl, Shr
    • BitAnd
    • BitXor
    • BitOr
    • Eq, PartialEq, Ord, PartialOrd
    • And
    • Or
  • Also: Fn, FnMut, FnOnce, Index, IndexMut, Drop

From One Type Into Another

  • Casting (as) cannot be overloaded - instead, we use From and Into.
    • trait From<T> { fn from(T) -> Self; }, called like Y::from(x).
    • trait Into<T> { fn into(self) -> T; }, called like x.into().
  • If you implement From, Into will be automatically implemented.
    • So you should prefer implementing From.
  1. struct A(Vec<i32>);
  2. impl From<Vec<i32>> for A {
  3. fn from(v: Vec<i32>) -> Self {
  4. A(v)
  5. }
  6. }

From One Type Into Another

  • But sometimes, for various reasons, implementing From isn’t possible - only Into.
  1. struct A(Vec<i32>);
  2. impl From<A> for Vec<i32> { // error: private type A in
  3. fn from(a: A) -> Self { // exported type signature.
  4. let A(v) = a; v // (This impl is exported because
  5. } // both the trait (From) and the type
  6. } // (Vec) are visible from outside.)
  7. impl Into<Vec<i32>> for A {
  8. fn into(self) -> Vec<i32> {
  9. let A(v) = self; v
  10. }
  11. }

Making References

  • Borrow/BorrowMut: “a trait for borrowing data.”¹
  1. trait Borrow<Borrowed> { fn borrow(&self) -> &Borrowed; }
  • AsRef/AsMut: “a cheap, reference-to-reference conversion.”²
  1. trait AsRef<T> { fn as_ref(&self) -> &T; }
  • So… they’re exactly the same?

¹ Trait std::borrow::Borrow

² Trait std::convert::AsRef


Making References

  • No! While the have the same definition, Borrow carries additional connotations:
    • “If you are implementing Borrow and both Self and Borrowed implement Hash, Eq, and/or Ord, they must produce the same result.”¹ ²
  • Borrow has a blanket implementation:
    • impl<T> Borrow<T> for T: you can always convert T to &T.
  • AsRef actually has its own blanket implementation:
    • impl<'a, T, U> AsRef<U> for &'a T where T: AsRef<U>
    • For all T, if T implements AsRef, &T also implements AsRef.
  • All this means you usually want to implement AsRef.

¹ Trait std::borrow::Borrow

² aturon on Borrow vs AsMut