#match #value #defines

fn_match

Defines a new type of match based on passing each value into a function

1 unstable release

0.1.0 Nov 24, 2022

#60 in #defines

WTFPL license

9KB

Defines another variety of match statement.

Rather than comparing a single value against a number of patterns, the fn_match passes each value to a given function, and evaluates the expression based on which case causes a favorable return.

The same way a traditional match can act like a sequence of if x == y for various y, fn_match can act like (and expands to) a sequence of if let Some(x) = foo(bar) for various bar.

Examples

Quick Start

let x = fn_match! {
    with fn: foo => y;
    "bar" => baz(y),
    "baz" => bar(y)
};

expands into

let x = if let Some(y) = foo("bar") {
    Some(baz(y))
} else if let Some(y) = foo("baz") {
    Some(bar(y))
} else {
    None
};

Detailed Example

One use case (and the motivating one) is for when using a Regex with mutually exclusive capture groups.

let regex = Regex::new(r#"^(?:(?P<int>\d+)|(?P<str>".+")|(?P<flt>\d+\.\d+))$"#).unwrap();
#
#
#

This regex can capture an int literal (235), a String literal ("hello"), or an (unsigned) float literal (87.43), and could be used to generate instances of the following enum:

enum Token {
    Int(u64),
    Str(String),
    Float(f64)
}

In standard Rust, the code to generate a Token from a given &str might look like:

let input = r#""hello world""#;
let caps = regex.captures(input).expect("failed to match");

let token = if let Some(tok) = caps.name("int").map(|s| s.as_str()) {
    Token::Int(tok.parse().unwrap())
} else if let Some(tok) = caps.name("str").map(|s| s.as_str()) {
    Token::Str(tok.trim_matches('\"').to_string())
} else if let Some(tok) = caps.name("flt").map(|s| s.as_str()) {
    Token::Float(tok.parse().unwrap())
} else { panic!("failed to match") };

assert_eq!(token, Token::Str(String::from("hello world")));

Applying fn_match, as well as an extra closure, allows repetition to be reduced, and the code to be rewritten as follows:

let input = r#"42"#;
let caps = regex.captures(input).expect("failed to match");

let f = |g| caps.name(g).map(|s| s.as_str());
let token = fn_match! {
    with fn: f => tok;
    "int" => Token::Int(tok.parse().unwrap()),
    "str" => Token::Str(tok.trim_matches('\"').to_string()),
    "flt" => Token::Float(tok.parse().unwrap())
}.expect("failed to match");

assert_eq!(token, Token::Int(42));

fn_match Syntax

There are three main components of an fn_match block:

  • A function to call
  • An identifier
  • A sequence of lhs-rhs pairs
#
    with fn: f => tok;
#

This line provides a function and an identifier. The function (placed in between the fn: and the =>) needs to take in a single value of the type that the later lines provide, and return an Option.

Note: the function expression is placed in each if let attempt after expansion. If the function is a closure, it is recommended to store it in a variable before the fn_match, to prevent multiple identical closures from being written in the final code.

The identifier (between the => and ;) is used to refer to the contained Some value, if the function returns that. (It's used as the binding pattern for if let Some(x)).

Following this line are a sequence of match pairs.

#
    "int" => Token::Int(tok.parse().unwrap()),
#

The left side of the => is the testing value, to be passed into the previously provided function.

The right side is an expression to be evaluated/returned (wrapped in its own Some) in the event that the function returned Some when called with the left value. The rhs can also refer to the identifier as the relevant Some value.

The entire fn_match block expression evaluates to an Option. Either a Some containing the rhs of the lhs that matched, or None if no lhs matched.

No runtime deps