Why does Rust declare function definitions in reverse?

This post is long for a simple question, but a large goal here is to demonstrate what I have personally felt is a significant pain point that didn't have to be.

I am a Rust beginner who was attracted to Rust by its language features. Most notably these:

Zero-Cost Abstraction
Memory Safety without a GC
Composition-based structure via Traits

I come primarily from a C# background (though I wrote c++ an age ago).

I recognize that the biggest need for Rust is found in embedded systems where people are tired of the long legacy of problems with C. That being said, I personally felt attracted to Rust because I would like to write more native code without having to go to a language that makes it easy to hang yourself. Even though I pride myself on clean code with minimal bugs and could get by in C++ myself, as a developer who manages other programmers, Rust is even more valuable to me because I can more safely count on junior programmers not writing memory-leaking crud.

The overall point being that Rust seems very attractive to someone from a Java or .NET world because we can say goodbye to the garbage collector, while gaining other benefits like the Trait-based architecture and (my favorite) zero-cost abstraction.

But I have a couple of hangups with the syntax and readability. And even though, I can look past them to see the genius of Rust, I believe the syntax is going to drag down development by developers who might otherwise get along in an easier-to-read language like C#.

My single biggest question is "why are the function definitions reversed?" and moreover, what's the point of the "fn" keyword? I can't see what's wrong with the C, C#, and Java way of doing things where the function leads with its return type (google agrees. Look at Dart).

On my team, I insist on readable code, and I make sure my team members steer away from over-zealous use of "var" keyword in c#. I want clearly defined Types that imply intent and improve readability in statements, expressions, and functions. When a reader scans code, their eyes anchor to the left of the lines, which means you need the most important information located at the left. In my experience, function return types and their name tell you more about what a function actaully does than its input parameters. To read Rust functions fast, you have to read the function name and then jump over to the right side of the line to see the return in order to find out what the function's overall objective is. That's not fast to scan and ingest.

Now maybe others disagree with me, but to me after reading code for years and doing code review with junior developres, this seems like it should be painfully obvious. Moreover, C#, C, and Java all define functions this way and those three languages together make up the lion's share of development. And to further support the belief in their design, just take a look at Dart. Google is betting the farm on Dart and Dart reads almost verbatim like C# or Java. I think Dart has an enormous furture ahead of it, and with more and more programmers moving to another language with C-like syntax, I'm sitting here wondering if Rust has created for itself an unnecessary uphill battle with the way it defines its most basic feature: a function.

Of course, many people may read this and think this absurd and I should be drinking the Rust kool-aid without any complaints. We can all look past the "shape" and "arrangement" of function definitions and accept their reversed order. But, the science of ergonomics says that's a recipe for nothing but problems -- and why should it be so? Did the function definitions really have to be reversed from the most commonly used languages?

Its quite possible everything I've written here is going to be met with a really darn good reason for why the function definitions are backwards, but I have yet to figure it out from hypothetical guessing.

Let me address my other syntax hangup for a moment -- a hangup which I fully accept and see as merited:
I hate the "let" keyword, but after some guessing I did figure out that the use of let must be an ergonomic decision to force Rust developers to write sound code.

I personally would rather see an "imm" keyword for immutable and abolish "let" completely. So code would be written like this:

imm x = 1;
mut x = 1;

instead of this:

let x = 1;
let mut x = 1;

However, I see the line-of-thinking as an ergonomic choice of let. Because "let x" takes fewer keystrokes to type, deveopers will naturally favor grabbing it before reaching for "let mut x", which means developers will be inclined to define immutable variables first and only make them mutable where necessary. This is pretty clever. I think its ugly as sin to read, but during the writing process it does encourage good code design.

That being said, I'm sitting here guessing there must be some similar design reason for why functions start with "fn" (which tells me absolutely nothing useful) and end with the return type. But I can't figure out what the reason is.

Can anyone shed some light on this?

p.s. I'm sorry for the long post. And I'll reiterate that I love Rust's core design principles -- they're amazing! But I want it to be more readable (especially during code review) -- and I feel like its lacking in that area.

2 Likes

Readability is incredibly subjective, and most likely just a reflection of what you're already used to.

23 Likes

many people may read this and think this absurd

Well, yes, I doubt this is going to get a good response. Is there any actual evidence that one is better than the other? It's just a question of what you're used to. I can't stand C syntax for function declaration - the lack of a fn keyword or equivalent makes it hard to visually parse or to grep for. But I am happy to admit that it's probably just because I haven't written much C.

In C++, that style of function declaration leads to the most vexing parse.

9 Likes

The fn keyword makes it easy to search for function definitions. If you want a list of all functions in a Rust file, you do grep '^\s*fn '. C provides no reliable alternative.

I find type suffixes clearer than prefixed types, because it reads the way I say it. Historically, it's largely because ML languages did it that way, and people tended to like it well enough not to change it.

6 Likes

To be fair, Rust doesn't have this problem because type name(expression) isn't a valid construction elsewhere in the language (:crossed_fingers: that somebody doesn't prove me wrong).

fn name is definitely more grepable and I think it ultimately comes down to personal preference. I have no problem reading function signatures in Rust, and I program regularly in C++ (majority of the time), Fortran (kill me), and Rust. The only wrong one here is Fortran. :slight_smile:

How about when returning nothing? i.e. implicit -> (), where C and C++ always make you write void.

To be specific, let mut can't be that easily swapped around, because let doesn't take a variable name, but a pattern that can be bigger and define several bindings:

let (mut x, y) = (1, 2);

defines mutable x and immutable y.

In C existence of typedef makes

foo * bar;

ambiguous, because it could be either multiplication or definition of bar, depending on whether any header anywhere has typedef foo, so you can't just parse one file — you have to preprocess and parse all system headers it includes first. There are similar ambiguities with casts, so special-casing unused multiplication is not enough.

One thing that C IMHO got right, and Rust didn't, is the struct literal syntax. Foo {.a = 1, .b = 2} is easier to edit from/to foo.a = 1 and both would be greppable with the same pattern.

But Rust has shipped this syntax with 1.0, so it's frozen and not up for debate.

3 Likes

Obviously this case should be written as:

() my_func() {
    // code here
}

:slight_smile:

1 Like
foo<bar > name(baz);

I mean, rust currently has a rule to explicitly forbid this because it looks like a chained comparison, but it's totally accepted by any sensible encoding of the expression grammar!

2 Likes

Haha ok, I accept this as a refutation.

Do you have any source for the claim about the "science of ergonomics" and function definition order? You keep using the adjective "reversed" to describe how Rust does it, but in fact the Rust ordering is the most syntactically straight forward order for an English speaker. The function definition reads left to right.

I also disagree that the return type is the most important bit of information. If you don't have the correct input you will never get any output, so the input type is every bit as important. Furthermore, the reductionist approach to function definition (i.e., the idea that knowing the return type leads to understanding the point of the function) is intrinsically flawed. To understand what a function is intended to do one must know both the input and the output parameters.

2 Likes

I actually find the C style reversed. When reading the code left to right, I first read the parameters, then the return type. When the function is called, it first needs to get the parameters and then it provides the result :innocent:. Even the arrow can be considered as kind of „leads to“ thingie.

Anyway, specific syntax is quite personal preference and the easiest thing to get used to, compared to other nuances of a language.

1 Like

Google doesn't really have an opinion. Go has the return type at the end, and it was designed by people having written C for literally decades. I can't find the quote right now, but I remember reading that Rob Pike thought the order of C prototypes was in reverse, so he fixed it for Go.

Judging from repos on GitHub and posts on StackOverflow, TypeScript seems to have about ten times the user base of Dart, and they have the return type at the end. Your comment sounds like the general trend is moving towards putting the return type at the beginning, while I feel the opposite is true.

The left side of a let statement is a pattern, and it can be a lot more complex than these examples suggest. Your imm keyword could occur somewhere in the middle of a more complex pattern, and it would become a lot less obvious what kind of statement we are dealing with. The let at the beginning of the line makes the type of statement clear upfront (and the same is true for fn).

6 Likes

A few more examples of languages whose function signatures are similar to Rust's:

Swift:

func f(x: ArgType) -> ReturnType

Nim:

proc f(x: ArgType): ReturnType

Kotlin:

fun f(x: ArgType): ReturnType

ActionScript:

function f(x:ArgType):ReturnType

Haxe:

function f(x:ArgType):ReturnType

Julia:

function f(x::ArgType)::ReturnType
9 Likes

fn 's can have (textually) "large" signatures:

  • setting up multiple input parameters
  • some nested structure of utility types (Option<Arc<Mutex<HashMap<...)
  • tuples for multiple inner values
  • lifetimes, for inputs and return values
  • trait bounds for inputs
  • possibly nested function-pointer signatures

->

  • the result and conclusion needs some space for itself
  • reinforcing the structure of an fn declaration, rather than hiding statement and name

where

  • separating some of these things into their own layout becomes necessary
  • and can reduce repetition and ambiguity for common elements

{

  • oh, and by the way, fn and let and struct and enum can all occur within a block to introduce a new function, binding, etc into scope.

}

3 Likes

I really like the consistency of fn and let.

How do you declare a struct? struct name
How do you declare a union? union name
How do you declare an enum? enum name
How do you declare a function? fn name
How do you declare a variable? let name

Having that keyword at the beginning helps make it more obvious for both humans and the compiler (for error messages especially) what's going on.

Even C++ is going that way, see posts like

8 Likes

Readability largely just comes down to personal preference and whatever you were last exposed to. As somebody that switches between C#, F#, Typescript, and Rust on a daily basis, function declaration syntax is not even something I think about usually because it's pretty obvious from context what I'm looking at.

However, to give you a concrete example of why the C way is suboptimal, let's look at a simple example from the world of functional programming:

Suppose I want to curry an add function and get an addTwo function (in F#):

let add l r = l + r
let addTwo = add 2

addTwo 1 //3
addTwo 2 //4

To do that in C, I need to make add a function that takes one int argument, and returns a function that takes one int argument and returns an int. This is the add function's signature in C

int (*foo(int ))(int )

Yikes! Reading that requires using the "right hand clockwise rule" and is pretty (IMO) unintuitive.

C# does a bit better:

Func<int, int> Add(int)

What's rather strange though is that to read the C# version, you read Add() right to left to see arguments and then return value but then you read Func<int, int> left to right to see the same info.

Here's what it looks like in Rust:

fn add(isize) -> impl Fn(isize) -> isize

Now, to read arguments and then return value, you just read left to right across the entire declaration. Isn't that more consistent? :slight_smile:

8 Likes

you can add Elm (and I assume Haskell)

function: ArgType -> ReturnType

Another point to note, is that this order applies to both fn and let, and Rust could not simultaneously use the C order for let and support type inference.

In C, you have

int x = 2;
int f() {}

While in Rust it is

let x: i32 = 2;
fn f() -> i32 {}

But it can also be

let x = 2;

That is, when the type is omitted, the type inference kicks in. With the C order, you cannot just omit the type, since it is part of the syntax to declare a new variable. This is the reason C++ had to introduce/reuse the auto keyword to support type deduction, and end up with a syntax similar to Rust in the case the type is omitted.

auto x = 2;

You can find the same order with the C++ functions declared with auto, and with lambdas:

auto f() -> int {}
[]() -> int {}

In the end, C++ mixes both orders anyway, and uses a longer keyword than Rust.

4 Likes