Given the big similarity to Rust, and having similar target group (people who care about concurrency in compiled languages), what made you decide to stray from Rust syntax @YorickPeterse ? Personal preference?
It's all about small stuff like: `async fn` (Rust) vs `fn async` (Inko), skipping parens for 0-arity functions in Inko, `::` vs `.`, `Generic<T>` vs `Generic[T]`.
`<T>` in Rust is due to C++ pandering (assuming that C++ like it—unsure). I don’t see why “people who care about concurrency in compiled languages” should care about C++ syntax (which is both ugly to read and hard for computers to parse).
I have mixed feelings on Rust's syntax, especially around generics, lifetimes, and the `modifier -> keyword` syntax (i.e. `async fn` or `pub fn`). For Inko, I wanted something that's easy to parse by hand, and no context specific parsing (e.g. `QUOTE -> something` being the start of a lifetime in one place, but a char literal in another place).
Another motivator for that is that years ago I worked on Rubinius for a while (an implementation of Ruby), and helped out with a parser for Ruby (https://github.com/whitequark/parser). The Ruby developers really liked changing their already impossible syntax in even more impossible ways on a regular basis, making it a real challenge to provide syntax related tools that support multiple Ruby versions. I wanted to avoid making the same mistake with Inko, hence I'm actively trying to keep the syntax as simple as is reasonable.
As for the specific examples:
`fn async` means your parser only needs to look for `A | B | fn` in a certain scope, instead of `A | B | fn | async fn`. This cuts down the amount of repetition in the parser. An example is found at https://github.com/inko-lang/inko/blob/8f5ad1e56756fe00325a3..., which parses the body of a class definition.
Skipping parentheses is directly lifted from Ruby, because I really like it. Older versions took this further by also letting you write `function arg1 arg2`, but I got rid of that to make parsing easier. It's especially nice so you can do things like `if foo.bar.baz? { ... }` instead of `if foo().bar().baz?()`, though I suspect opinions will differ on this :)
Until recently we did in fact use `::` as a namespace separator, but I changed that to `.` to keep things consistent with the call syntax, and because it removes the need for remembering "Oh for namespaces I need to use ::, but for calls .".
`[T]` for generics is because most editors automatically insert a closing `]` if you type `[`, but not when you type `<`. If they do, then trying to write `10<20` is annoying because you'd end up with `10<>20`. I also just like the way it looks more. The usual ambiguity issues surrounding `<>` (e.g. what leads to `foo::<T>()` in Rust) doesn't apply to Inko, because we don't allow generics in expressions (i.e. `Array[Int].with_capacity(42)` isn't valid syntax) in the first place.
> Skipping parentheses is directly lifted from Ruby, because I really like it. Older versions took this further by also letting you write `function arg1 arg2`, but I got rid of that to make parsing easier. It's especially nice so you can do things like `if foo.bar.baz? { ... }` instead of `if foo().bar().baz?()`, though I suspect opinions will differ on this :)
I've spent a little bit of time in Ruby land and such cute syntactical tricks tend to have limited benefits (none?), but significant downsides.
This:
foo.bar()
communicates very clearly that arbitrary code will get executed now.
But:
foo.bar
could be arbitrary code execution or a trivial and cheap field access. No way to know which without extra information. Reading code like this stutters; every instance of x.y requires pausing to look up type information.
There's a second problem. Allowing:
foo.bar
but not:
foo.bar x y
applies special treatment to functions with zero arity. Is there a reason for that? As far as I know function arity carries no special significance.
Consistency shouldn't be disturbed without a sound justification.
There's one advantage of foo.bar calling a function, it works well with referential transparency. Whether it's a property/value or function, only the result matters. I can't say it's a big difference though, I've only been mildly annoyed by having to change all call-sites when changing between them. Other languages allow code bodies for getters/setters (foo.bar = ...) so it still hides the call. For a C-style syntax language having the () seems less surprising.
When it comes to correctness, only the result matters, sure. But not so for performance and code understandability. Obscuring points of arbitrary code execution is awful.
The getter/setter pattern is also bad, but a little less so since it's exceedingly rare any real code will get executed there.
It's all about small stuff like: `async fn` (Rust) vs `fn async` (Inko), skipping parens for 0-arity functions in Inko, `::` vs `.`, `Generic<T>` vs `Generic[T]`.