@ Bindings
Tutorial Video
Text description (accessibility)
This video demonstrates the "@ Bindings" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Sometimes you want both to test a value against a pattern and to bind it to a name for use in the arm body. Key difference from OCaml: 1. **Syntax**: Rust uses `@` (before the pattern); OCaml uses `as` (after the pattern).
Tutorial
The Problem
Sometimes you want both to test a value against a pattern and to bind it to a name for use in the arm body. Without @ bindings, you would either have to test the pattern and reconstruct the value, or bind the name and recheck the condition inside the arm. The @ operator (at-binding) solves this: a @ 0..=12 both tests that the value is in range AND binds it to a, so you can use a in the expression without repeating the match.
🎯 Learning Outcomes
name @ pattern binds the value to name while also testing patterna @ 0..=12 combines range testing with bindinge @ Event::Click(..) binds the whole enum value while matching a variant@ can be used in nested patterns and with guards@ is common: error reporting (bind the bad value), range-based classificationCode Example
#![allow(clippy::all)]
//! @ Bindings
//!
//! Binding a name while also matching a pattern.
/// Bind while matching range.
pub fn describe_age(age: u32) -> String {
match age {
a @ 0..=12 => format!("child ({})", a),
a @ 13..=19 => format!("teen ({})", a),
a @ 20..=64 => format!("adult ({})", a),
a => format!("senior ({})", a),
}
}
/// Bind while matching enum.
#[derive(Debug)]
pub enum Event {
Click(i32, i32),
KeyPress(char),
}
pub fn process_event(e: &Event) -> String {
match e {
e @ Event::Click(_, _) => format!("click: {:?}", e),
e @ Event::KeyPress(_) => format!("key: {:?}", e),
}
}
/// Bind while destructuring.
pub fn first_two(v: &[i32]) -> Option<(i32, i32)> {
match v {
[first @ .., second, _] if v.len() >= 2 => Some((*first.first()?, *second)),
_ => None,
}
}
/// Bind with guards.
pub fn check_value(n: i32) -> &'static str {
match n {
x @ 0..=10 if x % 2 == 0 => "small even",
x @ 0..=10 => "small odd",
x if x > 100 => "large",
_ => "medium",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe_age() {
assert!(describe_age(5).contains("child"));
assert!(describe_age(15).contains("teen"));
assert!(describe_age(30).contains("adult"));
}
#[test]
fn test_process_event() {
let e = Event::Click(10, 20);
assert!(process_event(&e).contains("click"));
}
#[test]
fn test_check_value() {
assert_eq!(check_value(4), "small even");
assert_eq!(check_value(5), "small odd");
assert_eq!(check_value(200), "large");
}
}Key Differences
@ (before the pattern); OCaml uses as (after the pattern).a @ 0..=12 is common with range patterns; OCaml uses when guards for numeric ranges.@/as bindings at multiple levels of a pattern.OCaml Approach
OCaml uses as for the equivalent:
let describe_age age = match age with
| a when a <= 12 -> Printf.sprintf "child (%d)" a
| a when a <= 19 -> Printf.sprintf "teen (%d)" a
| a -> Printf.sprintf "adult (%d)" a
(* Or with constructor binding: *)
let f = function
| (Some _ as x) -> Printf.printf "got %s\n" (match x with Some s -> s | None -> "")
| None -> ()
OCaml's as in patterns corresponds to Rust's @.
Full Source
#![allow(clippy::all)]
//! @ Bindings
//!
//! Binding a name while also matching a pattern.
/// Bind while matching range.
pub fn describe_age(age: u32) -> String {
match age {
a @ 0..=12 => format!("child ({})", a),
a @ 13..=19 => format!("teen ({})", a),
a @ 20..=64 => format!("adult ({})", a),
a => format!("senior ({})", a),
}
}
/// Bind while matching enum.
#[derive(Debug)]
pub enum Event {
Click(i32, i32),
KeyPress(char),
}
pub fn process_event(e: &Event) -> String {
match e {
e @ Event::Click(_, _) => format!("click: {:?}", e),
e @ Event::KeyPress(_) => format!("key: {:?}", e),
}
}
/// Bind while destructuring.
pub fn first_two(v: &[i32]) -> Option<(i32, i32)> {
match v {
[first @ .., second, _] if v.len() >= 2 => Some((*first.first()?, *second)),
_ => None,
}
}
/// Bind with guards.
pub fn check_value(n: i32) -> &'static str {
match n {
x @ 0..=10 if x % 2 == 0 => "small even",
x @ 0..=10 => "small odd",
x if x > 100 => "large",
_ => "medium",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe_age() {
assert!(describe_age(5).contains("child"));
assert!(describe_age(15).contains("teen"));
assert!(describe_age(30).contains("adult"));
}
#[test]
fn test_process_event() {
let e = Event::Click(10, 20);
assert!(process_event(&e).contains("click"));
}
#[test]
fn test_check_value() {
assert_eq!(check_value(4), "small even");
assert_eq!(check_value(5), "small odd");
assert_eq!(check_value(200), "large");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe_age() {
assert!(describe_age(5).contains("child"));
assert!(describe_age(15).contains("teen"));
assert!(describe_age(30).contains("adult"));
}
#[test]
fn test_process_event() {
let e = Event::Click(10, 20);
assert!(process_event(&e).contains("click"));
}
#[test]
fn test_check_value() {
assert_eq!(check_value(4), "small even");
assert_eq!(check_value(5), "small odd");
assert_eq!(check_value(200), "large");
}
}
Deep Comparison
OCaml vs Rust: pattern at bindings
See example.rs and example.ml for implementations.
Exercises
fn check_age(age: u32) -> Result<(), String> using a @ 130..=u32::MAX => Err(format!("age {} is unrealistic", a)) to include the invalid value in the error.Option<Option<i32>> using outer @ Some(inner @ Some(v)) and format all three bindings in the result string.match point { p @ Point { x, y } if x > 0 && y > 0 => format!("{:?} is in Q1", p), ... } — bind the whole struct while also checking individual fields.