Compiler internals, function arity

What would actually be easier to implement in the Rust compiler as it stands at the moment

  • default arguments (e.g. even just c++ style trailing defaults) (on mismatched argument count .. fill in the blanks)

  • or currying (replacing mis-matched arity with a lambda, i guess..)

  • or arity overloading (e.g. fn foo(a,b) and fn foo(a) become 2 distinct functions)

I'm not giving an opinion here on which would be more useful; ( I realise there are fans of both(i) and (ii), and these are mutually incompatible , and 'neither' may be the answer to controversy..)

I know parsing default arg expressions is fairly easy, (I got that far when I last looked at it), but I seem to remember not being able to figure out how to actually use them (e.g. what the compiler was actually doing with an unresolved function call).

I remember thinking it would be nice to 'consider the arity as part of the function name', and that could be a step toward implementing any of those features

2 Likes

I think the currying idea would be pretty interesting for Rust and could probably desugar to some unboxes closures. You may end up with really horrible error messages though. Something which may have a similar implementation to currying would be conservative impl trait, although I'm not sure how difficult it would be to track lifetimes when currying is in play.

We already do something similar to the last point (if you squint really hard and tilt your head 15 degrees to the left) with monomorphising generics. So I imagine some of that knowledge could be transferred.

I don't know much about how the compiler works under the hood, but judging from what I can guess of the implementation option 3 may be easiest. It'd be nice to get function overloading, default params, and splats ("*"/"**") like in Python, but Rust has gone over 2 years without any of this functionality and I've never really encountered times when I'd prefer function overloading or any other arity things.

Is it worth complicating the language more when there are tricks like Foo { bar: 5, ..Default:: default () }, patterns, and destructure for using let, which are often alternatives to overloading or default params?

2 Likes

as far as i know this stops short i..e the compiler already knows exactly what function it wants just from the trait name & function name, although it is of course selected by the type of the first arg.

coming back to it after a long period away.. I guess the fact it has the operator overloading (which can be made to use both args, e.g. matrixmatrix, matrixvec doing different things) has helped a lot;

"which are often alternatives to overloading or default params?"

I would argue that thats an argument for currying :slight_smile: ... but what if we could choose on a function by function basis (e.g. initialisation type code/constructors will suit defaults; whilst maths and list manipulation would probably suit currying more.. you're never going to add a default parameter to 'dot_product()', but you would to 'create_window()')

i realise they say , 'well.. the closure syntax is pretty good, it's just 4 characters to make a lambda'.. still, coming from haskell, the extent to which currying makes passing functions around even more 'routine' is impressive

Regardless of which is better.. imagine if the compiler was just taken a step in a direction where it makes the experiments on this easier

  • then the community could make forks and demonstrate: "look , this is how cool rust is with currying!...", or "look how much better these GUI/AI library bindings are here with defaults!" .. and see if this pushes a consensus either way
3 Likes

Interesting discussion points!
Compiler internals are probably better of at the internals forum (more knowledgeable people hang out there), but the discussion how to use them / what is "better" sounds like a good fit here.

As for which is better: I think defaults are easy to do, even without sacrificing future compatibility, if you do them .Net style. That is: defaults are a preprocessing pass of the compiler, where the compiler appends "missing" arguments, resulting in always having a "full" function call in the generated binary.

I'm not familiar enough with currying to discuss those implications. To me it's one of those "apparently cool things the Haskell people keep mentioning" :wink:

Imagine a more elaborate version of defaults :-

  • defaults can be expressions of the other parameters,
  • the compiler analyses the dependancies to figure out which permutations of parameters are allowed
  • intermediate helper functions are rolled, and the compiler figures out which of these to remap to at the call-site , based on the given functions.

This would automate rolling a number of closely related functions in one place (again at a similar stage to macros, but relying on complex logic than the macro system can perform)

e.g. self.slice(from=0,to=self.len()) generates self.slice() { self.slice_sub(0,self.len()) }, etc etc

(i suppose just 'expressions using self' would be simpler and might handle enough use cases)

Interdependent parameter magic sounds nice, but I'm worried it won't pull its weight as a feature.

Rust 1.x is very much about explicitness and "no surprises", because we've seen in other languages that at some point the interactions become to hard to predict for us poor humans.

Having something that automatically interprets the semantics of your parameters and generates the "logical" interconversions sounds basically like the exact opposite of this.

Also, to actually specify those interdependencies, you would need a new syntax, either something directly between the ()-braces, or an extension of where: where paramA=4*paramB etc.

Both would quickly become less readable than simple oneliner wrapper functions.

Now, I know full well I'm in the minimalism camp. Maybe there really are convincing usecases, but adding new syntax needs to reach a pretty high bar for added value! (All concerns in How do we teach this? apply)

What I found in my attempt was a syntax dropped in trivially: i.e. just parsing =<expression> ; I think this is intuitive - it is then a matter of 'running with what the syntax allows you to express';
Although this might make one function seem bloated, I would argue the total work/navigation/communictation would have been reduced, i.e. the amount of similar helper functions that you'd have to scroll through; by defining several things in one place you've formally communicated that they are infact small variations with the same overall end result.

As a compromise , however, what if the expressions were restricted to reading 'self' (or slightly more versatile: previous parameters)

1 Like

I think there's a proposal to add strict defaults per-field, which may be relevant

1 Like

Sounds interesting! Would you happen to have a link to the RFC?