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: extended type inference for make and new #34515

Open
DeedleFake opened this issue Sep 25, 2019 · 22 comments
Open

proposal: Go 2: extended type inference for make and new #34515

DeedleFake opened this issue Sep 25, 2019 · 22 comments

Comments

@DeedleFake
Copy link

@DeedleFake DeedleFake commented Sep 25, 2019

Rationale

Currently in Go, type inference works in one direction and one direction only: From the values of an assignment to the variable declarations. In other words, given some expression with a statically-determinable type, which is basically every expression, the type of a declaration can be omitted. This allows for a lot of the perceived lightness of Go over many statically typed languages.

However, there are a number of situations where this is not possible, most of which have to do with make() and new(), both of which are unique, not including some stuff in the unsafe package, in that they take a type name as an argument. Normally this is a non-issue, as that type can be used to determine the return of the expression, thus allowing for inference in the normal manner:

m := make(map[string]interface{})
s := make([]string, 0, 10)
p := new(int)

Sometimes the variable must have a separately declared type, though, such as in the case of struct fields:

type Example struct {
  m map[string]interface{}
}

func NewExample() *Example {
  return &Example{
    m: make(map[string]interface{}),
  }
}

This leads to unwanted repetition of the type name, making later alteration more awkward. In particular, I thought of this while reading through one of the many proposals about ways to make anonymous structs more useful with channels, and I realized that the following pattern could get very annoying, exacerbating the existing issue:

type Example struct {
  c chan struct {
    v string
    r chan<- int
  }
}

func NewExample() *Example {
  return &Example{
    c: make(chan struct {
      v string
      r chan<- int
    },
  }
}

Proposal

I propose the introduction of a new keyword, say typeof, which takes a variable, function name, or a struct field identifier and essentially acts as a stand-in for a standard type name, possibly with the restriction of only being available as the argument to a make() or new() call. For example,

return &Example{
  m: make(typeof Example.m),
}

This would allow a type name to be pegged to an already declared variable type elsewhere.

Alternative

Alternatively, make() and new() could allow for the aforementioned types of identifiers directly, such as

return &Example{
  m: make(Example.m),
}

This has the advantage of being backwards compatible, but is potentially less flexible if one wants to extend the functionality elsewhere later, such as to generics.

@gopherbot gopherbot added this to the Proposal milestone Sep 25, 2019
@gopherbot gopherbot added the Proposal label Sep 25, 2019
@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Sep 25, 2019

If we are going to introduce typeof, I can't think of any reason why we would restrict it to being used with make and new. That would not be very orthogonal.

@dmkra

This comment has been minimized.

Copy link

@dmkra dmkra commented Sep 25, 2019

Type of m in Example is already known. So typeof or Example.m can be omitted:
return &Example { m: make() }

@DeedleFake

This comment has been minimized.

Copy link
Author

@DeedleFake DeedleFake commented Sep 25, 2019

That's backwards compatible, too, that way, although it brings with it an implicit assumption that typename.fieldname has a general meaning.

Edit: The comment this was a response to seems to have been deleted. Not entirely sure what happened there. It had a suggestion to use Example.m.(type) instead of typeof Example.m.

@bradfitz

This comment has been minimized.

Copy link
Member

@bradfitz bradfitz commented Oct 22, 2019

Type of m in Example is already known. So typeof or Example.m can be omitted:
return &Example { m: make() }

That's what I want myself. @griesemer points out that you'd also want to do new() then.

@griesemer

This comment has been minimized.

Copy link
Contributor

@griesemer griesemer commented Oct 22, 2019

Regarding typeof: Instead of a new keyword, typeof could easily be a built-in. The possible "danger" of a typeof operator is that people might start using it all over the place, for all kinds of declarations, thereby reducing the readability of the code.

There's clearly a strong sentiment for being able to use make() or new() and have both of them infer the necessary type. Though we don't have anything else in the language that behaves like this at the moment. Is there a better syntax?

@bcmills

This comment has been minimized.

Copy link
Member

@bcmills bcmills commented Oct 23, 2019

Building on @dmkra's observation, we could use _ as a stand-in for “infer the obvious type”.

type Example struct {
  m map[string]interface{}
}

func NewExample() *Example {
  return &Example{ m: make(_) }
}
type Example struct {
  c chan struct {
    v string
    r chan<- int
  }
}

func NewExample() *Example {
  return &Example{ c: make(_, 1) }
}
@bcmills

This comment has been minimized.

Copy link
Member

@bcmills bcmills commented Oct 23, 2019

That said, I think #21496 is a better fit for the map example:

return &Example{ m: {} }

And I suspect that #28366 would address most of the realistic use-cases for the chan example:

type Example struct {
  c chan(1) struct {
    v string
    r chan<- int
  }
}

func NewExample() *Example {
  return &Example{}
}
@bcmills

This comment has been minimized.

Copy link
Member

@bcmills bcmills commented Oct 23, 2019

@DeedleFake, there is some precedent for .(type) in earlier proposals too.

(For example, I used (type) as the “type, not value” indicator in https://github.com/golang/proposal/blob/master/design/15292/2016-09-compile-time-functions.md.)

@DeedleFake

This comment has been minimized.

Copy link
Author

@DeedleFake DeedleFake commented Oct 23, 2019

I like the _ for inference, but it feels a bit like it overlaps with _ as a discard assignment. This would allow it to be used on the right-hand side, technically, though only in the unusual case of make() and new() which take actual type names as arguments.

Maybe _.(type)? Maybe not.

@bradfitz

This comment has been minimized.

Copy link
Member

@bradfitz bradfitz commented Oct 23, 2019

Using .(type) for both dynamic type checks (as today with type switches) and static types would slightly be weird.

@bcmills

This comment has been minimized.

Copy link
Member

@bcmills bcmills commented Oct 23, 2019

@bradfitz, at least there is precedent for that! The cases within a switch statement can already be either dynamic values or static types, depending on whether you're switching on x or x.(type).

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 5, 2019

Using _ looks like we are discarding the value.

Another possibility would be . to refer to the current topic, sort of analogous to text/template.

var m map[int]string
m = make(.)

(Using dot might also work for #33359.)

But in general this would be a new kind of idea in Go that we don't currently have.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 5, 2019

Or we could take one of the ideas from #33359 and use ... here.

    var m  map[int]string
    m = make(...)
@jimmyfrasche

This comment has been minimized.

Copy link
Member

@jimmyfrasche jimmyfrasche commented Nov 5, 2019

Does there need to be anything?

Could the absence of a type argument be enough to signal that it should be inferred?

var ch chan T = make() // same as make(chan T)
var buf []T = make(10) // same as make([]T, 10)
x := new() // compile time error, no type can be inferred
@DeedleFake

This comment has been minimized.

Copy link
Author

@DeedleFake DeedleFake commented Nov 5, 2019

Slightly off-topic, but in the case of new(), I'm in favor of getting rid of the function completely and adding make(*T). I don't really see why there needs to be a separate function. With the above suggestion, it'll only work with a pre-existing pointer type variable anyways, which means that it would essential infer T from a *T variable, unlike make() which would just use the variable's type directly.

It's a small nitpick, though.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 6, 2019

Let's definitely keep the fraught discussion of new vs. make in a different issue.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 6, 2019

There doesn't need to be an argument to make, but personally I think some sort of marker would be better than having it just make something.

@jimmyfrasche

This comment has been minimized.

Copy link
Member

@jimmyfrasche jimmyfrasche commented Nov 6, 2019

It would make it somewhat more similar to the recent contracts draft, modulo the number of parens and commas.

@bcmills

This comment has been minimized.

Copy link
Member

@bcmills bcmills commented Nov 6, 2019

The ? symbol is also currently unused, right? That seems like a reasonable token to indicate “inferred”.

@beoran

This comment has been minimized.

Copy link

@beoran beoran commented Nov 14, 2019

I think typeof()as a built in function has the best potential here, and would be the most backwards compatible solution, and useful in several other cases, such as in low level programming, in conjunction with unsafe.Sizeof, unsafe.Alignof and unsafe.Offsetof.

If there is any risk that typeof() will end up being overused, then this can be solved through go lint checking. Personally I think the risk for abuse is low, since if you do typeof(x), then x will already be in scope, and the type of x should be relatively clear to the reader.

@bradfitz bradfitz added the dotdotdot label Nov 26, 2019
@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 26, 2019

Another idea would be to permit referring to the basic kind of type without filling out the elements. For example,

    var s []byte = make([], 10)
    var m map[int]string = make(map)
    var m2 map[int]string = make(map, 100)
    var c chan bool = make(chan)

This might make the code clearer to the reader, while still permitting the omission of the redundant type information.

@dotaheor

This comment has been minimized.

Copy link

@dotaheor dotaheor commented Nov 27, 2019

The new() and make(length, capacity) form looks consistent with this generic proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
10 participants
You can’t perform that action at this time.