Hacker News new | ask | show | jobs
by willtim 1880 days ago
In Rust, a function definition left-hand-side looks like an annotated pattern, e.g.

foo(x : int)

Therefore, one would expect to annotate the return type as,

foo(x : int) : string

Since the pattern is showing foo applied to x. The Rust syntax is actually confusing for both Haskell/ML programmers (where the arrow comes from) and mainstream programmers. It's too small an issue to change now though.

Rust's support for proper "algebraic data types" is very good and gives it an advantage over languages like C++. However there are some small surprises, such as forcing all enum constructors/fields to be public (one must therefore wrap it to make an abstract data type).

Every language has its warts and these are particularly minor ones.

6 comments

That would imply that "foo(x: int)" is a string rather than a function.

Haskell doesn't use that notation either, it uses -> both for the parameter list and for the return type, and separates argument names (arguably, these are poor choices, since currying is not an efficient CPU-native operation and not intuitive so distinguishing between multiple arguments and returning closures is useful, and argument names are useful for documentation).

> That would imply that "foo(x: int)" is a string rather than a function

But foo(x : int) is a string! It literally reads "foo applied to x". In the function definition, it appears to be used as a left-hand-side pattern which is "matched". The definition is written as if to say, whenever the term foo(x) is encountered, use this definition here. At least, that was my expectation.

> Haskell doesn't use that notation either

OCaml does and Haskell once had a proposal to add it. Haskell type signatures are normally written separately, but it does support annotating patterns with the right extensions.

No, foo(x: int) is not a string, it's not even an expression, it's not even an AST node. It's a fragment of the larger ast node

    fn foo(x: int) -> ReturnType {
        body
    }
The ast here splits into

    Function {
        name: foo
        signature: (x: int) -> ReturnType
        body: body
    }
I.e. the arrow binary op binds more tightly than the adjacency between foo and x: int. And the type of foo is a function, not a string.

A "better" way to write this (in that it breaks down the syntax into the order it is best understood) might be

    static foo: (Int -> ReturnType) = {
        let x = arg0;
        body
    }
Or to put it another way. Reading foo(x: int) as "foo applied to x" in this case is a mistake, because that's now how things bind. You should read that "foo is a (function that takes Int to String)". It's a syntactic coincidence that foo and x are beside eachother, nothing more.
That's a nice explanation of what's going on. My point remains that I found the syntax confusing though.
Ya, I'm not really going to defend the current syntax past "function syntax is hard".

It's mixing up assigning a global variable, specifying that variables type, and destructuring an argument list into individual arguments, in one line. I've played at making my own language, and this is one part that I've never been satisfied with.

Personally I'd probably at least go with a `foo = <anonymous function>` syntax to split out the assigning part. But that's spending "strangeness budget" because that's not how C/Python/Java do it, and I can understand the decision to not spend that budget here...

> But foo(x : int) is a string!

Can you say so decisively for a language with first-class functions?

You are quoting me out of context. In many languages, the term and/or pattern foo(x : int) is a string, if foo : int -> string.
Just a nitpick, but currying is a language detail, one can potentially create an efficient implementation with or without them.
Yes and of course Haskell is quite capable of supporting uncurried functions too, e.g.

foo :: (Int, Int) -> Int

foo (x, y) = ...

I don't think that would be the implication, because we are in the context of a function. Otherwise, we should also not write foo(x: int) but foo: int -> ?
I wouldn't say this is a wart or confusing (to me at least). As someone who uses Haskell, C++, and Rust regularly, I just accept that each language has its own syntax. It's true that Rust borrows ideas from many languages, but I view Rust's syntax as its own thing, and the meaning of the symbols are what they are. It doesn't have to do things the C++ way or the Haskell way. It does things the Rust way, and that's not a wart.
It did confuse me when I first saw it, but yes, it is not really a significant issue.
Having used both Haskell and main stream programming languages I did not at all think that was confusing. The type of "fn foo(x: int) -> string" is quite obviously "fn(x: int) -> string" for people coming from languages like C. I do not see how a colon would make anything more clear. Imagine the function "fn bar(x: fn(x: int) -> string)", would that be more clear with a colon?

On the other hand the enum thing is certainly surprising.

> Imagine the function "fn bar(x: fn(x: int) -> string)", would that be more clear with a colon?

In your example, why bother naming the inner "x" variable for the function param? It cannot be used on the right-hand-side (definition of "bar"). For that reason, the notation is not exactly "clear". In OCaml the annotation would be:

bar( x : int -> string )

In Rust you can write ```f: fn(i32) -> i32```.

Ocaml's syntax is more consistent, I agree, but its colon operator has different precedence than in Rust, so I am not sure its rational applies to Rust.

I typed `foo(x : int) : string` when I was starting and there was an error message telling me to use `-> string` so many people expect this syntax.
> one would expect to annotate the return type as

> foo(x : int) : string

> since the pattern is showing foo applied to x.

kind of like the C/C++ "declaration mirrors use" thing, i.e.

  int *foo;
which means

"the result of dereferencing `foo` is of type `int`"

which is not the same as saying

  foo : Ptr<int>;
because in the former, you're kind of describing what `foo` is a without actually saying it, if that makes sense.

i find that way of specifying types counterintuitive in both C/C++ and MLs.

FYSA, The Rust approach is the same way it's done in Python.
Well the Python community is hardly an authoritative figure on static typing :)
This is true but Python has a user community which is several orders of magnitude broader, which confuses the issue a bit. These days if I was designing a language I'd probably ask “How would I explain this to someone who learned JavaScript/Python?” since even if you have great reasons for doing things differently it's a pretty reasonable way to predict sources of confusion for newcomers.