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

Go 2: spec: assignability/conversion to non-interface types annoying to test and/or use #31444

Closed
seebs opened this issue Apr 12, 2019 · 10 comments
Labels
FrozenDueToAge LanguageChange v2 A language change or incompatible library change
Milestone

Comments

@seebs
Copy link
Contributor

seebs commented Apr 12, 2019

What version of Go are you using (go version)?

1.12, but it doesn't really matter.

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

N/A

What did you do?

Originally, was looking at pkg/plugin, but the underlying issue applies more broadly. The context was creating a defined type for functions, then declaring functions that match its signature, and checking whether symbols returned by p.Lookup() are of the type. Of course, they can't be -- no function ever has a defined type, it always has an underlying type. A variable of function type can have a defined type, of course, but variables and functions behave a bit differently in plugin code (a lookup of a non-function produces a pointer-to-object, a lookup of a function gives you a function-typed thing).

If you type-assert or type-switch a thing to a target type which is an interface type, the test performed is basically an assignability test on the underlying type and the target type. This allows you to determine not whether the existing object was "really" of that type, but whether it could be. For a non-interface target type, though, you can't do that; you can only check whether that is the specific underlying type.

Related code:

https://play.golang.org/p/qMxLfYzUgpc

(This seems surprising at first, but it's because int is a defined type, but function types aren't.)

The problem comes when trying to use something like plugin with, say, a defined API. The API might want to define a function type, say, api.FooFunc. I load a function symbol from a package, and I want to know whether it's a FooFunc. But I can't check that, because it can't ever actually have that type -- it's going to have whatever underlying type the API used. So I can do a type assertion on that literal type, but that means that I'm duplicating the type, so if the API wants to change the definition of the type (and plugins get updated), my code that's checking whether functions are of that type also has to change. Even if it's in the API, it has do be a duplicate of the type definition; I can't refer to the type definition directly.

Except, perhaps, by doing fancy things with reflect, and using AssignableTo or ConvertableTo.

On the other hand, it's also clear that in many cases, we want that distinction between types to be firmly protected. For instance, if we have type Miles int; type Kilometers int, we don't want a type switch to think that Miles and Kilometers are interchangeable, or that either is interchangeable with int. This may be adequately addressed by the assignability qualifiers, and the basic types being considered defined types.

What did you expect to see?

I'm honestly not sure, but the more I look at this the more it feels sort of weird that you need chains of function calls and tests using reflect to implement "if X could have been assigned to type Y, give me a type Y with X's value" for non-interface types, but for interface types, it's just y, ok := x.(Y).

I can't immediately see a good fix for this. It's very hard to write generically with the tools available, and I don't think it'd be a good idea to change the existing semantics for type assertions, but it might be nice to have some way to express a different kind of type assertion that can handle assignability.

This may not matter for non-functions, because non-functions can always be declared with the right type anyway. The issue comes from the distinction between func x() {} and var x func() = func() {}, which express substantially different things; there's no way to declare a function of a defined function type, as opposed to a variable holding a function-reference. (I suppose that would another way to solve this, but I don't have any good syntax ideas.)

What did you see instead?

An example someone from #darkarts suggested:

https://play.golang.org/p/QmTPtwwizW5

This seems like it's close to the shortest form (and it would need to be longer to avoid panics if the symbol wasn't the right type).

@bcmills bcmills changed the title assignability/conversion to non-interface types annoying to test and/or use Go 2: spec: assignability/conversion to non-interface types annoying to test and/or use Apr 12, 2019
@bcmills
Copy link
Contributor

bcmills commented Apr 12, 2019

#19778 and #20621 are a concrete proposal that I believe intends to address many of the same use-cases, although if I understand correctly, neither proposed to add something analogous to a looser type-switch.

The other interesting case for assignability vs. type is untyped constants, which receive a default concrete type whenever they are passed as variables.

At any rate: as you note, you can use reflect.ConvertibleTo to achieve similar results in many cases (https://play.golang.org/p/9zsFD2siUwr), without a change to the language. Can you give some concrete examples where that materially complicates the code?

(How often would a feature to address this help in practice, especially given the possibility of generics as an alternative way to spend the feature budget?)

@bcmills bcmills added v2 A language change or incompatible library change LanguageChange WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. labels Apr 12, 2019
@bcmills
Copy link
Contributor

bcmills commented Apr 12, 2019

That said, in general we expect proposals to come with a concrete proposal. (It's fine to solicit alternatives, but if we really have no idea how to address a problem then there's not much point keeping an issue open for it.)

So a concrete proposal would be really helpful here, even if it has known flaws.

@bcmills bcmills added this to the Proposal milestone Apr 12, 2019
@seebs
Copy link
Contributor Author

seebs commented Apr 12, 2019

I was sort of secretly hoping that the answer was "haha there's already a perfectly good solution, why don't you..." and I just missed it.

Hmm. Okay, though, here's at least an easy way to express the thing:

Assignable/Convertable Type Switches and Assertions

A type assertion may take the additional forms value.(~T) or value.(~~T). In the former case, the assertion is considered true if value's type is assignable to T; in the latter, the assertion is considered true if the value's type is convertible to T (and the resulting value is converted).

Similarly, the types used in the cases of a type switch may be prefixed with one or two ~ characters, indicating approximation. Thus:

type X int

...
var x X
var i interface{} = x
if _, ok := i.(int); ok { fmt.Println("no") }
if _, ok := i.(~int); ok { fmt.Println("no") }
if _, ok := i.(~float); ok { fmt.Println("no") }
if _, ok := i.(~~float); ok { fmt.Println("yes") }

This would also work the other way. Thus, if you had a plugin.Lookup value of type func(int) error, and type XFunc func(int) error, a type assertion on (~XFunc) would match.

I think ~ is currently impossible here lexically so it doesn't break existing code, assignability and convertability tests are an established thing...

@bcmills bcmills added NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. and removed WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. labels Apr 12, 2019
@seebs
Copy link
Contributor Author

seebs commented Apr 12, 2019

Looking at this... I'm not sure whether this is enough clearer or more expressive than the reflect operations to justify a language change. The comparison is basically:

if f, ok := fn.(~DesiredType); ok {
// CODE
}

vs

var f DesiredType
fp := &f
if reflect.TypeOf(fn).AssignableTo(reflect.TypeOf(f)) {
	reflect.ValueOf(fp).Elem().Set(reflect.ValueOf(fn))
//CODE
}

If there's a way to get rid of the need for the separate fp, it has thus far escaped me. This isn't necessarily a huge problem for the rest of the code, but I'm not at all confident that a casual reader can parse this cleanly.

@bcmills
Copy link
Contributor

bcmills commented Apr 12, 2019

Generally the pattern I've seen is to pull the reflect.TypeOf call up to a package-level variable:

var desiredType = reflect.TypeOf(*new(DesiredType))

[…]

	if reflect.TypeOf(fn).AssignableTo(desiredType) {
		[…]
	}

@seebs
Copy link
Contributor Author

seebs commented Apr 12, 2019

Hmm. Yeah, that seems a bit nicer. I think it's still necessary to have the pointer object around to use Set, though. I dunno. It's certainly a lot more to read, but it's not insane, and it's reasonably isolatable.

@beoran
Copy link

beoran commented Apr 16, 2019

The idea of loosened type asertions and switches with ~ prefixes seems nice and is backwards compatible. But for your use case, couldn't you use an alias in stead of a type definition for your function types and use that?

Something like this:

package main

import (
	"fmt"
)

type X int

type FuncPtrX = func() *X

func A() *X {
	return nil
}

func isItFuncPtrX(in interface{}) {
	if _, ok := in.(FuncPtrX); !ok {
		fmt.Printf("not a FuncPtrX: %T\n", in)
	} else {
		fmt.Printf("isItFuncPtrX OK\n")
	}
}

func isItFuncX(in interface{}) {
	if _, ok := in.(func() *X); !ok {
		fmt.Printf("not a func() *X: %T\n", in)
	} else {
		fmt.Printf("isItFuncX OK\n")
	}
}

func main() {
	isItFuncPtrX(A)
	isItFuncPtrX(func() *X { return nil })
	isItFuncX(A)
	isItFuncX(func() *X { return nil })
}

https://play.golang.org/p/IpwMJQ0EhY0

@seebs
Copy link
Contributor Author

seebs commented Apr 16, 2019

Ooh, that hadn't occurred to me. Yes, I probably could. It's slightly less useful in the non-function case, where I want to distinguish things actually of the type from merely-same-underlying-type, but it seems like it'd work.

I think this is a combination of two minor warts; one is that you can't give a function a defined type, the other is that it's occasionally desireable to have a way to do assignability/conversion tests for things other than interfaces. And this particular use case just happens to overlap them. If functions could have defined types, I'd probably never have noticed the assignability-test question, and if the assignability-test existed, I probably wouldn't have really thought about the defined types.

@seebs
Copy link
Contributor Author

seebs commented Apr 16, 2019

This overlaps a bit with #30931, which is a proposal for allowing defined/named types for functions.

@ianlancetaylor
Copy link
Contributor

Thanks. I think you're right that the most annoying case arises when converting to function types, and it seems that using an alias addresses that case. Other than for function types I think it's rare to want to ask whether an interface type is assignable or convertible to some other type, and the reflect package is available for cases where that is important. So this doesn't seem to justify adding a new language feature.

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

No branches or pull requests

5 participants