Go: proposal: Go 2: implied variadic arguments

Created on 25 Jun 2018  路  8Comments  路  Source: golang/go

Executive Summary

Given the following function definition:

func MyFunction(args []string) {
//...
}

The function should be usable in any of the following ways:

MyFunction([]string{"String1", "String2"}) //the normal way
MyFunction("String1", "String2") //variadic arguments

Variadic arguments are implied. There is no need for explicit variadic argument function definition. In fact, we can get rid of the explicit variadic argument syntax (the ellipsis).

The Problem

API design is one of the challenging issues in programming. Go is one of the few languages that are forgiving enough to handle any oversight in the initial design and allows code to evolve. We shouldn't have to choose between func MyFunction(args []string) and func MyFunction(args ...string).

This may seems trivial, but currently Go treats the two definitions as distinct and different from each other. If you do func MyFunction(args ...string), you need to remember adding ellipses MyFunction(array ...) when you pass an array. On the other hand, if you choose func MyFunction(args []string), you need to wrap the arguments into an array MyFunction([]string{"item1", "item2"}). Go should be smart enough to know we essentially mean the same thing.

Solution

I propose making variadic arguments implicit and get rid of explicit variadic arguments (the ellipsis). So, given the following function definition:

func MyFunction(count int, args []string) {
//...
}

It should be usable in any of the following ways:

MyFunction(10, []string{"item1", "item2"})
MyFunction(10, "item 1", "item 2")

The existing rules of variadic arguments still apply. So for instance, the following would be illegal.

func MyFunction(args []string, count int) {
//...
}

MyFunction("item 1", "item 2", 10) //ILLEGAL

Backward Compatibility

Backward compatibility is a thorny issue, especially when we are making changes to existing features instead of merely adding new ones. This proposal will break backward compatibility because we are changing existing features. While making variadic arguments implicit does not break backward compatibility, the removal of explicit variadic arguments breaks existing code. However, I believe it is also gofix-able.

Importance

There is nothing severe or critical about this proposal. Without this proposal, gophers can still get on with their everyday lives, with perhaps a very minor annoyance. However, what this proposal does is to streamline existing features making them more coherent.

FrozenDueToAge Go2 LanguageChange Proposal

Most helpful comment

This proposal implies that two function calls with identical arguments can match two function signatures with structurally different arguments. That is a recipe for bugs to arise as package interfaces change; in fact, preventing this kind of thing is exactly what function signatures are for.

This is a local convenience but potential global error source, just the kind of thing Go's design has pushed against.

All 8 comments

This proposal implies that two function calls with identical arguments can match two function signatures with structurally different arguments. That is a recipe for bugs to arise as package interfaces change; in fact, preventing this kind of thing is exactly what function signatures are for.

This is a local convenience but potential global error source, just the kind of thing Go's design has pushed against.

This proposal implies that two function calls with identical arguments can match two function signatures with structurally different arguments.

It's impossible situation because Go does not have overloaded functions.
func f(x string, y string){}
func f(x []string){}
====> compile error: f redeclared in this block

Separate from Rob's concern, this seems unavoidably ambiguous. See https://play.golang.org/p/MDLK9scN6Qk. Under the proposal, what happens when a function that takes a slice of empty interfaces is called with a slice of empty interfaces as its argument? The user might want the slice to be unpacked into varargs or not, and there's no way to know.

@dmkra I wasn't clear about this, but I meant over the passage of time, as a program grows, a function's signature might change or a call site might change and the result would still compile but behave incorrectly.

Separate from Rob's concern, this seems unavoidably ambiguous. See https://play.golang.org/p/MDLK9scN6Qk. Under the proposal, what happens when a function that takes a slice of empty interfaces is called with a slice of empty interfaces as its argument? The user might want the slice to be unpacked into varargs or not, and there's no way to know.

I don't think there is any ambiguity between function f1 and f2. Either way, the arguments still have to be unpacked inside both functions. Under this proposal, there will be no more explicit variadic arguments. Hence, your example is going to be like this:

func f1(x []interface{}) int {
    return len(x)
}

func f2(x []interface{}) int {
    return len(x)
}

func main() {
    x := []interface{}{1, 2}
    fmt.Println(f1(x), f2(x))
        fmt.Println(f1(x), f2(1, 2, 3, 4, 5)) //this is also legal
}

Variadic arguments is a non-essential feature. As a proof, there are programming languages that can do just fine without this feature. Variadic arguments is a mere sugar for array arguments. Hence, we should recognize variadic argument as it is: a sugar for array argument, and not treating it as a distinct type.

How do you choose whether func f1(items []interface{}) and func f1(items ...interface{}) is more correct? The fact that you can use either one interchangeably (albeit with a bit more typing - as shown in the example below) shows that there are very little differences between the two of them:

func f1(args []interface{}){ /*...*/ }
func f2(args ...interface{}){ /*...*/ }

var array = []interface{1, 2, 3}
f1(array)
f2(array...)

f1([]interface{}{1, 2, 3})
f2(1, 2, 3)

We shouldn't have to choose between func f1(items []interface{}) and func f1(items ...interface{}). Instead of having to make such trivial decision, I think it is better to have just one correct answer and have the language recognize the sugar as necessary.

func (c *Class) AddStudents(students []Student){ 
   //...
}

class.AddStudents(getStudents()) 
class.AddStudents(   
    NewStudent("Mike"),
    NewStudent("Jake"),
    NewStudent("Mary"),
)

It results in a cleaner syntax.

@dmkra I wasn't clear about this, but I meant over the passage of time, as a program grows, a function's signature might change or a call site might change and the result would still compile but behave incorrectly.

Could you give an example of such situation? I think Go has enough features to prevent such situations and that the presence of explicit variadic arguments isn't going to help.

Consider:

package main

import (
    "fmt"
)

func f1(x ...interface{}) int {
    return len(x)
}

func f2(x []interface{}) int {
    return len(x)
}

func main() {
    x := []interface{}{1, 2}
    fmt.Println(f1(x), f2(x))                                     // results not compatible
    fmt.Println(f1(x...), f2(x))                                  // ellipsis matter
    fmt.Println(f1([]interface{}{x, x}), f2([]interface{}{x, x})) // results not compatible
}

Plaground

Output:

1 2
2 2
1 2

I think this demonstrates sufficiently that the proposal is not keeping the existing functionality of the language, i.e. it's removing a feature instead of, as claimed, only getting rid of unnecessary syntax.

@cznic Thanks for the example. That's actually a great argument against this proposal. I hadn't thought about interface{} and its implication. I agree that there is no point in continuing this proposal and that Go may be better off to remain as it is.

I am closing down this proposal.

Was this page helpful?
0 / 5 - 0 ratings