golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.06k stars 17.55k forks source link

proposal: Go 2: provide default values for function parameters when caller uses "_" #28197

Closed biter777 closed 5 years ago

biter777 commented 5 years ago

Suggestion for Go v2.x - Default values in functions (not like in C++).

Like so...

func Example(value1 int, value2 string = "default", value3 float64 = 3.14) { ... }

Function calls...

1) Example(1, "string", 1.11) value1 = 1 value2 = "string" value3 = 1.11

2) Example(1, _, 1.11) value1 = 1 value2 = "default" value3 = 1.11

3) Example(1, "string", _) value1 = 1 value2 = "string" value3 = 3.14

4) Example(1, , ) value1 = 1 value2 = "default" value3 = 3.14

5) Example(, , _) Compile error (value1 must be set).

I think this is quite the "Go-way". Thx for attention.

mvdan commented 5 years ago

This has already been rejected in the past - see #21909, for example.

biter777 commented 5 years ago

Thnx, but it is not the same! In #21909 - "not Go-way", because are allowed skipping values when calling function. Skipping values must be indicated explicitly.

meirf commented 5 years ago

robpike talks about default arguments https://github.com/golang/go/issues/21909#issuecomment-330443402 and references this:

One feature missing from Go is that it does not support default function arguments. This was a deliberate simplification. Experience tells us that defaulted arguments make it too easy to patch over API design flaws by adding more arguments, resulting in too many arguments with interactions that are difficult to disentangle or even understand. The lack of default arguments requires more functions or methods to be defined, as one function cannot hold the entire interface, but that leads to a clearer API that is easier to understand. Those functions all need separate names, too, which makes it clear which combinations exist, as well as encouraging more thought about naming, a critical aspect of clarity and readability.

Seems like using explicit skips doesn't quite get around this underlying point that robpike is making.

Separately, imo, one of the big benefits of default values in functions is that it makes the calling code more readable to humans. The reader sees fewer arguments and has less information to think about. In your proposal, the reader will see _ and will be distracted and want to know what defaults are being used instead of just trusting the library. So it looks like this has all the costs of pythonic defaults (see quote above), but loses a big benefit.

biter777 commented 5 years ago

I have already read that robpike stands primarily for a clear code definition, simplicity and understanding of the code. In my opinion, the omission of arguments in the form of "_" fits into this concept.

I do not want to argue and firmly defend this idea, this is just a discussion. There is a request from the community for such functionality. It seems to me sooner or later it will take place.

randall77 commented 5 years ago
func Example(value1 int, value2 string = "default", value3 float64 = 3.14) {
...
}
var f = Example

What is the type of f? Can you call it with _ as one of the arguments?

It seems to me that default arguments just don't mesh very well with Go's type system.

bronze1man commented 5 years ago

I do not like this proposal, because the result code is not easy to read.

If you have too many parameters of a function, struct should be a better option than passing a lot of parameters one by one when you consider the read of the code.

Better proposal https://github.com/golang/go/issues/12854:

So I like the untyped composite literals proposal https://github.com/golang/go/issues/12854 which can save some sytax from the caller.

biter777 commented 5 years ago

I do not like this proposal, because the result code is not easy to read. If you have too many parameters of a function, struct should be a better option than passing a lot of parameters one by one when you consider the read of the code. Better proposal #12854...

An example of bronze1man has the right to exist. But if you bring the example closer to a more practical application, it will look something like this ...

type ExampleReq struct{
   value1 int
   value2 string
   value3 float64
}
func Example(req *ExampleReq) {
   if req == nil {
      return
   }
   if req.value2 == "" {
     req.value2 = "default"
   }
   if req.value3 == 0 {
     req.value3 = 3.14
   }
...
}

Total: 15 lines of code instead of just 1 line.

Also, with this approach, which now we has to be applied in one form or another, the behavior of the less function arguments is transparent and more difficult to understand than the definition of default values in the function declaration. This is my opinion, of course.

And someone tell me what to do if I want req.value3 to remain equal to 0???

bronze1man commented 5 years ago

@Biter777 I admit this req struct way to improvement code readability is not so elegant or tidy. But I tried serval ways, and this is the best way to fit in current golang sytanx. Use pointer of struct in the function request will make the code more difficult to read and an alloc per function call. I have tried use it, but now i use struct of request everywhere.

biter777 commented 5 years ago

@bronze1man ok... And if i want to use req.value2 = "" I need to add a "value2Zero bool" (or better value2Default) in stuct?! Okey, 15 lines vs 1 line we get.

1) Do not forget that if there are many such functions, then we get a bunch of structures used once. 2) In addition, if I want to see which values are used by default, I must look at the function body (instead of studying only the function declaration). 3) In addition, each developer can do this in his own way. Code immersion required. This is clearly not the "go-way".

This is all not good. Or just one me it seems so?

bronze1man commented 5 years ago

@Biter777 Yes ,this function req struct way is not good. But I think function req struct way has better code readability than Example(1, _, _) , if you have 10 parameters to pass in a function, function req struct way should be better code readability. Example(1, _, _) may be better when number the function parameters is less then 3.

As an example, please see the windows api CreateProcessW (c syntax) https://stackoverflow.com/a/42544/1586797 https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-createprocessw .

If the golang syntax can be changed, I think something like following may be better than both ways:

func Example(value1 int,value2 string,value3 float64,value3Zero bool) {
   if req.value2 == "" {
     req.value2 = "default"
   }
   if req.value3 == 0 &&req.value3Zero ==false {
     req.value3 = 3.14
   }
...
}

func main(){
   Example(value1: 0);
   Example(value2: "string");
   Example(value3: 0, value3Zero: true);
}
beoran commented 5 years ago

In stead of having default parameters, just make new wrapper functions that fill in the defaults in various reasonable ways. This is the Go way.

Just look at the os package. Unlike C where you have a single fopen(), in Go we have func Create(name string) (*File, error), func Open(name string) (*File, error) and func OpenFile(name string, flag int, perm FileMode) (*File, error). The former two are wrapper functions around the latter where perm and flag are filled in with reasonable defaults. The resulting API is far easier to use.

biter777 commented 5 years ago

@beoran, yes is a way out of situation. But for our Example(...) func we must write a 4 wrappers instead of 1. And if value1 also takes on the default value, then already 8 wrappers. And the 3 parameters in the function is not a difficult case, but already have up to 8 wrappers.

And the amount here is even secondary. Each developer will give different names to the additional wrappers, i.e. without a single standard. The resulting API is more difficult to use.

beoran commented 5 years ago

I see what you mean, but this is a synthetic example, and unlikely too appear in real code.

Do you have any examples from a large Go code base where such a proliferation of wrappers is an actual problem? Or other examples from real in production Go code where default arguments would have helped?

biter777 commented 5 years ago

@beoran, I did not specifically look for it, but for example, I encountered such an option. github.com/coreos/etcd/pkg/transport

func LimitListener(l net.Listener, n int) net.Listener func NewKeepAliveListener(l net.Listener, scheme string, tlscfg tls.Config) (net.Listener, error) func NewListener(addr, scheme string, tlsinfo TLSInfo) (net.Listener, err error) func NewTLSListener(l net.Listener, tlsinfo TLSInfo) (net.Listener, error) func NewTimeoutListener(addr string, scheme string, tlsinfo TLSInfo, rdtimeoutd, wtimeoutd time.Duration) (net.Listener, error) func NewUnixListener(addr string) (net.Listener, error)

beoran commented 5 years ago

Sure there are many overloads, but I think that doesn't look like a problem at all. Rather it looks quite clear, because I can see from each function name what it does.

We could do something like listen1 := transport.NewTimeoutListener("10.20.30.40", "ipv4", tlsInfo, 10, 20) listen2 := transport.LimitListener(listen1, 16)

I can see immediately that in listen2 I have a time out listener that is limited. I don't have to go read the documentation.

With default arguments we get:

func NewListenerEx(addr String, scheme String = "ipv6", tlsinfo * TLSInfo = nil, rdtimeout time.Duration = 0, wtimeout time.Duration = 0, limit int = 0)

Then when calling the functions: listen1 := transport.NewListenerEx("10.20.30.40", _, _, _, _, _) listen2 := transport.NewListenerEx("10.20.30.40", "ipv4", tlsInfo, 10, 20, 16)

It's not immediately clear that in listen2 I have a limited timeout listener in listen2. I have to read the documentation of the function to find out what is going on. So I would say this example is actually an example of how default values make the API less easy to understand.

biter777 commented 5 years ago

@beoran, the fact that the names of the functions provide some clarity is understandable and reasonable. For this and there are names of functions.

I think in this case it might look like this ...

Instead of this:

addr := append(make([]string, 0, 1), "127.0.0.1")
tslinfo, err := transport.SelfCert(zap.L(), "\", addr)
if err != nil {
...
}
listen1, err := transport.NewTimeoutListener("10.20.30.40", "ipv4", tslinfo, 10, 20)

In the following way:

listen1, err := transport.NewTimeoutListener("10.20.30.40", _, _, 10, 20)
biter777 commented 5 years ago

@beoran, you suggested an idea. We are having a discussion, right? It is possible to allow the use of default values only for "internal" functions that are in small letters.

Those, would look something like this ...

func NewListener(addr, scheme string, tlsinfo *TLSInfo) (net.Listener, error) {
    return newListenerEx(addr, scheme, tlsinfo, _, _, _)
}

func NewLimitListener(n int) (net.Listener, error) {
    return newListenerEx(_, _, _, _, n, n)
}

func newListenerEx(addr String, scheme String = "ipv6", tlsinfo * TLSInfo = nil, rdtimeout time.Duration = 0, wtimeout time.Duration = 0, limit int = 0) (net.Listener, error) {
...
}
beoran commented 5 years ago

Well, limiting defaults to private functions and methods would certainly solve the API problem. But then again it would add some inconsistency, and it doesn't solve the problem of having to find out what the defaults might be when you read the code.

As for the examples compare listen1, err := transport.NewTimeoutListener("10.20.30.40", _, _, 10, 20) with listen1, err := transport.NewTimeoutListenerIpv4NoTls("10.20.30.40", 10, 20)

I think the latter is again easier to read and understand without having to go back to the definition of the function, to see what default will be used.

hooluupog commented 5 years ago

Or once a function has default values for parameters, it must use named prameters.
listen1, err := transport.NewTimeoutListener("10.20.30.40", _, _, 10, 20) vs listen1, err := transport.NewTimeoutListener(addr : "10.20.30.40", rdtimeoutd : 10, wtimeoutd : 20)

beoran commented 5 years ago

Yes, but now I don't even see that default parameters were used.

listen1, err := transport.NewTimeoutListener(addr : "10.20.30.40", scheme: _, tlsinfo: _, rdtimeoutd : 10, wtimeoutd : 20)

Above would solve that, but that is much longer than just listen1, err := transport.NewTimeoutListenerIpv4NoTls("10.20.30.40", 10, 20)

bronze1man commented 5 years ago

@beoran I think that add NewTimeoutListenerIpv4NoTls can solve the default parameter problem. But for a function with 7 parameters, there are too many way to use default values, So there will be a lot of function. The cost of change that root functions will increase a lot. So I think function req struct way should be better than write a lot of function to do one thing like NewTimeoutListenerIpv4NoTls or NewTimeoutListener.

beoran commented 5 years ago

Yes, then you get something like this: listen1, err := transport.NewListener(transport.Settings{Hostname: "10.20.30.40"}) which is also quite good.

biter777 commented 5 years ago

listen1, err := transport.NewListener(transport.Settings{Hostname: "10.20.30.40"})

@beoran, ok, how in this construction values by default are transferred? Is there an example for our "Example" functions?

ianlancetaylor commented 5 years ago

We aren't going to do this. Default argument values were deliberately omitted from the original language design.

This proposal presumably permits constant expressions as defaults, but many function arguments are types that have no constant values. If we allow non-constant values as defaults, then it becomes unclear when the value is computed--at the point of the function definition or the function call? This could be solved but it adds significant complexity to the reader. It makes it harder to understand what the code actually does. The benefit is not worth the cost.