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

Function clauses RFC #1564

Closed
wants to merge 1 commit into from
Closed

Conversation

Havvy
Copy link
Contributor

@Havvy Havvy commented Apr 1, 2016

I wasn't going to submit this, thinking personally that the drawbacks outweigh the benefits, but I've had others actually say they like it, so figured I'd listen to the IRC community and actually submit it.

@alilleybrinker
Copy link

This seems like a lot of additional language complexity for very little gain.

@ftxqxd
Copy link
Contributor

ftxqxd commented Apr 1, 2016

Another big drawback is that when looking at the source code for a function, it could be easy to think that the primary function definition is the only one if you don’t realise that one of its arguments has a refutable pattern (e.g., if you skim over the function definition, or it uses an enum pattern that you assumed was a tuple struct pattern). This could make it hard to understand code you didn’t write (or indeed code that you did write but forgot the details of), potentially allowing bugs to slip by.

Requiring an extra keyword for multi-definition functions (e.g., fn match) would help this (but not entirely fix it).

@eternaleye
Copy link

An alternate option, that may reduce the syntactic overhead of stating the types every time, avoid being as bad as requiring an enclosing match, and also clarify the situation regarding attributes is to possibly take an idea from the Perl 6 community.

Specifically, they support defining a 'proto' function which must have a {*} body, and then some number of 'multi' functions which provide bodies.

If the proto function contains all of the type signatures, and the multi function bodies simply carry refutable matches, this could work well. Function attributes would sit on the proto function, etc.

Interestingly, that same syntax could easily be extended in the future to handle function specialization based on target attributes, as was recently added in GCC.

However, I agree that it's not a good trade-off to make it this time.

@Havvy
Copy link
Contributor Author

Havvy commented Apr 1, 2016

@AndrewBrinker I agree, which is the main reason I wasn't going to submit it.

@P1start From experience with Elixir, I can say with pretty good assurance that I've never not noticed another clause for a function.

@eternaleye Yeah, header signatures did pop into my head for a moment, but they're backwards compatible with this.

@eternaleye
Copy link

Oooh, here's an odd thought.

fn foo(Option<i32>, Result<(),()>) -> i32 {
    fn self(Some( x ), Ok(())) => {
        x
    }
    ...
}

In favor:

  • Separates types from identifiers
  • Doesn't collide with anything due to self being a keyword
  • Retains locality of definition
  • Is forwards compatible with other kinds of multidispatch
  • Allows attributes to sit on the enclosing function
  • => brings to mind match, allows eliding return type
  • Errors still point to the body of the function
  • Is really just pure sugar over the below syntax, minus two repetitions of the identifiers and a level of indentation
fn foo(args, here) {
    match (args, here) {
        (args, here) => {
            body
        }
    }
}

Against:

  • Magical passing of parameters in same order to inner functions
  • Might be non-obvious that fn self is exclusive of any actual code in the outer function
  • Possibly surprising that fn self is allowed (and doesn't shadow self) if used with a method

Another option:

fn foo(Option<i32>, Result<(),()>) -> i32 {
    fn (Some( x ), Ok(())) => {
        x
    }
    ...
}

This avoids the "unshadowed self surprise" issue, while retaining the advantages - nameless fn declarations are not permitted, so are backwards compatible to add.

@aturon aturon added the T-lang Relevant to the language team, which will review and decide on the RFC. label Apr 3, 2016
@mdinger
Copy link
Contributor

mdinger commented Apr 3, 2016

This is similar to the suggestions in this thread where this:

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
    }
}

could be shortened to:

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

There are other suggestions where they chain different keywords together essentially extending the idea and allowing somewhat generic keyword chaining:

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

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

@glaebhoerl Had some opinions on how the syntax should be ordered.

I kinda think this looks nicer than the proposed solution for what it's worth.

@brendanzab
Copy link
Member

I always thought having something similar to @mdinger's match syntax above for fn and for would be nice, but I think it would have to be a separate RFC. Would be nice for combating rightward drift, and encouraging smaller, pure functions.

Not really a fan of the Erlang/Elixir style though. It's easier for them to get away with because they don't have types to deal with, or having to cover each pattern.

@Havvy
Copy link
Contributor Author

Havvy commented Apr 4, 2016

On IRC, Jose Valim (founder of Elixir) was discussing alternate ways of doing function clauses in Elixir because they're verbose too. So given that, the Elixir syntax version is definitely dead in the water, so an alternate syntax is definitely preferable.

@Centril
Copy link
Contributor

Centril commented Apr 5, 2016

At least for me, reading functions defined in this way is readable and less verbose than using a match inside the function. Here we can take inspiration from Haskell (and Erlang).

before :: Maybe Foo -> ()
before optional_foo =
    case optional_foo of
    Just foo -> consume foo
    Nothing -> ()

after :: Maybe Foo -> ()
after (Just foo) = consume foo
after Nothing    = ()

In Haskell, it is very common to define recursive functions (mentioned in motivations already) this way. https://en.wikibooks.org/wiki/Haskell/Pattern_matching It also has a nice mathematical feel to it, hence the readability. So a solid +1 on this RFC from me.

Even syntactic sugar can be useful sometimes.

If this is implemented in Rust, it shouldn't be called overloading in the docs, as that requires different arguments (arity/types) and instead just pattern matching. https://en.wikipedia.org/wiki/Function_overloading

@glaebhoerl
Copy link
Contributor

It's worth noting that Elm considers this bad style and has even removed it from the language. (There might be better links but that's the only one I found.)

@Centril
Copy link
Contributor

Centril commented Apr 5, 2016

@glaebhoerl that's strange, it is considered good style in haskell =)

@plietar
Copy link

plietar commented Apr 5, 2016

In Haskell everything is based around functions thus pattern matching on arguments is a core concept.
I would find it much less useful in Rust, and I don't think the added complexity is worth the little benefit.

@Ericson2314
Copy link
Contributor

I don't even like this in Haskell, \case FTW.

@ghost
Copy link

ghost commented Apr 7, 2016

@Havvy, How about an alternative solution? Instead of baking in support for function / arity overloading we do it in a macro-esque way that simply (heh) desugars it into a trait implementation.

fn new_to_string!(x: u8) -> String {
    x.to_string()
}

fn new_to_string!(x: u32) -> String {
    x.to_string()
}

desugars to:

trait NewToString {
    fn new_to_string(self) -> String;
}

impl NewToString for u8 {
    fn new_to_string(self) -> String {
        self.to_string()
    }
}

impl NewToString for u32 {
   fn new_to_string(self) -> String {
       self.to_string()
   }
}

fn new_to_string<T: NewToString>(x: T) -> String {
    x.new_to_string()
}

That's a third of the characters (115 vs 331) to write the same thing.

We would need a different (but similar) syntax to that because it would conflict with macros, but you get the idea.

This way we know from the source that what we're doing is not function overloading but instead generating a trait implementation. We also don't increase the complexity of the language/compiler, and are simply providing something that helps with creating traits.

Snake case for the function converts into pascal case for the trait implementation.

Currently, macro_rules prevents us from writing this as a macro:

macro_rules! overload {
    (fn $name:ident($($arg:ident: $ty:ty)*) -> $result:ty $block:block) => ();
    (fn $name:ident($($arg:ident: $ty:ty)*) $block:block) => ()
}
src\main.rs:129:55: 129:67 error: `$result:ty` is followed by `$block:block`, which is not allowed for `ty` fragments
src\main.rs:129     (fn $name:ident($arg:ident: $ty:ty) -> $result:ty $block:block) => ();

For syntax, maybe trait fn?

trait fn new_to_string(x: u32) -> String { ... }

or a fn! macro?

fn! new_to_string(x: u32) -> String { ... }

@Havvy
Copy link
Contributor Author

Havvy commented Apr 7, 2016

@Zvxy That's something completely different (actual overloading), and is backwards compatible with pretty much any function clause syntax. Function clauses are basically a marriage of fn and match.

@ghost
Copy link

ghost commented Apr 7, 2016

@Havvy, oh derp. I didn't notice you didn't include arity overloading in your rfc.

clauses with each clause having the same type signature but different refutable patterns

Heh.

@Havvy
Copy link
Contributor Author

Havvy commented Apr 7, 2016

Hmm, I have a general concern with how any of these syntax proposals would not be possible with closures...

@mdinger You could actually combine refutable and irrefutable patterns using that syntax.

match fn foo(Bar{baz, ..}: Bar) -> Result<Baz, Baz> {
  (Bar{qux: Some(_)}) -> Ok(baz),
  _ -> Err(baz)
}

I have no clue if that would actually be useful.

@glaebhoerl
Copy link
Contributor

Just to explore the alternative of \case (per @Ericson2314), in Rust it could look like something like this:

results.filter(|match| {
    Ok(_) => true
    Err(_) => false
})

(Obviously not the most realistic example. Examples are hard.)

(Another option is to just omit the scrutinee to denote a match-lambda, with an intuition based on partial application, but this was deemed not obvious enough by the GHC folks (hence \case and not case of), and is probably even less suited for Rust, considering that it doesn't even have partial application of functions (and it may be syntactically ambiguous to boot):

results.filter(match {
    Ok(_) => true
    Err(_) => false
})

.)

Anyway this doesn't help as much as it does in Haskell, because in Haskell you can do myfunc = \case ..., but Rust doesn't normally swing that way. Maybe with consts:

const myfunc: fn(Option<bool>) -> (bool, bool)
    = |match| {
        Some(true) -> (true, true)
        Some(false) -> (true, false)
        None -> (false, false)
    };

But that's still a bit eh. Maybe if it were allowed to omit the type annotation and write

const myfunc = |match: Option<bool>| -> (bool, bool) {
    Some(true) -> (true, true)
    Some(false) -> (true, false)
    None -> (false, false)
};

instead... what if we also had fn literals (cf #1558)?

const myfunc = fn(match: Option<bool>) -> (bool, bool) {
    Some(true) -> (true, true)
    Some(false) -> (true, false)
    None -> (false, false)
};

I'm not sure if any of these feel really convincing to me...

@eternaleye
Copy link

@glaebhoerl: Along the lines of omitting the scrutinee on inline ones, what about match _ { ... } ? That is, explicitly using a placeholder value.

As far as definition goes, I still think match fn is the best syntax I've seen: It preceding the argument list gives the reader context, and unlike match: allows matching the whole argument list cleanly (as a tuple).

@plietar
Copy link

plietar commented Apr 7, 2016

While match fn looks quite clean, it includes some detail of the function's implementation in the signature.
At first glance, match fn foo(bar: u8) looks like a special kind of function which should be invoked differently.

@nikomatsakis
Copy link
Contributor

After some discussion yesterday in the @rust-lang/lang meeting, we've decided to close this RFC for prioritization reasons. The core idea here is very good; I think most of us have wanted some sort of shorthand for a match from time to time (though I personally have wanted it primarily in closures, which aren't addressed by this RFC). But it would be a major new piece of syntax which would require a large amount of bandwidth to work out, and it just doesn't seem to be sufficiently common for that to make sense right now, given all the other things going on.

I have opened a postponement issue here: #1577 (so please feel free to continue discussion there)

@Centril
Copy link
Contributor

Centril commented Mar 18, 2018

Not that this has any bearing on this RFC (since it was closed in 2016), but in case anyone reads this in 2018 and beyond... I'd like to take back everything I said and align myself with @Ericson2314's comment:

I don't even like this in Haskell, \case FTW.

I think:

after :: Maybe Foo -> ()
after (Just foo) = consume foo
after Nothing    = ()

is bad style and I would never write Haskell that way these days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.