nitlang / nit

Nit language
http://nitlanguage.org
Apache License 2.0
242 stars 67 forks source link

What syntax for multiple returns? #839

Open privat opened 10 years ago

privat commented 10 years ago

For the definition site, things seems straightforward.

fun div(a, b: Int): Int, Int do
   return (a/b, a%b)
end

The only question is about parentheses. In the current grammar, parentheses are mandatory in the return. But do we still force them to be mandatory in the signature foo(a,b:Int): (Int, Int) ?

For the client site, things become quite complex.

Do we force parenthesis in left-values?

(q, r) = div(7, 3) # or
q, r = div(7, 3)

Do we allow any kind of left-values? Especially when each left value in a multiple-return can have a distinct nature.

var q, var r = div(7,3)
var q
#...
q, var r = div(7,3)
x.foo.bar, y.baz["hello"] = div(7,3)
egagnon commented 10 years ago

For me: I would say to put parentheses everywhere (signature, return, call site).

Why? Because my eyes need parentheses at the call site to clearly parse heterogeneous constructs. It is for syntactic consistency that I would also put parentheses in the signature and return.

q, var r = div(7,3)  # My eyes hurt. My brain tells me: what's that "q" doing there?

(q, var r) = div(7,3)  # My eyes still hurt, but a little less. :)
# My brain tells me: why would this "r" declaration
# be valid outside the parenthesized scope?
# Yet, it's relatively normal for a declaration to span outside parentheses.
# Think of function parameter declarations that span to the function body.
lbajolet commented 10 years ago

As we discussed earlier with @privat, declaring and using vars in a multiple return seems a bit confusing for me.

e.g.

(var x, y) = div(5,2)

Were x and y declared here ? Is x the only one introduced and y was declared before ? Or are both declared here like what we'd have in Java or C ?

I'd say we go towards a 2-step approach; first declare the vars, then use them in the multiple return.

fun div(x,y: Int): (Int, Int) do return (x/y, x%y)

var x
var y
x, y = div(5, 2)

Bonus: Parentheses become nearly useless, but @egagnon seems to like them and I don't personally find them to be much of a burden in this case, even if it's not a very Nit-ey style of writing code.

Another solution is to inspire from Go, and have the variables used in a multiple return implicitely defined if not already present, though this might be confusing sometimes when modifying code in an already-existing method.

Go :

func SumProductDiff(i, j int) (int, int, int) {
    return i+j, i*j, i-j
}

func main() { 
    sum, prod, diff := SumProductDiff(3,4)
}

Nit :

fun div(x,y: Int): (Int, Int) do return (x/y, x%y)

x, y = div(5, 2)

As for the more complex expressions as receivers of the multiple return, why not, but considering an AVarDeclExpr is a complex expression, It'd be hard to justify the decision of refusing them. I think that the use of temp variables to store the results and assign later is not that bad.

Besides if the variable is used later as a parameter to a method, the problem stays the same and a temporary variable will be used anyway.

Finally, for the Are the parenthesis mandatory for the return type of a method ?, I'd say no too, this is quite readable even without them.

fun div(x, y: Int): Int, Int do return (x/y, x%y)

x, y = div(5, 2)

This seems readable enough for me. It seems though that most functional-inspired languages I know of (Go, Haskell, Lisp...) have decided that they're mandatory.

Note: If the grammar evolves even more, I'd vouch for the disappearance of parenthesis in the return !

fun div(x, y: Int): Int, Int do return x/y, x%y

x, y = div(5, 2)

Bye-bye parenthesis !

egagnon commented 10 years ago

My eyes like the idea of not allowing for variable declarations on multi-return function call sites:

var x, y
x, y = div(5, 2)

I think that allowing for automatic variable declaration without the var keyword would be a mistake. The idea of requiring the var is not for parsing reasons, it is for catching programming errors.

One thing I do worry about is consistency, as it renders a language more intuitive. Apparently, Nit already supports the following syntax:

for key, value in map do
  ...
end

This does encode a non-parenthesized multi-assignation. It would then be inconsistent to require parentheses elsewhere. So, getting rid of multi-return variable declarations would solve at least two problems: inconsistent requirement of parentheses, and ambiguity of var x, y = div(5, 2).

So, my vote is for "no parentheses" anywhere (including function declaration, return, and call site), and for "no variable declaration" on multi-return call sites.

Note that I would still allow for variable declarations on single-return call sites to multiple-return functions (again, for consistency):

var x = div(5, 2)
Morriar commented 10 years ago

Another solution is to use parenthesis to remove the ambiguity of variable declaration:

var x, y = div(5, 2) # declare x and y
(var x), y = div(5, 2) # declare x, but reuse existing y
x, var y = div(5, 2) # reuse x, declare y

I don't like the idea to forbid variable declaration with multiple return:

1) For the reason explained by Etienne on single-return declaration 2) Most language with multiple-return allow it 3) It takes to much lines for simple things:

var x
var y
x, y = obj.coords

In this case, programmers will use single returns because more elegant:

var x = obj.coords.x
var y = obj.coords.y

While it can be very neat with multiple returns and declarations:

var x, y = obj.coords
egagnon commented 10 years ago

@Morriar suggested (var x), y = div(5, 2): I like that.

lbajolet commented 10 years ago

I'm not that fond of the disambuiguisation via parenthesis, It's too cumbersome to use and a bit error-prone, it feels like the return of yet another structure like the writable that no one was able to use first try.

Yes, the two-step approach is indeed too long for such a simple operation, but it has the merit of being easy to read.

As for the languages that support a similar structure for multiple-returns, for what I know, they're either:

If someone knows of another language that has a solution to a similar problem, I'd be glad to hear it !

I kind of like the Go approach, but as @egagnon signaled, yes, the implicit declaration part is a bit dangerous.

Another possibility is that no reassignation is made when using multiple-returns.

Something like :

# x and y are declared here and can be reassigned later
# in anything other than a multiple-return.
var x, y = div(5, 2)

Though this would lack consistency, which I happen to like as well.

egagnon commented 10 years ago

Why do you guys write:

var x
var y
x, y = obj.coords

Why not

var x, y
x, y = obj.coords

instead?

lbajolet commented 10 years ago

From what I know, as of today we can't chain declarations of variables on a single line, that has to be done in two statements.

But I might have missed a thing or two, anyone can confirm ?

egagnon commented 10 years ago

OK. So, how about a trade-off that should cover 90+% of use cases?

var x, y = obj.coords  # both x and y are being declared
----
x, var y = obj.coords  # rejected: syntax error

Now for the 10-% use cases:

# x already declared
var y
x, y = obj.coords
----
# y already declared
var x
x, y = obj.coords

That would get rid of all parentheses, allow for simple syntax for common cases, and wouldn't overly complicate the syntax for mixed uses.

What do you think?

lbajolet commented 10 years ago

This looks like a good trade-off for my part !

jpages commented 10 years ago

I like the last proposal from @egagnon

nit does not have a syntax with many parentheses, so I like the idea to not introduce a new construction with that.

Plus, this is really simple

privat commented 9 years ago

I did not commented on this (old) issue but +1 for the latest proposal of @egagnon

Tagachi commented 9 years ago

I like the proposal of @egagnon But, as the language swift, and in the way of OO of Nit, i see the functions with multiple return values like an object. ie, in the signature of our function, we could name our return values like:

fun div(a, b: Int): (div: Int, mod: Int) do return a/b, a%b

In fact, we could be stored both variables in a unique variable as:

var division = div(7,3)

and get return values like an object:

print "Division of 7 by 3: {division.div} modulus: {division.mod}"
Morriar commented 9 years ago

@Tagachi seems like you will create a lot of disposable instance of your kind of tuple?

privat commented 9 years ago

I do not really like anonymous tuples with named fields in statically typed languages as they are hard to expose, reuse, document or extends, that is somewhat contradictory with statically typed OO that encourages these things.

Such a tuple is analogous to a class, except that a class has a name, can have super-classes and can have methods

So let just try to expose the class instead!

class DivResult
   var div: Int
   var mod: Int
end

Then the div method can happily use it

fun div(a,b: Int): DivResult do return new DivResult(a/b, a%b)

Note that for the client, this change nothing except that things are more OO-clean.

var division = div(7,3)
print "Division of 7 by 3: {division.div} modulus: {division.mod}"

So one first issue is: is the addition of tuples useful? I do not think that in this case anonymous types really worth it as standard classes seem to solve the thing.

As said @Morriar one other issue with the approach is the mandatory allocation.

The last issue I see is mot philosophical: is this really useful with a real meaning?

I thing that the point about multiple return is when we have to return things that are not cohesive or not meaningful per se. Here, I do not see in which other cases the tuple (or the class DivResult) could be reused, even in the client code having a single variable to hold both the quotient and the remainder seems more an artifact of the programming language than a real need of the programmer or a real meaningful abstraction for the designer.

As an analogy, the same could be done with parameters (and is in fact done in some functional languages and some dynamically typed OO languages that does have a different philosophy than Nit).

fun div(data: (dividend, divisor: Int)): Int do return data.dividend/data.divisor
var data = (7,3)
var res = div(data)

Here, again, I do not this that the tuple used as an argument is that useful for the programmer. It may be useful for the language designer so that all function only need now to accept a single argument and that everything about multiple arguments is solved (delegated) trough tuples. Yes, this also allows the user to do some nice tuple shenanigans in a programmatic way, especially it also simplify some issue when meta-programming (or functional-programming). But again, does the additional complexity worth it?

lbajolet commented 8 years ago

Ping ? More and more I'd like to use this kind of structures, mainly for performance reasons.

The last example coming to mind is from the Codec class, and particularly its UTF-8 counterpart in my current refactor of the I/O library:

class Writer
    var codec: Codec = utf8_codec is writable
    fun write(s: Text) do
        var b = codec.code_string(s)
        write_bytes(b. items, b.length)
    end
end

class UTF8Codec
    fun code_string(s: Text): Bytes do
        var b = new Bytes.with_capacity(s.bytelen)
        b.append_text(s)
        return b
    end
end

One easy optimization the could be done here would be to directly return a NativeString instance from the passed Text if it is a FlatString along with its bytelen, avoiding the creation of ephemeral instances here (which could add up badly if a lot of strings are written like in the compiler, where these manipulations are done 1.4M times).

So considering @egagnon's proposal was accepted as a good one, can we expect such a feature to be implemented in a (near?) future @privat ?

lbajolet commented 8 years ago

Ping ? This would be a fun addition to the language :)

MehdiAit commented 8 years ago

I don't know if it's to late but personally I prefer this syntax :

x, y = div(5, 2)

It seem very simple and intuitive to use! And I think this write is closer to the script syntax language.