Match fn sugar


#1

This has probably been brought up before but I couldn’t find the magic github/google query to find a previous discussion.

A fairly common pattern is for a fn to immediately wrap a match expr, like so:

fn foo(x: Option<int>, y: Option<int>) -> Option<int> {
    match (x, y) {
        (Some(x), Some(y)) => Some(x + y),
        (Some(x), None) => Some(x),
        _ => None
    }
}

This is unsatisfying for a couple of reasons: the match args (and frequently the match arm params) are a stutter of the function args, and the inner expr adds height and an extra scope that contributes to rightward drift.

Would people be open to an alternate syntax for this pattern that eliminated these two issues? Something like:

match fn foo(Option<int>, Option<int>) -> Option<int> {
    (Some(x), Some(y)) => Some(x + y),
    (Some(x), None) => Some(x),
    _ => None
}

This compares pretty nicely with the equivalent Haskell:

foo :: Maybe Int -> Maybe Int -> Maybe Int
foo (Just x) (Just y) = Just (x+y)
foo (Just x) _ = Just x
foo _ _ = Nothing

#2

This is pretty nice in a lot of cases, especially when implementing common functional idioms. As a data point, basically every single function in https://github.com/reem/adamantium is just a single match block and rightward drift and associated extra code makes it much harder to look at then the equivalent Haskell.

However, this could be implemented in a 100% backwards compatible way, so is unlikely to be implemented pre-1.0.


#3

I agree that this is a common pattern that would benefit from a bit of syntactic sugar. If this gets implemented I think it would be interesting to have something like GHC’s LambdaCase extension in Rust closures too:

Haskell syntax example:

let getJust = \case
  Just x  -> x
  Nothing -> error "getJust: Nothing"

Rust:

let get_just = match {
  Some(x) => x
  None    => fail!("get_just: None")
};

#4

You can’t quite do it exactly with a Macro-By-Example (because you can’t gensym the generated function’s argument names, or at least I don’t know how), but this would be a pretty simple compiler plugin.

A rough sketch of how it might look but as an MBE (playpen link):

#![feature(macro_rules)]

macro_rules! matchfn {
    ($name:ident ($($arg:ident : $arg_ty:ty),+) -> $fn_ty:ty {
        $($pat:pat => $rhs:expr),+
    }) => {
        fn $name($($arg : $arg_ty),+) -> $fn_ty {
            match ($($arg),+) {
                $($pat => $rhs),+
            }
        }
    }
}

matchfn!(foo(_a: Option<int>, _b: Option<int>) -> Option<int> {
    (Some(x), Some(y)) => Some(x + y),
    (Some(x), None) => Some(x),
    _ => None
})

pub fn main() {
    println!("{}", foo(Some(12), Some(24)));
    println!("{}", foo(Some(36), None));
    println!("{}", foo(None, None));
}

#5

Actually, you can omit the argument names if you walk the list of types and build up the signature and the match tuple along the way: playpen link


#6

That’s pretty sweet. Only seems to work w/ exactly two args right now, though.


#7

Was a typo, here’s a fix: playpen. But I agree, a proper syntax extension would be better. Errors and warnings wouldn’t be polluted with macro expansion locations; you could have an easy optional return value; you can have type parameters (haven’t even tried that one yet).

But it’s nice how much is prototypable with just macro_rules.


#8

If this happens there probably could also be other variants:

loop fn foo() {...}
loop match fn foo(Option<int>, Option<int>) -> Option<int> {...}

Also, if it could handle methods too:

fn next(&mut self) {
    match self.state {
        _ => {};
    };
}

Then this could be reduced by 2 nesting levels. There could possibly be more variants (while;for;if/else) if there was a workable scheme.


#9

Another thought is demarcating a match block in ‘arg receiver position’ using doubled brackets (or whatever):

fn foo(x: Option<int>, y: Option<int>) -> Option<int> {{  
    (Some(x_), Some(y_)) => Some(x_+y_),  
    (Some(x_), None) => Some(x_),
    _ => None
}}

The advantage of this is that it extends naturally to closures:

|x, y| {{
    (Some(x_), Some(y_)) => Some(x_ + y_),
    (Some(x_), None) => Some(x_),
    _ => None
}}

And impls can more closely match trait signatures:

trait Foo {
    fn foo(&self, x: Option<int>) -> Option<int>;
}

impl Foo for Option<int> {
    fn foo(&self, x: Option<int>) -> Option<int> {{
        (&Some(x_), Some(y_)) => Some(x_ + y_),
        (&Some(x_), None) => Some(x_),
        _ => None        
    }}
}

#10

No a very useful use case.


#11

I can understand if people don’t want to add this, but it’s certainly a valid and useful use case. Its the core way you define functions in Haskell, and there’s a ton of existing Rust code that uses the more verbose form this sugars.


#12

I like the idea, but of course not the particular syntax. I would prefer something like:

fn foo(x: Option<int>, y: Option<int>) -> Option<int> match {  
    (Some(x_), Some(y_)) => Some(x_+y_),  
    (Some(x_), None) => Some(x_),
    _ => None
}

where conceptually the whole function is a match block.

Unfortunately if we go to extend this to lambdas, it’s slightly ambiguous:

|x, y| match {
    (Some(x_), Some(y_)) => Some(x_ + y_),
    (Some(x_), None) => Some(x_),
    _ => None
}

The problem is that {} is itself an expression. Is it the match scrutinee or the body?


#13

I thought of that syntax but don’t really like it because of the ambiguity you mention and because the ‘match’ runs up against the back of the return type.

Just to throw it out there, I think the true ideal syntax for this is

fn foo(x: Option<int>, y: Option<int>) -> Option<int> {  
    Some(x_), Some(y_) => Some(x_+y_),  
    Some(x_), None => Some(x_),
    _ => None
}

That is, no special syntax at all to set off the match and not requiring parens for the argument tuple. I believe this is unambiguous b/c ‘=>’ isn’t used for anything else, but I haven’t seriously proposed it b/c the parser lookahead and magic tupling seem like things that would get a lot of pushback.

The other alternative I was thinking of was something like

fn foo(x: Option<int>, y: Option<int>) -> Option<int> {=>  
    (Some(x_), Some(y_)) => Some(x_+y_),  
    (Some(x_), None) => Some(x_),
    _ => None
}

But that’s another magic sigil.

Is there something specific about the doubled brackets you don’t like? Is it just the lack of explicitly saying ‘match’?


#14

Hilarious brain fart aside: this is similar to how many years ago I consistently read (Brent) ‘Scowcroft’ as ‘Snowcroft’, to the point where I was tremendously confused and rather agitated by the fact that if I wrote the name into Google myself, it only returned a single result, while if I copy pasted the name from a web page, it returned lots of results. It took me a while to realize what was going on and that the world hadn’t stopped making sense, only my brain.

Similarly, in this case for years now I had been under the impression that the => operator used by Rust for match and by Haskell for type class contexts is the greater-than-or-equals operator, and specifically thinking that this would likely end up causing some problems for Haskell when the type system advanced to the point that they got to writing inequality comparisons at the type level.

…but it turns out that >= and => are distinct entities.

Is there something specific about the doubled brackets you don’t like?

It seems awfully arbitrary and not very aesthetically appealing (line noise, etc.).


#15

On Sat, Oct 18, 2014 at 12:44:50PM +0000, glaebhoerl wrote:


#16

Discourse ate your reply :\


#17

Working off @mdinger’s comment, more match sugar:

for v in foo.iter() {{
    (Some(x), Some(y)) => println!("Total: {}", x+y),  
    (Some(x), None) => println!("x: {}", x),
    _ => println!("no x")
}} 

loop rx.recv() {{
    (Some(x), Some(y)) => println!("Total: {}", x+y),
    (Some(x), None) => println!("x: {}", x),
    _ => println!("no x")
}}

Or more like @glaebhoerl’s alternative:

for match v in foo.iter() {
    (Some(x), Some(y)) => println!("Total: {}", x+y),  
    (Some(x), None) => println!("x: {}", x),
    _ => println!("no x")
} 

loop match rx.recv() {
    (Some(x), Some(y)) => println!("Total: {}", x+y),
    (Some(x), None) => println!("x: {}", x),
    _ => println!("no x")
}

I think ‘if let’ and ‘while let’ are already the analogous sugar for ‘if’ and ‘while’.


#18

For language features like this, my request is that if they incur overhead in the compiler (I don’t know how much there actually would be), there should be a way to disable them. This and other features (like if let) might be things I just decide not to use, e.g. for simplicity and consistency reasons (one of the great things about C is how dead simple the language is, not just for computer to parse, but for humans, too!). If I can squeeze out any more compiler performance by not using more sugary language features, I’d absolutely like that option.


#19

FWIW, In OCaml, the sugar is the following:

let f = function 
 | ....

is (exactly) the same as

let f x = match x with
 | ...

#20

Simplicity is in the eyes of the beholder, I guess. C is simple but a lot of C programs end up being verbose and complex b/c the language isn’t expressive enough. Compact forms for common idioms lead to simpler programs (though admittedly it can be taken too far).