Could rust do this - Kotlin: single expression function with inferred return type,

Would rust consider absorbing this feature seen in Kotlin: a syntactic shortcut for single-expression functions with an inferred return type.
e.g. fun addExpr(first: Int, second: Int) = first + second

I think this syntax is very intuitive: to my mind it is inspired by haskell and I independently implemented it in my rust-inspired pet-language before I knew kotlin did this.

I think it would also streamline writing some simple constructors:-

fn make_foo(a:..,b:...)= Foo{ x: .... , y: .. , z: ... }

.. and it would be nice to drop a nesting level in functions which are just one 'match' ...

    fn foo(a:...) =match a{ ...  
    }

(maybe you could kill 2 birds with '1.5 stones' and have a special case =match{ } that matches on the argument type directly)

I gather some assistance is also requested for complex iterator return signatures.

The appeal is that the body of the function is clearly visible in the same line as the inputs, and in many cases -simple math helpers.. think of things like min/max, clamp , lerp etc- , the function is sometimes simpler than the type signature/bounds.

I do like rusts setup most of the time, but that doesn't mean there aren't other cases where different behaviour isn't useful. This reminds me of a discussion over what features a tool needed..

"we use features a/b/c 90% of the time , features d/e/f 10% of the time"

You might then mistakenly assume d/e/f are extraneous, but without them those 10% of use cases end up taking 10x as long, so their presence actually nearly doubles productivity.

I know there are macros, however those have a very different feel, and it's a big syntax change to move code between macros and functions (as jonathan blow explains in his excellent videos, 'code has a maturation cycle').

I know there are objections to whole program inference, but this doesn't go that far.

Do people find it problematic in Kotlin ?

(i would still prefer this [https://github.com/rust-lang/rfcs/pull/2063])

EDIT: see recent reddit thread [https://www.reddit.com/r/rust/comments/6zy8hl/kotlins_coroutines_and_a_comparison_with_rusts/]

One problem with this is that the return type of the function is API, while the implementation of the function is, well... implementation. Part of the reason functions (as opposed to closures) require a return type is so that changing the body in a way that changes the return type results in an error, and requires the author knowingly change the API-level signature.

Making it implicit in the manner you suggest would, IMO, increase the likelihood of APIs breaking in unexpected ways.

14 Likes

But the main use would be simple functions.
API's return types could also be changed explicitely anyway

Simple functions are usually composed a lot, e.g. you write 'min' and 'max', then you can express 'clamp()= min( ( max ..)..)' etc. Once a function has been defined and used, you aren't just going to go and arbitrarily change what it does.

    fn add_scaled(a,b,f)=a+b*f;
    fn lerp(a,b,f) = add_scaled(a,(b-a),f)
    fn invlerp(a,b,x) = (x-a)/(b-a)
    fn lerp_between(x0,y0,x1,y1,x) = lerp(y0,y1, invlerp(x0,x1, x))

You could say 'only export such functions if they are also used internally' (especially in tests) .. in the above example if I go and change 'what lerp does' , 'lerp_between' breaks. I wouldn't bother , if I wanted to do something else i'd make a new function

the other scenario where you might want to end up with functions like this, extracting a lambda and naming it - you already created a context (function using the original lambda)

You could easily put this feature in but guard against exports requiring a #[], warning about the hazard you mention;

Another issue is that this introduces a relatively strange new syntax, when (if const/static type annotation elision is approved), it might permit this to naturally fall out of existing features:

const FOO = |a, b| a + b;
3 Likes

I'm not a fan of this idea, it means you can no longer understand what something does just by looking at its function signature so you'd need to wade through the entire function's implementation to understand what it's doing.

Also, what makes it so hard to type the -> Foo part of your function signature? If you later on go and change how the function works so it returns a different type, I'd expect compilation to fail and say the return type from the definition and the actual type returned no longer match up. If you have something like C++'s auto return type (like your RFC suggests) then that means it'll break at all of the call sites, spitting out loads of spurious compiler errors for the one issue. I've had enough experience with C++ templates and the massive amounts of errors they can spew forth to value the ability to deduct all types and behaviours purely from a function's signature.

7 Likes

Isn't that akin to what's proposed with the impl Trait RFC ? You have an fn under your eyes but you don't know yet what's the return type.

No; impl Trait not only allows the author of the function to avoid writing the return type, but also prevents callers from interacting with it in any way beyond what that trait permits. (Well, plus auto-traits, but that's a detail that doesn't change the overall effect significantly.)

As a result, a return type whose critical behavior is not entirely captured by a (list of) trait(s) cannot be handled by impl Trait in this use case.

This is also why impl Trait lacks the downsides of this proposal that I have a problem with: Changing the implementation alone cannot break users of the function unless the user also changes the signature.

it means you can no longer understand what something does just by looking at its function signature

The case where this is most applicable is one liners, where behaviour is plainly visible:-

fn lerp(a,b,f)=a+(b-a)*f

there is a threshold below which the operation is clearer than the type-signature.

1 Like

when (if const/static type annotation elision is approved), it might permit this to naturally fall out of existing features7:

const FOO = |a, b| a + b;

Well, if you could declare 'global lambdas' for simple cases like that, and use them like functions, that would satisfy my use case.

const lerp=|a,b,f| a+(b-a)*f; etc etc.

some would say it's the lambda syntax thats is strange but i'm already used to it. I quite like the intention behind what JAI is trying to do (making lambdas and function declaration identical) - this reminds me of that, (although JAI does look odd.)

" If you have something like C++'s auto return type (like your RFC suggests) then that means it’ll break at all of the call sites,"

To me this is like saying "I shouldn't go out today incase I get hit by an asteroid. they did have an asteroid over in russia recently."

You'd also break your module's own tests. Note in my example, I show a chain of one liners that build on each other.

How about constraining it to functions that the module actually uses (especially in tests).

Who would give out a library that doesn't have some sort of sample code... functions that have never been called by person writing them?

if you really needed to change the type, then .. what can you do about it?

You could have synergy between the version control system , compiler, the doc tool , and package manager: the package manager could give you this error , then you can decide to rename the function, or inform your users. The doc tool could actually show the types (this would be useful within function bodies, given the internal inference)

I do not remember details but it was implemented recently in compiler that you can use capture-less closures in place of functions. So if you allow capture-less closures as globals you're done. No new syntax, no problems with public API. I like that.

2 Likes

Does that mean there's no semantic difference between non-capturing, constant closures, and regular functions?

I think that's https://github.com/rust-lang/rfcs/blob/master/text/1558-closure-to-fn-coercion.md, which was released in 1.19 IIRC.

Haskell already does this, though admittedly if you turn on -Wall it will warn you not to. I don't think it's an impossibility. In fact, I suspect changing a return type will cause type errors elsewhere in the library if you do this. Elm even has a feature that forces you to bump the package version according to SemVer if you change the API, which to my mind is even more desirable.

I suspect changing a return type will cause type errors elsewhere in the library if you do this

Exactly

Whist it's more work, we could easily say you can only export an inferred function if it's already used within the library (perhaps within a deliberately illustrative #[test]), i.e. the pattern of it's usage has already been established - at which point it's far less likely to change.

in the examples I usually give there is a chain of contexts built up:-

fn madd(a,b,f) = a+b*f;
fn lerp(a,b,f) = mad(a,b-a,f);
fn inv_lerp(x0,x1,x)=(x-x0)/(x1-x0);
fn lerp_between(x0,y0,x1,y1,x) = 
    lerp(y0,y1, inv_lerp(x0,x1,x)); // creates context for lerp & invlerp

// there would probably be a test showing  
// 'inv_lerp(a,b, lerp(a,b,f) ) == f' when given enough precision

fn clamp(lo, hi, x) = min(hi,max(lo,x));
fn clamp_s(abs_max, x) = clamp(-abs_max,abs_max,x);
fn clamp01(x) = clamp(zero(),one(),x);

fn deadzone(zone,x) = if x>zone {x-zone} else if x<-zone {x+zone} else {zero()};
fn deadzone_compensated(rng,d,x) = deadzone(d,x)*(rng/(rng-d)); 

(such one liners are handy for currying, if we do eventually get that..)
1 Like

This distinction ("single-expression") seems to be an indication that Kotlin isn't expression-oriented. In rust every function is a single expression, so a reasonableness restriction is much harder to express.

A function being called isn't nearly enough to know whether you broke something someone is relying on. For example, if I return the result of maping on a Vec, did I want to expose its ExactSize-ness? Its DoubleEnded-ness?

In rust every function is a single expression,

well i know we could say 'fn foo()={.....}' but that would be stretching the intent, in that instance you're straying from the intended use case.

A function being called isn’t nearly enough to know whether you broke something someone is relying on.

If you think it's going to be hazardous... just choose a different library?
surely people can decide what they need on a case by case basis.

Also how does writing the types out guarantee that they wont be changed?

For example, if I return the result of maping on a Vec, did I want to expose its ExactSize-ness? Its DoubleEnded-ness?

in these scenarios, it would depend on how it's called. you'd call something with 'any sequence', and return the same sequence , with the interior type mapped. The function itself would tell you all you need to know (if it relies on double-ended-ness, it wouldn't have been callable)

( Eventually rust will allow genericity over the type of collection , so these types of helpers will become more applicable IMO)

Kotlin is expression oriented. Although pure block is not an expression, you can totally write fun foo() = run { anything goes here }.

1 Like