1 unstable release
0.1.0 | Nov 24, 2022 |
---|
#55 in #defines
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.