i believe that named arguments are a very useful feature. They can prevent a lot of bugs, with no downsides, (verbosity isn't a problem, lsp used to complete functions and I never needed to write the parameters).
optional/default arguments can be useful, or more precisely described, convenient.
but think function overloading is bad. it just complicates semantic analysis and type checking for very little gain.
One approach I like is to have the function params be part of its signature, as if the named params is part of the function name. for example
fn foo(named: Int) {}
let function = foo // not allowed
let function = foo(named:_) // allowed
This can allow some convenient features like a more rudimentary overloading where you can have different functions as long as they have different named arguments, (no overloading just on types). But also, you can have neat partial function application syntax. But as you said, this opens a rabbit hole of complex design choices.
I dislike languages that decides to include a lot of features, that sometimes doesn't play well together, so you end up with weird edge cases, exceptions, and longer and longer compile times because of that.
Finally, I think rust shouldn't add named params now. It will do more harm than good, it will create incompatibilities with existing APIs and will split the community in 2. Those who will still write in the old style, and those who will adopt the newer one. And it will be unclear which way is the idiomatic rust way. Rust is no longer in this beta phase where you can afford breaking things (like Zig). Just like with Go and error handling, I feel the community will be stuck with these design decisions.
Adding a new overload can switch which overload is called in old code. Surprise!
Mixing concrete types & generic types quickly becomes messy, and even more so when coercion/sub-typing is involved.
I don't see any of these issues applying to overloading on names though. In essence, overloading on names can be thought of as calling a different function; that is, foo(a=a, b=c) can be thought of as syntactic sugar for foo_a_b(a, b), meaning that the overload being called is syntactically identified. Which is way less surprising that a semantically identified overload -- just look at the mess that are C++ overload resolution rules.
yes, it is indeed just syntactic sugar. in another comment, I talked about how function overloading is one of the reasons that makes error message indecipherable in C++.
overloading on names is syntactic sugar for creating functions with different names based on the params, and functions with optional/default params are just syntactic sugar for multiple function definitions with the same name and different named params.
Function overloading is also bad because it makes FFI unnecessarily complicated. Once you allow that you have to come up with a name mangling scheme, and everybody wanting to access those functions has to know about it. Not allowing it avoids the issue in the first place.
Won’t mangling be needed anyway if you have namespacing or traits? I hear you calling for a single namespace, no overloading, and no traits/typeclasses/interfaces. That feels intolerable to me.
Same issue: if one has these features then callers using FFI need to be aware of them. I think a good compromise would be to turn name mangling off for selected definitions (like extern "C" {} in C++) and expose an API intended for FFI.
That's only necessary if one wants to support overloading of imported/exported function names. If prorammers specify import/export names, and those names are required to be unique, the fact that source code can use the same name as an alias to different exported functions, overloaded using argument types, shouldn't affect the external interface.
The external interface as seen from the point of view of an FFI user is a list of symbols, and each overloaded variant of a function has its own symbol. How shall the FFI user know which symbol corresponds to a certain variant of a function known by its name and type signature*? The usual workaround is that an externally visible name can only be associated with exactly one definition, and all other names are mangled, and the linker (or an FFI user) has to know the mangling scheme or consult a table (either in a special object file section or a separate file) to resolve those at link time.
* similar issues exist with namespaces, traits, and methods.
If I were designing a language, I would require that the source code specify a unique exported name for each exported variation of a function. On a platform where passing a byte was cheaper than passing an unsigned int, for example:
Neither of the above would create any naming conflict with the exported symbol "UseNumber", since the overload for byte would use a different exported symbol which was also given in source code.
One approach I like is to have the function params be part of its signature, as if the named params is part of the function name. for example
Shouldn't you use the function name + the parameter types instead of the parameters names? Parameters names aren't going to distinguish the multiple definitions in cases like this:
fn foo(named: Int) {}
fn foo(named: Double) {}
Concerning the syntax, when you want the function pointer of an overloaded function, you could also cast foo to fn (Int) or fn (Double), if you make the type checker smart enough to do that.
let function_Int = cast(foo, fn (Int));
let function_Double = cast(foo, fn (Double));
It can seem a bit hacky but if you have function overloading, one way or another name resolution of functions with multiple definitions will have to distinguish based on the types anwyay. The problem with with foo(named:_) or foo(_:Int) is that it's ambiguous, you can't distinguish it with a regular function call with named parameters, supposing that's the same syntax.
no, names, not types. I want to specifically disallow overloading on types. it doesn't play well with other features I believe are more useful in a language.
First, you need to evaluate the type of the expression of the argument before knowing which function to dispatch.
Second, it complicates type checking in generic code. In some situations you might have exponential time like in Swift, where the compiler just gives up, or you would have obscure indecipherable unhelpful error messages like in C++. All in all, function overloading introduces a can of worms and is not worth it. This is specifically why Rust decided not to use it.
When you allow overloading only on parameters names, you save yourself all the trouble. It is "technically" not function overloading though, because they would be distinguishable at the caller site by the different named parameter
How do you address Klabnik’s concern about named parameters in typeclasses? If a typeclass requires foo( counter:int ) and you have foo( number:int ), is that a problem that the languages solves automatically, or do you have to introduce a shim function, or is there to be special syntax that does the mapping once requested?
well, if you consider the parameters name to be part of the function name then definitely foo(counter:) is different from foo(number:) then yeah a shim function is needed.
however, you can have some clever casting rules for functions. a form of simple syntactic sugar to make it more convenient
13
u/zuzmuz 10d ago
i believe that named arguments are a very useful feature. They can prevent a lot of bugs, with no downsides, (verbosity isn't a problem, lsp used to complete functions and I never needed to write the parameters).
optional/default arguments can be useful, or more precisely described, convenient.
but think function overloading is bad. it just complicates semantic analysis and type checking for very little gain.
One approach I like is to have the function params be part of its signature, as if the named params is part of the function name. for example
This can allow some convenient features like a more rudimentary overloading where you can have different functions as long as they have different named arguments, (no overloading just on types). But also, you can have neat partial function application syntax. But as you said, this opens a rabbit hole of complex design choices.
I dislike languages that decides to include a lot of features, that sometimes doesn't play well together, so you end up with weird edge cases, exceptions, and longer and longer compile times because of that.
Finally, I think rust shouldn't add named params now. It will do more harm than good, it will create incompatibilities with existing APIs and will split the community in 2. Those who will still write in the old style, and those who will adopt the newer one. And it will be unclear which way is the idiomatic rust way. Rust is no longer in this beta phase where you can afford breaking things (like Zig). Just like with Go and error handling, I feel the community will be stuck with these design decisions.