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: spec: allow struct fields to implement interface methods #48288

Closed
bcmills opened this issue Sep 9, 2021 · 21 comments
Closed

proposal: spec: allow struct fields to implement interface methods #48288

bcmills opened this issue Sep 9, 2021 · 21 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@bcmills
Copy link
Contributor

bcmills commented Sep 9, 2021

This is another attempt to address the use-cases underlying proposals #21670 and #47487, in a more orthogonal way.

I propose that, as of some Go version 1.XX:

  1. If a struct type is defined, or used as a type literal (including as the type in an alias declaration), in a package compiled at language version 1.XX or higher, the fields of function type within that struct are considered to be in the method set of the type.

    • Once such a struct value has been packed into an interface variable, that interface value can be passed to other packages regardless of their language version.

    • Type-assertions on such an interface variable will continue to see the fields of function types within the method set of the type, regardless of the language version of the package in which the type-assertion is made.

  2. If a struct value stored in an interface is type-asserted to a struct type defined (or used as a type literal) in a pre-go1.XX package, the resulting value does not have the fields of function type in its method set, even if those methods were present in the type of the operand.

Example:

-- go1xx/go1xx.go --
package go1xx

type Struct = struct{ M func() }
type Interface = interface { M() }

-- go1xx/example_test.go --
package go1xx

func ExampleTypeAssertion() {
	// ok: both Struct and go117.Struct are aliases of 'struct { M func() }', so they are mutually assignable.
	var s1 Struct = go117.Struct{}
	var s2 go117.Struct = Struct{}

	// ok: Struct (the type of a1) is defined in a package at Go 1.XX, so M is in its method set.
	var i1 Interface = s1
	var i2 go117.Interface = s1
	var i3 Interface = Struct{}

	// ok: this 'struct{ M() }' is a type literal in a Go 1.XX package, so M is in its method set.
	var i4 Interface = struct{ M() }{}

	// ok: go117.Interface and Interface have the same method set, so they are mutually assignable.
	i2 = i1
	i1 = i2

	// ok: the concrete type of i1 is 'struct{ M() }', and go117.Struct is an alias for 'struct{ M() }',
	// so the type-assertion succeeds.
	s2 = i1.(go117.Struct)

	// error: struct field M is not in the method set of go117.Struct (an alias declared in a pre-go1XX package).
	var e1 Interface = go117.Struct{}
	var e2 Interface = s2
	var e3 Interface = i1.(go117.Struct)
	var e4 go117.Interface = go117.Struct{}
}

-- go1xx/go.mod --
module example/go1xx
go 1.XX

-- go117/example.go --
package go117

import (
	"example/go1xx"
)

type Interface = interface{ M() }
type Struct = struct{ M func() }

-- go117/example_test.go --
package go117

func ExampleTypeAssertion() {
	// ok: the alias go1xx.Struct is declared in a package at Go 1.XX, so M is in its method set even in a pre-1.XX package.
	var x Interface = go1xx.Struct{}

	// ok: go1x.Struct is a type alias for 'struct { M func() }', so the type-assertion succeeds.
	y1 := x.(struct{ M func() })

	// error: struct field M is not in the method set of the literal type 'struct { M func() }' in a pre-go1XX package.
	var z1 Interface = y1

	// ok: go1xx.Struct is an alias for type 'struct { M func() }', so value y1 of that type is assignable
	// (even though it has a different method set).
	var y2 go1xx.Struct = y1

	// ok: go1xx.Struct (the type of y2) is an alias declared in a package at Go 1.XX, so M is in its method set.
	var z2 Interface = y2
}

-- go117/go.mod --
module example/go117
go 1.17

Language change template answers

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?

    • Experienced.
  • What other languages do you have experience with?

    • C, C++, Python, Java, Rust, OCaml, Standard ML, JavaScript, Emacs LISP, probably a few others I'm forgetting.
  • Would this change make Go easier or harder to learn, and why?

    • Unclear. It would remove one puzzling question (“if I can invoke x.M(), why is M not a method of x?”), but add a different one (“why does struct { M() } not implement interface{ M() } in this legacy package?”).
    • As packages and users migrate to Go 1.XX, the hard-to-learn part becomes less and less relevant.
  • Has this idea, or one like it, been proposed before?

  • Who does this proposal help, and why?

  • What is the proposed change?

    • Please describe as precisely as possible the change to the language.
    • What would change in the language spec?
    • Please also describe the change informally, as in a class teaching Go.
  • Is this change backward compatible?

    • Yes, with the caveat that most of the complexity of the change results from maintaining compatibility.
  • Show example code before and after the change.

    • See the first section.
  • What is the cost of this proposal? (Every language change has a cost).

    • Compilers and source analyzers would have to track type aliases more carefully. (The method set of an alias depends on its declaring package, not just its literal definition.)
    • In addition to compiler and runtime changes, any tool that analyzes Go source code (such as gopls, go vet, or staticcheck) would need to be updated to recognize struct fields used to satisfy interfaces.
    • What is the compile time cost?
      • Comparable to the cost of compiling the equivalent hand-written methods today.
    • What is the run time cost?
      • Comparable to the run time cost of dispatching the equivalent hand-written methods today.
  • Can you describe a possible implementation?

    • I can, but I believe it is straightforward enough to omit that detail for now.
    • Do you have a prototype? (This is not required.)
      • No.
  • How would the language spec change?

    • The definition of method sets would be amended (precise wording TBD). I don't think any other spec changes would be strictly required.
  • Orthogonality: how does this change interact or overlap with existing features?

    • This change makes “methods” in some sense more orthogonal.
      • Method calls are unified with struct field selectors. The expression x.M() can now be thought of as “a method call” regardless of whether it is also “a field access combined with a function call”.
      • Method expressions are similarly unified with struct field selectors.
      • The reader of a codebase can see a call of the form y := x.M(), where y has type T, and know that x implements interface { M() T } without needing to also know the type definition of x.
  • Is the goal of this change a performance improvement?

    • No.
  • Does this affect error handling?

    • Only marginally. It allows the error interface to be implemented by struct { Error func() string }, which may be useful in a few narrow cases.
  • Is this about generics?

    • Not really. However, since generics are defined in terms of interfaces, it would allow struct types with fields of function types to satisfy some type constraints that they previously did not satisfy. That may allow for more concise generic code in some cases.
@bcmills
Copy link
Contributor Author

bcmills commented Sep 9, 2021

Under this proposal, the example in #47487 (comment) becomes:

func main() {
	var N int64
	cw := struct{ Write func(p []byte) (n int, err error) }{
		Write: func(p []byte) (n int, err error) {
			n, err = os.Stdout.Write(p)
			N += int64(n)
			return n, err
		},
	}
	writeTo(cw)
	fmt.Println(N, "bytes written")
}

@bcmills
Copy link
Contributor Author

bcmills commented Sep 9, 2021

The above could perhaps be made more concise via #12854 and/or #21496.

@bcmills
Copy link
Contributor Author

bcmills commented Sep 9, 2021

Ideally, I think this proposal would be adopted in conjunction with #25860, where the underlying type of an “interface literal” is just the corresponding anonymous struct type.

Then the above example becomes:

func main() {
	var N int64
	cw := io.Writer{
		Write: func(p []byte) (n int, err error) {
			n, err = os.Stdout.Write(p)
			N += int64(n)
			return n, err
		},
	}
	writeTo(cw)
	fmt.Println(N, "bytes written")
}

where the underlying type of cw is struct{ Write func([]byte) (int, error) }.

@Merovius
Copy link
Contributor

Merovius commented Sep 9, 2021

If a struct value stored in an interface is type-asserted to a struct type defined (or used as a type literal) in a pre-go1.XX package, the resulting value does not have the fields of function type in its method set, even if those methods were present in the type of the operand.

I'm not sure how to understand this, especially with the highlighted section. In general, a type literal is not defined in any package - it's not a defined type. It also seems this would imply that

// in example.com/pkga, which uses go1.17
func F() interface{} {
    return struct { Read func([]byte) (int, error) }{nil}
}


// in example.com/pkgb, which uses go1.XX
func F() interface{} {
    return struct { Read func([]byte) (int, error) }{nil}
}

// in example.com/C
func main() {
    a, b := pkga.F(), pkgb.F()
    _, okA := a.(io.Reader)
    _, okB := b.(io.Reader)
    fmt.Println(reflect.TypeOf(a) == reflect.TypeOf(b), okA, okB) // prints true, false, true
}

which seems strange.

@bcmills
Copy link
Contributor Author

bcmills commented Sep 9, 2021

In general, a type literal is not defined in any package - it's not a defined type.

Writing with precise grammar is hard. 😩 Read that sentence as: “a struct type defined in, or a struct type used as a type literal in, a pre-go1.XX package”.

(“defined” and “used as a type literal” are mutually exclusive.)

It also seems this would imply … which seems strange.

Hmm. I think that reflect.TypeOf(a) should not be equal to reflect.TypeOf(b) when a and b have different method sets, even if they have the same textual representation.

That resolves the apparent contradiction:

	fmt.Println(reflect.TypeOf(a) == reflect.TypeOf(b), okA, okB) // prints false, false, true

@Merovius
Copy link
Contributor

Merovius commented Sep 9, 2021

So, you are proposing to change the notion of type identity to specifically mention that method sets must be equal? That seems like kind of a big change (with a bunch of knock-on changes) for a relatively minor language feature.

Generally, I think this proposal would be a language redefinition, as code compiles the same before and after, but its meaning (e.g. what interfaces are implemented by a value) changes between versions. If a modules upgrades from go1.17 to go1.XX, it can't really tell if they are affected by this in unforseen ways. As far as redefinitions go, the dangers of this are probably relatively small, but it still seems worth saying.

@Merovius
Copy link
Contributor

Merovius commented Sep 9, 2021

Writing with precise grammar is hard. 😩

To clarify: I understood what you meant by "defined in a package", I was trying to get at exactly the point that the way non-defined types work is that it doesn't matter what packages they are created in (well… except for unexported identifiers), so tying their behavior to that seems confusing to me.

@bcmills
Copy link
Contributor Author

bcmills commented Sep 9, 2021

So, you are proposing to change the notion of type identity to specifically mention that method sets must be equal?

Hmm. I had though that the reflect package did not actually guarantee reflect.TypeOf to return ==-equal Type values for “identical” types, but reading the docs again I see that such a guarantee is there now. 😞

Generally, I think this proposal would be a language redefinition, as code compiles the same before and after, but its meaning (e.g. what interfaces are implemented by a value) changes between versions.

That's a good point. However, the surface area of the redefinition is very small: the only programs that succeed before but fail after would those that expect a type-assertion of a specific form to fail. I'll have to give the compatibility implications a bit more thought. 🤔

@Merovius
Copy link
Contributor

Merovius commented Sep 9, 2021

One way to side-step these issues (but which makes this feature even more unwieldy to use) would be to only attach the methods to defined types. The example would then be

func main() {
	var N int64
        type writer struct { Write func(p []byte) (int, error) }
	cw := writer{
                Write: func(p []byte) (n int, err error) {
			n, err = os.Stdout.Write(p)
			N += int64(n)
			return n, err
		},
	}
	writeTo(cw)
	fmt.Println(N, "bytes written")
}

This also solves the argument that, traditionally, methods are only attached to defined types. It still save you from having to define a package-scoped type.

Though, to be clear: I already find the originally proposed syntax pretty bulky.

@ianlancetaylor ianlancetaylor added the v2 A language change or incompatible library change label Sep 9, 2021
@beoran
Copy link

beoran commented Sep 10, 2021

This proposal would have great impact on the language for a relative minor feature, also it is difficult to opt out. It would be better if this feature was opt in, for example by adding an "interface" keyword after the struct field.

type WriterWrapper struct { Write func(p []byte) (int, error) interface }

Then is expanded by the compiler to:

type WriterWrapper struct { Write func(p []byte) (int, error) }
func (w WriterWrapper) Write()(int, err) {
return w.Write()
}

@sammy-hughes
Copy link

sammy-hughes commented Sep 10, 2021

I suggest that this is already, at some level, available in normal Go 1.

The interest for this proposal is that an object may be passed across a barrier constrained by an interface. This can be achieved by defining an adapter type which retains a pointer to the object, and which can implement behaviors for that interface as if they were being satisfied by the type itself, with all exported methods and fields available for functionality and mutability as needed.

Yes, it is boilerplate, but no this does not represent an increased capability to the developer, rather a convenience, and a tremendously costly convenience.

In example, a scenario involving a very basic iterator with a simple stateful indexer (which does not itself satisfy the interface. That would be meaningless), a consumer of the interface type, and finally an example of a type which uses an adapter type that implements the interface, by which the example type can use machinery intended for the interface.

The only edgecase would be if there were unexported fields/methods on the parent object which were necessary for the interfaced machinery, but I can think of a half-dozen possible workarounds. EDIT: I just added a demonstration of the most likely useful such workaround.

//Basic interface and an embedable, stateful indexer
type Iterator interface{
	Next()bool
	Do()
}
type IsIterator struct {i,l int}
func (iter *IsIterator) Init(index, length int) *IsIterator {
	iter.i = index
	iter.l = length 
	return iter
}
func (iter *IsIterator) Next() bool {
	if iter.i < iter.l {
		iter.i++
		return true
	}
	return false
}
func (iter *IsIterator) Index() int {
	return iter.i-1
}

//Example consumer of interface Iterator
func Iterate(i Iterator) {
	for i.Next() {
		i.Do()
	}
}

//Example object with an adapter type
type NotIterator struct {
	Names []string
	ids []int
}
type CanIterate struct {
	*NotIterator
	*IsIterator
	DoPlugins []func(int)
}
func (not *NotIterator) ToIter() *CanIterate {
	return &CanIterate{
		not,
		new(IsIterator).Init(0,len(not.Names)),
		nil,
	}
}
func (not *NotIterator) BeNasty() *CanIterate {
	iter := not.ToIter()
	iter.DoPlugins = []func(int){
		func(i int) {
			fmt.Printf("Oh, and their id is %v!\n",not.ids[i])
		},
	}
	return iter
}
func (can *CanIterate) Do() {
	fmt.Printf("%v\n",can.NotIterator.Names[can.IsIterator.Index()])
	if can.DoPlugins != nil {
		for _, do := range can.DoPlugins {
			do(can.Index())
		}
	}
}

func main() {
	example := NotIterator{[]string{"Franz", "Hermes", "Wallace", "Jimmy", "Kev", "Fernando"}, []int{87,22,19,98,10092,48}}
	Iterate(example.ToIter())
	Iterate(example.BeNasty())
}

@Merovius
Copy link
Contributor

@sammy-hughes

The interest for this proposal is that an object may be passed across a barrier constrained by an interface. This can be achieved by defining an adapter type which retains a pointer to the object

No, the interest is specifically not having to write that adapter type. It is already clear that you can write it, but we'd like to avoid having to do that. That's the motivation.

Yes, it is boilerplate, but no this does not represent an increased capability to the developer, rather a convenience, and a tremendously costly convenience.

The intent of the discussion is to try to find a tradeoff which provides the convenience at as low a cost as possible - and then decide if it's worth that cost. If you can provide good, new arguments for either the cost being higher/lower than described so far, or the convenience being smaller/larger, that would be helpful.

@sammy-hughes
Copy link

sammy-hughes commented Oct 1, 2021

@Merovius, I do apologize for the delayed response. For starters, I raised my own kerfuffle with a language proposal, and it's been a busy few weeks both personally and at work. First time I've checked my notifications in nearly a month.

I certainly also apologize for missing that avoiding a package-scoped type definition was a stated part of the goals for this proposal. Failure to read thoroughly. I suggest that my earlier "this can be done" comment be rescoped as a "this is a fairly natural pattern." I also possibly need to restructure it to be more applicable to the conversation, given your clarification.

As to the cost, Go compiled binaries currently store declared types and method tables in a read-only portion of the application, allowing for A. safe concurrency and protection from malicious remotes, with attacks such as are possible in Javascript, Python, and PHP, but B. allows for direct lookups on known types, without indirection or parametric lookups.

Point A could be sacrificed to accomplish this proposal. It's already possible with machinery that uses unsafe. Since Go doesn't have an equivalent to the eval() or exec() functions available in many interpreted languages, the safety concern is only valid if an attacker cares enough to precompile a malicious function for your platform, and you're silly enough to allow it -- only slightly more ridiculous than someone pulling off such an attack using eval/exec behaviors. The bigger issue is that, using this approach, that type must be protected in cases where it is used across different threads in the same invokation, with something like a global mutex before/after using that method, further complicated by abstraction in interfaces

This is not at all what's being suggested, but serves to illustrate 1. that the behavior requested can be mocked using existing functionality as a prototype, but that 2. the behavior introduces some concerning new concerns with which to be concerned. Beyond that, it prepares a big ol' "This is why" for a caveat on...

Point B would require that some level of parametric indirection be accomplished when looking up functions to satisfy an interface. Yes, in code, the method call x.ConvertToY() is the same as invoking the value stored by x.ConvertToY = func(ptrToX *T) func() Y {return func(){return NewY(ptrToX);};}(x), but they're quite different after being reduced to IR ops. While some languages have multiple-pass compilers that resolve those differences into something that is actually roughly the same, Go emphasizes simplicity and transparency, and has a two-stage compiler that targets first an intermediate representation, then remaps symbols to platform-specific instructions, with no further transformation steps.

To accomplish the desired behavior by requiring that interface-satisfaction should consider both static, extremely localized method tables, the following static checks -- note that function calls are being considered as lefthand being declared paramter and righthand being assigned value:

  1. For each righthand value, for any interface methods which are not satisfied by the static method table, is the field-name corresponding to the function name on the interface non-nil and of a type which is assignable to the type of the function of the signature required by the interface.
  2. For each righthand value capable of satisfying the interface by machinery satisfying point 1, does the value become mutated while being indirected by the interface, and does any possible new value for any such field possibly fail to satisfy point 1.
    The above would have to be analyzed b go vet, gopls, and the compiler, and represents a not-insignificant body of work on the toolchain. This represents the ideal case, where the method table for each type is compiled to include any function definitions which become assigned to a property of a given type, and that resolution of such functions would be resolved for each point-in-time in the binary, sometimes being a different ordinal function for the same value, having been assigned a new function on that field at a later point in the program.

As a budget consideration, to implement the above as a runtime-resolved lookup, it would be possible that, at compile time, a virtual function table, containing functions which may or may not be a current value for any field on an instance of the type, might be kept, and the runtime unrolling would add the interface resolution step of a lookup on that vtable, to get the current value to be considered for that interface method. This would be an uncomplicated way to accomplish point 1 above, but it adds to all method resolutions an extra step, and if done at runtime, necessarily makes all new compiled binaries slower, at a factor dependent on the extent of their use of interfaces. The cost could be small, but it would be a cost to pay, if relegated to a runtime check.

As someone that's also proposing a different language change, I share the observation that is is an obnoxious limitation, but I don't see any good way to accomplish, given the current interface resolution mechanics.

@Merovius
Copy link
Contributor

Merovius commented Oct 1, 2021

@sammy-hughes

I don't understand what you are assuming is changing here. I find your comment confusing.

As you can already explicitly write down the types and methods to achieve this, it seems a very questionable assumption that it is infeasible or more difficult, or that it would change the safety of a Go program, to have the Go compiler automatically generate those type-definitions and methods. The compiler statically knows which func-valued fields a given struct literal has. So it knows statically, which methods it would get under this proposal and how to call them. Just as with every other method.

@zephyrtronium
Copy link
Contributor

@sammy-hughes

The compiler already generates wrapper methods in several cases. This proposal would add one more such case – one which would be straightforward for the compiler to analyze. Implementing this change would probably take around ten or fifteen lines of code in total.

I also find your point about concurrent mutations confusing. That is a data race, and it has identical implications as any other data race. It's a problem with the program being written, not a problem for the compiler to try to solve.

@zikaeroh
Copy link
Contributor

zikaeroh commented Oct 2, 2021

One thing I'm wondering about this proposal is how it interacts with reflect-created structs; will those also satisfy interfaces? Previously, #4146 proposed something in reflect to create an interface implementation, and this issue would effectively implement it via structs.

If #4146 is implemented (in some way), it can lead to some interesting constructs like generic mocking (which I have been playing with, but couldn't finish as there's no way to generate a value that implements a specific interface).

@sammy-hughes
Copy link

sammy-hughes commented Oct 3, 2021

@Merovius, wishing that a thing is reasonable is a far cry from demonstrating such. As someone that is also proposing a language change, I recognize that such feels like a frustrating dismissal, but if you think @bcmills is onto something, write a prototype using either unsafe, reflect, or code generation, and enrich his proposal by providing a possible explicit path to implementation.

My arguments above, and in this reply, are based on the following:

I do not observe any simple mechanism by which the Go Compiler can guarantee a valid program which uses any implementation of the proposed functionality, explicitly,

  • A) no implementation which can support existing guarantees will be simple
  • B) without significant new work, any implementation of the above will be unsafe, which is to say, the outcome is dependent on the developer to provide and enforce applicable guarantees.

My earlier comment was an attempt to explore two potential implementation paths and the attendant compromises.

@zephyrtronium suggested that a simpler implementation would only need to add a function for the struct type, expecting the value at the struct location + field-offset to be a function reference, and invoke that. The wrapper function would be in the base function table for that type, even if internally would follow that indirection. Simple solution.

What happens when it fails?

The zero-value for a function reference is nil. (func())(nil)() has no meaning and is an error. Should the compiler protect you from this, possibly emitting a compile-time error if the wrapper method is invoked having a zero-value on the underlying field? How might the compiler know if the field on the type has a nil value, during any given invokation? is such tooling present in the compiler already, or is this new work?

@zephyrtronium also noted that one of my suggested implementations simply represents a data-race, but I'd argue that no other examples of a structural data-race exist in Go, and certainly no other example exists of a data-race caused by implicit behavior around auto-implementation of a structural feature.

As an aside, a comment directed at @zephyrtronium, if an automatic implementation should create a getter-caller for a specific field, why is it unacceptable to write such yourself, with attendant nil-check and internal error-handling when appropriate?

@zephyrtronium
Copy link
Contributor

The zero-value for a function reference is nil. (func())(nil)() has no meaning and is an error.

That expression does have meaning: "call a function value on a nil function." The result is a nil dereference panic. An identical kind of panic happens when calling a method on a nil interface.

Should the compiler protect you from this, possibly emitting a compile-time error if the wrapper method is invoked having a zero-value on the underlying field? How might the compiler know if the field on the type has a nil value, during any given invokation? is such tooling present in the compiler already, or is this new work?

It is impossible for the compiler to statically detect this. That would require solving the halting problem. Instead, the existing nil dereference panic that happens at run-time when any other nil function is called seems reasonable.

@zephyrtronium also noted that one of my suggested implementations simply represents a data-race, but I'd argue that no other examples of a structural data-race exist in Go, and certainly no other example exists of a data-race caused by implicit behavior around auto-implementation of a structural feature.

It's already possible to have a data race on a method call:

a := &T{}
go func() { a = nil }()
a.M()

It is true that a.M() can still succeed if the method does not dereference a, but I don't see this as a notably different kind of race condition. I also think it's misleading to say that the data race is "caused by implicit behavior," because half of the data race is on an assignment, which is very much explicit.

As an aside, a comment directed at @zephyrtronium, if an automatic implementation should create a getter-caller for a specific field, why is it unacceptable to write such yourself, with attendant nil-check and internal error-handling when appropriate?

Substitute "method" for "type" in the comment from @Merovius before:

No, the interest is specifically not having to write that adapter type. It is already clear that you can write it, but we'd like to avoid having to do that. That's the motivation.

@sammy-hughes
Copy link

sammy-hughes commented Oct 4, 2021

@zephyrtronium, if we can agree that, without seeing any other part of a larger example, the following snippet shows A(3) to provide weaker guarantees than B(3), then we likely have no disagreement other than API preference:

var functionalVariable func(int)(bool, error)

func A(value int) (bool, error) {
    return functionalVariable(value)
}

func B(value int) (bool, error) {
    if functinoalVariable == nil {
        return false, errors.New("functionalVariable was not initialized to a valid function")
    }

    return functionalVariable(value)
}

I absolutely want a program to break at the compile step if it can break, even where it otherwise might run fine. Others might find that, crassly put, a stupid and restrictive expectation. I can accept that, but my preference stands.

It's already possible to have a data race on a method call

Can I just cede that point? I don't particularly care. I gave the "modify method table at runtime" example with the explicit qualification that it was a definitively bad solution. It was one of two implementations I could think of, and both involved aggressive compromises.

Both strategies I suggested involved implementation in the IR-to-instruction step, while your conception of the functionality was more at the pre-IR level. I believe I missed the point, and as far as any response to my suggestion, if profitable, if we agree that my suggestions were impractical, then further discussion or comparison serves no purpose.

That expression does have meaning: "call a function value on a nil function." The result is a nil dereference panic.

I appreciate that you're not expecting magic. I don't like the resulting API, but at least this indicates that the difference of opinion is merely a disagreement over whether that API is acceptable.

If this were a language proposal I were making, I would go after a formal enum construct such as Rust has, and use an enum of functions as an example. I'd propose that such an enum be permitted as a method definition as a follow-on proposal. I recognize that what I describe above is a completely distinct feature from what @bcmills originally proposed, with almost no overlap. It's still an avenue that would permit whatever behavior I were intending satisfy my expectation of an API, namely that it fails at compile when incorrectly used.

All that said, I no longer see an issue with how you describe the feature being proposed, at least from an implementation-details perspective. You were clear about what you would expect from the feature, and I believe I could even pull off a decent prototype of such functionality as a code-generation tool. The fact that it doesn't meet my preferences is an arbitrary detail.

@ianlancetaylor
Copy link
Contributor

#47487 appears to be headed toward acceptance. If #47487 is accepted, then presumably this one should be declined.

@bcmills
Copy link
Contributor Author

bcmills commented Oct 7, 2021

I agree. I do like this proposal for how it unifies field accesses and method-calls, but I don't see how to resolve the issues @Merovius pointed out for the migration path, and given that #47487 is likely to be accepted it will address many of the same use-cases.

I withdraw this proposal.

@bcmills bcmills closed this as completed Oct 7, 2021
@golang golang locked and limited conversation to collaborators Oct 7, 2022
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

8 participants