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. }

8. mod japanese {
9. pub mod greetings {
10. }
11. pub mod farewells {
12. }
13. }

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

3. #[cfg(english)]
4. pub use english::*;

6. #[cfg(japanese)]
7. pub use japanese::*;

Using External Crates

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

3. 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;

3. 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"

4. [features]
5. # Enable default dependencies: require web-vortal *feature*
6. default = ["web-vortal"]

8. # Extra feature; now we can use #[cfg(feature = "web-vortal")]
9. web-vortal = []

11. # Also require h9rbs-js *crate* with its commodore64 feature.
12. using-html9 = ["h9rbs-js/commodore64"]

14. [dependencies]
15. # Optional dependency can be enabled by either:
16. # (a) feature dependencies or (b) extern crate h9rbs_js.
17. 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. }

6. fn midterm2() {
7. #![test]
8. // ...
9. }

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>);

3. impl From<A> for Vec<i32> { // error: private type A in
4. fn from(a: A) -> Self { // exported type signature.
5. let A(v) = a; v     // (This impl is exported because
6. }                       // both the trait (From) and the type
7. }                           // (Vec) are visible from outside.)

9. impl Into<Vec<i32>> for A {
10. fn into(self) -> Vec<i32> {
11. let A(v) = self; v
12. }
13. }

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