What is the actual reason of having `->` before return type

why -> is used?

instead of just the return type like Go

(Note this is purely personal speculation)

I could see several reasons to do so:

  1. It is more "mathematical" since that is generally the style for functions f(R) -> R
  2. Go require the wrapping of multiple return types in () but not single types. Rust doesn't.

Using -> notation for function types is common in functional languages like Haskell and the ML family, including OCaml. (Rust was originally written in OCaml.) It's also found in languages like Swift and Erlang, which have some common influences with Rust.

10 Likes

I’d like to remark that the similarities to mathematical notation or Haskell are a bit more limited than the previous comments might suggest. In maths or Haskell you give function signatures independent from the defining equation.

mathematical:

𝑓: β„€ β†’ β„€
𝑓(π‘₯) = π‘₯Β² βˆ’ 1

Haskell:

f :: Integer -> Integer
f x = x^2 - 1

Rust (although i64 is not quite all integers):

fn f(x: i64) -> i64 { x.pow(2) - 1 }

for the record, there’s also (among even more other alternatives):
mathematical:

𝑓: β„€ β†’ β„€
π‘₯ ↦ π‘₯Β² βˆ’ 1

4 Likes

What exactly are you referring to?

Rust doesn't have multiple return types. It has tuples though, and tuples do require ().

Also - how is this relevant to the ->?

1 Like

The -> is consistent with the Fn traits:

fn foo(f: impl Fn(i32) -> i32) -> i32 {
    todo!()
}

Whereas in Kotlin, functions use : but closures use ->, which I find inconsistent:

fun foo(f: (Int) -> Int): Int =
    TODO()

The same applies to TypeScript. In Go it is consistent:

func foo(f func(int) int) int {...}

But I find this less readable.

9 Likes

but i found this more clean

The usage of -> for function return types has been around since at least Rust 0.1 in 2012. For an example, see this function in libcore from release-0.1:

#[doc(
  brief = "Negation/Inverse"
)]
pure fn not(v: t) -> t { !v }

So I suspect that it's a combination of A) graydon's personal style choice, B) it not being a bad style, and C) momentum from people using Rust and becoming used to it.

Even today, it's fairly ambiguous whether it's good or not. @Aloso, you see it as positive. @milrope, you see it as negative. I'd argue that it's a fairly inconsequential style choice.

There are other similar marks that graydon has had on the project, even though Rust 1.0 was a very different language from when he had stopped working on it. For instance, the fn keyword also remains from that time, as do things with a larger impact, like implicit returns.

5 Likes

I have a really hard time reading these Go types. To me they look like a soup of keywords without any relation (in particular without syntax highlighting). Only parentheses and letters, this could almost be LISP. :wink: There's a reason many natural languages have particles that go between the nouns; -> plays the role of such a particle..

Of course, this is subjective.

28 Likes

What does -> vs : have to do with eliding {}? They seem orthogonal to me.

6 Likes

I'd argue that the signature/body confusion becomes significantly clearer if you never have one-line functions.

For instance, in your example, this reads pretty clearly to me:

fn foo(x: Something, y: SomethingElse) -> Foo {
    Foo { x, y }
}

The first line is the declaration, and the inside is the body. It is a bit more confusing when the declaration spans multiple lines, but avoiding that is usually possible. And when it isn't, like with where clauses, the where clause gives extra space between the return type and the body.

I wouldn't be against a Scala-style function declaration like you've stated, but I think the usefulness becomes very limited once you start writing bigger functions. For a bunch of getter methods, it would be great! But anything more than one line, and it starts to end up a bit weird. Given that limited usefulness, I think changing function formatting to clearly separate them could end up better in pratice.

Given that rustfmt always expands single-line function definitions into multi-line ones, I've never run into this problem in practice. Yes, it's a solution using formatting standards. But is formatting really a bad solution to a readability problem?

3 Likes

Have one-line functions with = been discussed before? Otherwise I'd like to start a new thread.

2 Likes

Couldn't they be implemented as a macro?

Not sure why you'd say that. It works out very well.

6 Likes

Generally, : is used to denote the type of the thing that comes before it. I think it could be argued that in order to be consistent, in the case of functions this would have to be the type of the whole function itself, not just the return type.

8 Likes

Exactly. The thing in front of the : is however (syntactically) not just the function. It is the result of applying the function to some parameters (in the form of some variables). The proposed syntax from @anon2808951 is:

fn foo(x: Something, y: SomethingElse): Foo // { .. } OR = ...

This reads as: foo(x,y) has type Foo. Ascribing a type to foo itself would, in a Rust-like syntax, need to look roughly like

foo: fn(Something, SomethingElse) -> Foo

It is however not an uncommon or particularly bad mistake to mix up a function 𝑓 with the result of its application to a variable, 𝑓(π‘₯). People still say things like β€œthe function 𝑒ˣ” all the time and AFAIK before the 20th century, most mathematicians tended to use 𝑓 and 𝑓(π‘₯) interchangeably in general. However at that time, type systems where not necessarily a thing yet either.

Nowadays, we have a set theoretical foundation of maths, where ∈ plays more-or-less the role that : does in some programming languages and in type theory. So in math, for a function 𝑓 you could write a function signature like 𝑓: 𝐴→𝐡, but the β€œtype” of this function could also be expressed in terms of sets, usually with the notation π‘“βˆˆπ΅α΄¬. This 𝑓 is distinguished from its image 𝑓(π‘₯) for some π‘₯∈𝐴. For π‘₯∈𝐴 you’d have 𝑓(π‘₯)∈𝐡.

5 Likes

TBH, this gives me the slightly uneasy feeling of mixing function declaration with function application, but I can't say whether that actually matters or not. But note that for the type of foo you had to resort to using -> for the return type again.

For a different example, how would the following look in the new syntax?

fn foo(f: impl Fn(Foo) -> Bar) -> Baz

The arrow for the Fn trait would not change, so it would be:

fn foo(f: impl Fn(Foo) -> Bar): Baz

Also note that (as I mentioned in an earlier comment) Rust syntax does not resemble maths very much anyways, and I’m not saying that I’m personally in favor of changing the -> to :. I’m just saying that there is a way in which using : would make a lot of sense. I personally find the way that Rust syntax currently works pretty readable in this regard and as long as it’s readable Rust can do whatever it likes to do.

I think this is the case because the function declaration in Rust (or C or similar languages) syntax is based on function application syntax, that is, you write arguments comma separated, in parentheses, after the function’s name.

In more detail: declaration and definition are actually intermixed in Rust, into one syntactic construct, which is actually mostly just (mathematical) function definition syntax plus some type annotations on the arguments and without an equals sign. Compare my (already mentioned) earlier comment on mathematical function definition syntax; function definitions simply contain an actual function application (with variables as arguments) on the left hand side of the β€œequals” sign, so that’s the fundamental reason why, as mentioned, β€œfunction declaration syntax is based on function application syntax” in Rust.

1 Like

I think you meant instead:

fn foo(f: fn(Foo) -> Bar) -> Baz

The following code is legal today:

let foo: fn(f: fn(Foo) -> Bar) -> Baz = |f| { .. };

So interestingly enough fn foo syntax can be viewed as a sugar for:

const foo: fn(fn(Foo) -> Bar) -> Baz = |f| { .. };
1 Like