fn_match 0.1.0

Defines a new type of match based on passing each value into a function.
Documentation
  • Coverage
  • 100%
    2 out of 2 items documented1 out of 1 items with examples
  • Size
  • Source code size: 10.16 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 1.14 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Links
  • zacryol/fn_match
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • zacryol

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

# use fn_match::fn_match;
# let foo = |i: &str| Option::<()>::None; // placeholder
# let bar = |_| ();
# let baz = |_| ();
let x = fn_match! {
    with fn: foo => y;
    "bar" => baz(y),
    "baz" => bar(y)
};
# assert!(x.is_none());

expands into

# use fn_match::fn_match;
# let foo = |_: &str| Some(()); // placeholder
# let bar = |_| 5;
# let baz = |_| 6;
let x = if let Some(y) = foo("bar") {
    Some(baz(y))
} else if let Some(y) = foo("baz") {
    Some(bar(y))
} else {
    None
};
# assert_eq!(x, Some(6));

Detailed Example

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

# use regex::Regex;
let regex = Regex::new(r#"^(?:(?P<int>\d+)|(?P<str>".+")|(?P<flt>\d+\.\d+))$"#).unwrap();
#
# assert!(regex.is_match("23"));
# assert!(regex.is_match("\"hello\""));
# assert!(regex.is_match("23.89"));
# let caps = regex.captures("23").unwrap();
# assert_eq!(caps.name("int").unwrap().as_str(), "23");
# assert!(caps.name("str").is_none());
# assert!(caps.name("flt").is_none());
#
# let caps = regex.captures("\"hello world\"").unwrap();
# assert_eq!(caps.name("str").unwrap().as_str(), "\"hello world\"");
# assert!(caps.name("int").is_none());
# assert!(caps.name("flt").is_none());
#
# let caps = regex.captures("42.1").unwrap();
# assert_eq!(caps.name("flt").unwrap().as_str(), "42.1");
# assert!(caps.name("int").is_none());
# assert!(caps.name("str").is_none());

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:

# use regex::Regex;
# let regex = Regex::new(r#"^(?:(?P<int>\d+)|(?P<str>".+")|(?P<flt>\d+\.\d+))$"#).unwrap();
# #[derive(Debug, PartialEq)]
# enum Token {
#     Int(u64),
#     Str(String),
#     Float(f64)
# }
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:

# use regex::Regex;
# use fn_match::fn_match;
# let regex = Regex::new(r#"^(?:(?P<int>\d+)|(?P<str>".+")|(?P<flt>\d+\.\d+))$"#).unwrap();
# #[derive(Debug, PartialEq)]
# enum Token {
#     Int(u64),
#     Str(String),
#     Float(f64)
# }
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
# use regex::Regex;
# use fn_match::fn_match;
# let regex = Regex::new(r#"^(?:(?P<int>\d+)|(?P<str>".+")|(?P<flt>\d+\.\d+))$"#).unwrap();
# #[derive(Debug, PartialEq)]
# enum Token {
#     Int(u64),
#     Str(String),
#     Float(f64)
# }
# let input = r#"99.98"#;
# 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::Float(99.98));

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.

# use regex::Regex;
# use fn_match::fn_match;
# let regex = Regex::new(r#"^(?:(?P<int>\d+)|(?P<str>".+")|(?P<flt>\d+\.\d+))$"#).unwrap();
# #[derive(Debug, PartialEq)]
# enum Token {
#     Int(u64),
#     Str(String),
#     Float(f64)
# }
# let input = r#"99.98"#;
# 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::Float(99.98));

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.