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

doc: document idiomatic way to ensure that type satisfies a generic constraint #50813

Open
virtuald opened this issue Jan 25, 2022 · 12 comments
Open
Labels
Documentation Issues describing a change to documentation. NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@virtuald
Copy link

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

// This playground uses a development build of Go:
// devel go1.18-16d6a5233a Tue Jan 25 00:39:08 2022 +0000

What did you do?

https://gotipplay.golang.org/p/RX-7wE7yCzp

package main

// traditional golang interface
type I interface {
	Fn() int
}

// generic interface
type TI[T any] interface {
	Fn() T
}

// generic constraint
type CI[T any] interface {
	~struct {
		Thing int
	}
	Fn() T
}

//
// Implementation
//

type Concrete struct {
	Thing int
}

func (c Concrete) Fn() int {
	return c.Thing
}

// ok
var _ I = Concrete{}

// ok
var _ TI[int] = Concrete{}

// error: interface contains type constraints
var _ CI[int] = Concrete{}

// this doesn't seem very idiomatic
func SatisfiesCI[C CI[int]](c C) {}

func init() {
	SatisfiesCI(Concrete{})
}

Description

The current FAQ describes the idiomatic way to ensure your type satisfies an interface. I find I use this idiom a lot to make sure I don't accidentally break concrete implementations of things that are only used by a few consumers of a library, but not necessarily in the library itself (... or maybe I'm too lazy to write a test for it :shipit:).

As I was migrating a project to see what it would look like under generics, I made some of the interfaces generic and that worked fine. Unfortunately, this sort of check doesn't work with constraints, and there doesn't seem to be a good way to ensure that your type satisfies the constraint other than creating a function to do so.

I get why you can't assign to a constraint interface -- you can't create variables that "are a constraint".

It would be good for someone more experienced with generics to decide on an idiomatic way to do this sort of 'constraint assertion', and make sure it ends up in the FAQ. It's possible there isn't a non-clunky way right now, and that this issue would need to be deferred until more experience is had with generics.

It's also possible I missed an earlier issue/discussion that asked for this, in which case I apologize (but I did look!).

@gopherbot gopherbot added the Documentation Issues describing a change to documentation. label Jan 25, 2022
@ydnar
Copy link

ydnar commented Jan 26, 2022

Would this work?

https://gotipplay.golang.org/p/EKwVbjuZYEK

var _ any = TI[int](Concrete{})

@virtuald
Copy link
Author

No, that's for TI, which is just a generic interface. The constraint is CI.

@mknyszek mknyszek changed the title faq: document idiomatic way to ensure that type satisfies a generic constraint doc: document idiomatic way to ensure that type satisfies a generic constraint Jan 26, 2022
@mknyszek mknyszek added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Jan 26, 2022
@mknyszek mknyszek added this to the Go1.19 milestone Jan 26, 2022
@mknyszek
Copy link
Contributor

CC @ianlancetaylor maybe?

@ianlancetaylor
Copy link
Member

The best I can come up with is

func SatisfiesCI[_ CI[int]]() {}
var _ = SatisfiesCI[Concrete]

Anybody have any other ideas?

@ydnar
Copy link

ydnar commented Jan 26, 2022

This?

https://gotipplay.golang.org/p/lq3GDh15HpB

type _ interface {
	CI[int]
	Concrete
}

@ianlancetaylor
Copy link
Member

@ydnar I don't see how that ensures that Concrete satisfies the constraint CI[int]. That will work if you substitute any other type for Concrete.

@ianlancetaylor
Copy link
Member

This version does it in a single declaration, but you still have to give the declaration a real name:

type SatisfiesCI[_ CI[int]] struct {
	_ *SatisfiesCI[Concrete]
}

@virtuald
Copy link
Author

One of my first tries at this tried to define a variable with an anonymous struct and assign it, but I quickly discovered you can't add a type parameter to an anonymous struct so that didn't work... and it's a lot of punctuation.

// this doesn't work
var _ = struct[x CI[int]{_ x}{Concrete{}}

ianlancetaylor's single definition is better than the functional definition.. but it's not terribly obvious what it's doing (though maybe that's why it should be named?). Though, I guess is the standard idiom really all that obvious if you haven't seen it before either?

One could add a general 'Satisfies' function to the constraints package? But I don't believe there's a way to make a function that would work for all types/constraints without some C++-style meta-madness. But if there were, then you could write something like:

var _ constraints.Satisfies[CI[int], Concrete]

@virtuald
Copy link
Author

virtuald commented Jan 30, 2022

func SatisfiesCI[_ CI[int]]() {}
var _ = SatisfiesCI[Concrete]

I've been using this one from Ian with a specific flavor. I create the SatisfiesXXX function right next to the constraint, so it's really obvious why it exists even with complex generic parameters. It gives a nice error message, and in particular is really easy to understand at the point the error is generated because it's just two names, similar to the original idiom. Also, at the point of use, type inference makes it so you often (but not always) only need to put in the typename, regardless of the number of generic parameters.

type SatisfiesCI[_ CI[int]] struct {
  _ *SatisfiesCI[Concrete]
}

I found that the single-declaration version that Ian came up with is really difficult to write correctly, really difficult to read, and particularly if there are a few generic parameters you're doing a lot of duplication of text.

@ianlancetaylor
Copy link
Member

Moving to Backlog.

@ianlancetaylor ianlancetaylor modified the milestones: Go1.19, Backlog Jun 24, 2022
@lpar
Copy link

lpar commented Jan 6, 2023

Just hit this issue. We're looking at implementing some container/collection classes, and I wanted to be able to enforce that a particular generic type implementation satisfies a specific generic interface.

For example, I might want to be able to assert that both ConcurrentSliceSet[E] and SliceSet[E] implement Set[E].

I can write something like var _ Set[string] = (*SliceSet[string](nil)) (assuming string satisfies E), but that's obviously not ideal. Maybe now we have generics it's time to revisit whether Go needs some specific interface assertion syntax?

@ydnar
Copy link

ydnar commented May 19, 2023

Inspired by type alias syntax (type T = U), what if the language permitted assertions using an == or ~= operator? e.g.:

type T int

type T == fmt.Stringer
type T == error

func (v T) String() string {
	return strconv.Itoa(v)
}

func (v T) Error() string {
	return v.String()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Documentation Issues describing a change to documentation. NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
Status: No status
Development

No branches or pull requests

6 participants