Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add an extract macro which extracts values matched in a pattern #495

Open
vxpm opened this issue Nov 24, 2024 · 10 comments
Open

add an extract macro which extracts values matched in a pattern #495

vxpm opened this issue Nov 24, 2024 · 10 comments
Labels
api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api

Comments

@vxpm
Copy link

vxpm commented Nov 24, 2024

Proposal

Problem statement

it's not uncommon to pattern match on a value only to obtain the value of a binding. currently, the only way to do this is to either match on the value, use if let or use let else. all these solutions involve multiple (~3) lines of code for a relatively simple operation, making code less concise.

Motivating examples or use cases

enum Value {
    Int(u64),
    Float(f64),
    Char(char),
}

fn main() {
    use Value::*;
    let arr = [Int(0), Char('a'), Float(0.0), Int(1), Char('b')];

    // notice how this simply filter_map takes 4 whole lines!
    let chars = arr.into_iter().filter_map(|x| match x {
        Char(c) => Some(c),
        _ => None,
    });

    for c in chars {
        println!("got a character: {c}");
    }
}

Solution sketch

i propose an extract macro which extracts values matched in a pattern in a way similar to the matches macro:

macro_rules! extract {
    ($expression:expr, $pattern:pat $(if $guard:expr)? => $extracted:expr $(,)?) => {
        match $expression {
            $pattern $(if $guard)? => Some($extracted),
            _ => None
        }
    };
}

the motivating example would then become:

enum Value {
    Int(u64),
    Float(f64),
    Char(char),
}

fn main() {
    use Value::*;
    let arr = [Int(0), Char('a'), Float(0.0), Int(1), Char('b')];

    // a single line!
    let chars = arr.into_iter().filter_map(|x| extract!(x, Char(c) => c));
    for c in chars {
        println!("got a character: {c}");
    }
}

Alternatives

as an alternative, just keep using match, if let and let else, since they are able to get the job done. however, it's important to notice that they can also do what matches does, but that does not make matches undesirable or any less useful.

Links and related work

searching crates.io for extract macro shows a crate implementing a macro with the exact same proposed syntax, but slightly different implementation.

@vxpm vxpm added api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api labels Nov 24, 2024
@vxpm vxpm changed the title add anextract macro which extracts values matched in a pattern add an extract macro which extracts values matched in a pattern Nov 24, 2024
@kennytm
Copy link
Member

kennytm commented Nov 24, 2024

For this particular example, it seems more natural that the library author is going to provide a TryFrom impl:

impl TryFrom<Value> for char {
    type Error = ...;
    fn try_from(value: Value) -> Result<Self, Self::Error> {
        match value {
            Value::Char(c) => Ok(c),
            _ => Err(...),
        }
    }
}

then you can use the standard:

let chars = arr.into_iter().filter_map(|x| x.try_into().ok());

@vxpm
Copy link
Author

vxpm commented Nov 24, 2024

sure, but that depends on the library author - the user has no control over whether the type implements TryFrom whatsoever. it's not hard to find cases like this within rust tools:

let lifetimes = params.args.iter().filter_map(|arg| match arg {
    GenericArg::Lifetime(lt) => Some(lt),
    _ => None,
});

(https://github.com/rust-lang/rust/blob/f5d18576856ef45d1e47de79889ae7db9d1afa29/src/tools/clippy/clippy_lints/src/lifetimes.rs#L175-L178)

let tokens = node.preorder_with_tokens().filter_map(|event| match event {
    rowan::WalkEvent::Leave(NodeOrToken::Token(it)) => Some(it),
    _ => None,
});

(https://github.com/rust-lang/rust/blob/f5d18576856ef45d1e47de79889ae7db9d1afa29/src/tools/rust-analyzer/crates/syntax/src/ast/edit.rs#L83-L86)

plus, it's not always possible. consider the following enum:

enum Value {
    A(u64),
    B(char),
    C(u64),
}

you can't have a TryFrom<Value> for u64 that satisfies the "i only want u64 from A" or "i only want u64 from C" use cases.

edit: i got a little confused and edited the last example multiple times. i just woke up so please forgive my sillyness.

@jdahlstrom
Copy link

I've sometimes wanted this, it's a fairly natural extension of the matches! macro.

@scottmcm
Copy link
Member

I'm not a fan of that motivating example, because to me the thing I'd expect is that it has

impl Value {
    fn char(self) -> Option<char> {}
}

Just like how there's Result::err and such. And if it's not in the crate you're using, you could PR it. (And a derive macros can generate them too.)

@the8472
Copy link
Member

the8472 commented Nov 25, 2024

all these solutions involve multiple (~3) lines of code for a relatively simple operation, making code less concise.

You could configure rustfmt to let those be one-liners.

@joshtriplett
Copy link
Member

I think it's worth comparing to the example written with if let:

enum Value {
    Int(u64),
    Float(f64),
    Char(char),
}

fn main() {
    use Value::*;
    let arr = [Int(0), Char('a'), Float(0.0), Int(1), Char('b')];

    let chars = arr
        .into_iter()
        .filter_map(|x| if let Char(c) = x { Some(c) } else { None });

    for c in chars {
        println!("got a character: {c}");
    }
}

The chain wraps onto three lines, but the if let is all on one line. I think this looks clearer.

@joshtriplett
Copy link
Member

joshtriplett commented Nov 26, 2024

I feel like extract! as written just looks like a shorthand for a match or if-let, and I think it doesn't carry enough weight to justify the additional surface area.

One approach I do think would carry its weight (name and exact syntax extremely subject to bikeshed):

    let chars = arr
        .into_iter()
        .filter_map(opt_closure!(|Char(c)| c));

opt_closure! would take a closure that takes a fallible pattern as an argument, apply that pattern to the argument, and if it fails, return None.

@kennytm
Copy link
Member

kennytm commented Nov 27, 2024

macro_rules! opt_closure {
    (|$p:pat_param| $e:expr) => {
        |x| if let $p = x { Some($e) } else { None }
    };
}

How is the "weight" carried by opt_closure! heavier than extract!? The macro working as a higher-level function "transforming" a closure also makes it more abstract and less intuitive than extract!.

@jdahlstrom
Copy link

I feel like extract! as written just looks like a shorthand for a match or if-let, and I think it doesn't carry enough weight to justify the additional surface area.

But the same could be said of matches!() (or a possible future language feature like an is operator, which would presumably also allow destructuring).

// Note that matches! and extract! additionally support if guards
if let $pat = $arg { true } else { false }

if let $pat = $arg { Some($out) } else { None }

@cuviper
Copy link
Member

cuviper commented Nov 27, 2024

We had evidence of matches! as a common use case, especially through the popularity of the matches crate.

The extract_macro crate has few downloads and zero dependents on crates.io.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api
Projects
None yet
Development

No branches or pull requests

7 participants