Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: Go 2: add mandatory constructors for named type #28939

Closed
robzhang opened this issue Nov 25, 2018 · 9 comments
Closed

proposal: Go 2: add mandatory constructors for named type #28939

robzhang opened this issue Nov 25, 2018 · 9 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@robzhang
Copy link

Problem

Say we are designing a fraction type:

type Frac struct {
	n int   //numerator
	d int   //denominator, mustn't be 0
}

According to the math definition, Frac.d must't be zero, so we need implement a New function in the package to serve as its constructor:

func New(n, d int) frac {
	if d == 0 {
		panic("denominator can not be zero")
	}
	return frac{n, d}
}

but the New function can't avoid misusing, such as

var f frac

or

f := frac{x, 0}

which are illegal fractions.

In short, the xxx.New() functions is just advisory constructors, but sometimes we need mandatory
constructors.

Proposal

This is a backward-compatible proposal to introduce new init functions to named types as its constructors. When initiate new instance of some type, if the type has no init function, the go compiler initiate it with its default ZERO value, but if the type has any explicit init function, the compiler must call its matched constructor to initiate the instance.

Syntax example:

func (f *Frac)init(n int) {
        f.n = n
        f.d = 1
}

func (f *Frac)init(n, d int) {
        if d == 0 {
                ...
        }

        f.n = n
        f.d = d
}

Now user code:
var f frac
will not compile, because there has no matched constructor, and
f := frac{x, 0}
will call the func (f *Frac)init(n, d int) constructor.

@gopherbot gopherbot added this to the Proposal milestone Nov 25, 2018
@martisch
Copy link
Contributor

martisch commented Nov 25, 2018

From what i understood I do not think it is a backwards compatible change for types that already have an init function defined before the new language change as code (e.g. var f frac) that does not have a matching constructor after the change to create an instance of the type will not compile anymore.

Go is usually defined in a way to avoid hiding expensive operations behind simple constructs e.g. for f:=frac{x, 0} it is not obvious that an init function is called that might do all kinds of things up to never returning.

Its also allows to break simple language constructs in the distance by adding code. By adding an init function with arguments all var declarations maybe invalid for that type.

Note that go does not currently allow to define a method on the type with same name and different number of parameters so that would be another language change unless it would be a special exception for init only.

There are other open questions for the proposal. If a value e.g. for map access for which a key is not present needs to be created or a value for a bare return would this also call a matching constructor or use the zero value or would it fail to compile as there may be no constructor with no arguments?

In general I think we cant avoid misusing a type as the programmer can always use unsafe and other workarounds to not construct a type through the init function if they otherwise avoid the documented API contract for use of the type.

The general Idea of constructors and destructors has been previously discussed in proposal #21737 which was rejected.

@ianlancetaylor ianlancetaylor changed the title proposal: add mandatory constructors for named type proposal: Go 2: add mandatory constructors for named type Nov 25, 2018
@ianlancetaylor ianlancetaylor added LanguageChange v2 A language change or incompatible library change labels Nov 25, 2018
@ianlancetaylor
Copy link
Contributor

Related to #21737.

@deanveloper
Copy link

Go is designed to be verbose so that you know exactly what happens. Having var f frac execute arbitrary code is not intuitive to me

@AndrewWPhillips
Copy link

AndrewWPhillips commented Nov 26, 2018

I think it's great that you are trying to ensure the internal consistency of instances of your Frac struct. In Go we try to make default (zero initialised) instances be valid but this is not always possible.

However there is a mechanism to do what you want already in Go, assuming Frac is in its own package, which it probably should be (even if just an internal package).

You simply need to make the type unexported from the package. Then have the exported "constructor" return an instance of the type. For example:

  package frac

  type frac struct { n, d int }

  func New(n, d int) frac { ...

Then from another package:

  f := frac.New(1, 2)
  
  f := frac.frac{}  // illegal

@DeedleFake
Copy link

This is a backward-compatible proposal to introduce new init functions to named types as its constructors. When initiate new instance of some type, if the type has no init function, the go compiler initiate it with its default ZERO value, but if the type has any explicit init function, the compiler must call its matched constructor to initiate the instance.

This seems like overkill for this specific use-case. What you're asking for has the same problems as operator overloading. Wouldn't it be easier to just allow the user to specify alternative defaults for a type? For example,

type Frac struct {
  n int
  d int = 1
}

Now the 'zero' value for Frac.d is 1 instead of 0.

I'm not really arguing in favor of this, for the record; things like this have been proposed and shot-down before, if I remember right. I'm just trying to provide a simpler alternative that doesn't involve hidden functions getting run without the user explicitly asking for it.

@bradfitz
Copy link
Contributor

bradfitz commented Dec 11, 2018

When this has mattered for me, I end up doing something like:

type T struct {
    x, y                          int
    userProperlyCalledConstructor bool
}

func NewT(x, y int) *T { return &T{x, y, true} }

func (t *T) M() {
   if !t.userProperlyCalledConstructor {
      panic("you didn't use the NewT constructor")
   }
   // ....
}

I was also amused by @AndrewWPhillips's suggestion which is usually just a sign of bad API design. I never considered that pattern might have a valid use!

@ianlancetaylor
Copy link
Contributor

We appreciate the problem that this issue is trying to solve. It might be nice to have better ways to ensure that a type can only be set to valid values. But this particular approach of requiring a constructor to run implicitly does not seem to fit the spirit of the language. It's also not clear how to handle composite literals with field keys, as in Frac{d:0}. And it's not clear how to handle composite literals of more complex types, in which some fields of the outer struct are themselves structs, etc.

@networkimprov
Copy link

Has there been consideration of defined-type default initializers?
If not would that merit a separate proposal?

type T struct {
   i int
   s []byte
}
init T{i: 42, s: make([]byte, 21)} // allow constants and make(T, constant)

type I int
init I(1)
default I(1) // alternative keyword

func f() {
   var t T // t.i == 42 && len(t.s) == 21
}

@ianlancetaylor
Copy link
Contributor

@networkimprov Default initializers as you describe them address a slightly different problem, in that they are restricted in the values they can set. I can't recall the idea having been proposed before. It's not clear to me that it's worth the cost.

@golang golang locked and limited conversation to collaborators Dec 12, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

9 participants