This syntax looks ugly... why?

Why the <T,U> next to the function name?

fn print_trio<T, U>(x: T, y: U, z: i32) {
    println!("x: {:?}, y: {:?}, z: {:?}", x, y, z);
}

When it could've just been in the function parameter declaration

fn print_trio(x: <T>, y: <U>, z: i32) {
    println!("x: {:?}, y: {:?}, z: {:?}", x, y, z);
}

It's worse than that, it has to be this or it won't compile:

fn print_trio<T, U>(x: T, y: U, z: i32) where T: Debug, U: Debug {
    println!("x: {:?}, y: {:?}, z: {:?}", x, y, z);
}

but it can be shortened to:

fn print_trio(x: impl Debug, y: impl Debug, z: i32) {
    println!("x: {:?}, y: {:?}, z: {:?}", x, y, z);
}

As long as you don't need any relationships between types or complex trait bounds.

2 Likes

Some whys:

  • Sometimes a function has to be called with explicit generic parameters, in which case it is clearer for the declaration to contain that parameter list in the same order.
  • The generic parameters might not be used in the parameter list or return type at all — e.g. TypeId::of().
10 Likes

It may be subjective. To me, fn print_trio<T, U>(x: T, y: U, z: i32) { is easier to read and understand than fn print_trio(x: <T>, y: <U>, z: i32) {. The former declaration is clear: there is a function print_trio and it has two template arguments. But as for the latter, you have to read carefully the entire declaration to determine whether a function is a regular one or it is a template one.

1 Like

How would it work if you want to take a Vec<T> parameter?

2 Likes

If you don't need named generic types, then impl Trait does the job:

fn print_trio(x: impl Debug, y: impl Debug, z: i32) {
    println!("x: {x:?}, y: {y:?}, z: {z:?}");
}

There were also ideas to unify arguments and generic parameters. Something like this:

fn f(
    x: &[T; N],
    type T: Debug,
    const N: usize,
) { ... }

With hypothetical optional/default arguments syntax we could allow this function to be called without explicitly providing the later two parameters when they can be inferred.

While I like such syntax, it's highly unlikely that we will see it in Rust 1.x.

1 Like

Because that's what C++ did, and Rust wanted to avoid picking a syntax that C++ users would find weird and unfamiliar.

4 Likes

:scream:

As a meta-point, note that "ugly" is never a helpful phrasing, as it's fundamentally subjective.

Why is it that way? Well, mostly because that's what C++ Templates look like on the use side, that's similar to what Java generics look like (albeit in a different spot), and is exactly what C# Generics look like.

So it looks similar to what other curly-brace languages tend to have, which is what Rust generally does for syntax unless there's a strong reason to break from convention. Familiarity is good, so long as it's not majorly-misleading.

12 Likes

Another why: It's consistent for generics to go on the function name like they do for structs.


A couple of people have mentioned argument position impl Trait (APIT).

fn print_trio(x: impl Debug, y: impl Debug, z: i32) {

Be aware that the only potential benefits of argument position impl Trait are ergonomic/aesthetic (and those quickly fall apart in the face of multiple bounds). You can't name the types, callers can't name the types (turbofish), and it's incompatible with precise capturing.[1]


  1. It can also be a breaking change to switch between named generics and APIT. ↩︎

2 Likes

It's definitely 'ugly' in my opinion, and after reading your answer I think I know why. I think it's because I don't have Java or C-ish background.

Tell me... is there still hope for me? I'm really struggling learning Rust

EDITED:
I am a Node.js developer. I used to be a Ruby dev. I do a bit of Python for sys admin stuff. I'm not the best with Typescript, just plain JS

Yes. But to give a better answer, can you say what languages you know?

I edited my comment ... basically Node.js (JS not so much TS), some Python, and some Ruby

I used to do Ruby (on Rails) before discovering Rust, and I find the two to be nearly equally beautiful.

I myself have a background in Python as well and did web application backend development with Python 3 for almost 10 years. Back at the university we had mandatory courses for Java and Scheme (I still remember all the memes) and later at another uni I also learned C++. In-between I taught myself some C and also did a small contribution to the Linux kernel (for an AMD HID driver). So there's definitely hope for you, if you're willing to embrace new ideas and adapt to unfamiliar syntax.

I don't mean to be condescending, but if you don't have familiarity with languages that do have strict and static typing and generics, judging a syntax that is meant to solve a real-world issue as "ugly" might come across as ignorant. There is no accounting for taste, but such a judgment coming from somebody without necessary background knowledge may come across as trollish behavior.

3 Likes

I can give you one general tip I learned over the years when it comes to taste questions while learning new programming things.

Don't trust your initial negative response to a new concept or syntax. Use it for a while and see if you get used to it. Most negative responses in my opinion just boil down to the new thing not beeing familiar. So most problems or feelings of uglyness just go away over time. Then you can focus on the things that still bug you and try and improve these.

The same holds for descovering new codebases. Don't judge bevor you got a good look at it.

7 Likes

Well, I'm C++ developer with decades of experience yet I still find C++ and Rust syntaxes ugly and Haskell syntax beautiful.

But so what? I still would program things in C++ and hope to switch to Rust while Haskell is something I admire yet couldn't use for my $DAYJOB.

Rust absolutely did the right choice with it's syntax: it's very important practical mimicry. Rust had to had syntax similar to C++ to succeed.

Just accept the fact that Rust syntax, while ugly, is designed to be easy to parse both by compilers and humans. At least Rust functions don't look like this:

 void (*signal(int sig, void (*func)(int)))(int);

Can you even tell why this is even a function and not a pointer, at a glance? And yes, that's something from the standard documentation.

Different people are different. For me feeling of ugliness of C, C++, or Rust syntax never goes away. But I learned to appreciate consistency and decidability.

Yes, decidability, damn it! C++ syntax, is, quite literally, undecidable. Means: it's not possible to write a program that would be able to say whether is something is syntactically valid C++ program or not. Neat, isn't it?

And C syntax. Well. What do you think about this:

    int coords[3]

Can I put 4 coordinates in that array? No? But what if I zoom out a bit:

   void process(int coords[3])

And now it's Ok, right?[1] Who the heck developed this atrocity?

Rust syntax may be ugly, but it's practical, decidable and easy to parse… what else do you need?


  1. Technically it's “maybe” because coords is a pointer here and answer depends on how much memory was passed into that function. ↩︎

3 Likes

TS also has the same syntax for generics btw.

1 Like

Odd this hasn't been mentioned yet, but there is a real difference between the syntaxes: you can't write the equivalent of fn foo<T>(a: T, b: T) in the latter without additional syntax or logic.

As for why the syntax looks like that in the first place? In C++ (which may be the origin of this particular angle bracket syntax, though surely not the concept?) the equivalent feature is called "function templates" or "class templates", with the typically abysmal syntax:

template<class T, class U>
void foo(a: T, b: U);

with the semantics, roughly, that when the template is instantiated, the template parameters are replaced in the templated declaration as a whole creating a new declaration.

One way to view this is that the template is a compile time function that creates new declarations when needed, so you can view the angle brackets as the parameter list for the template with different delimiters to runtime functions. Later languages simplified this syntax (and made the implementation perhaps more complicated!), but the general concept of generic parameters being "higher level" parameters survives (though they're not always compile time in other languages now)

2 Likes