Skip to content

proposal: constraints: new package to define standard type parameter constraints (discussion) #47319

rsc announced in Discussions
proposal: constraints: new package to define standard type parameter constraints (discussion) #47319
Jul 21, 2021 · 9 comments · 49 replies

We are ready to finish the discussion on #45458, because type sets have been accepted.
Here is the current API:

The intent is that if we add any new predeclared types, such as int128 and uint128, those types will be supported by these constraints as appropriate based on the constraint descriptions.

// Package constraints defines a set of useful constraints to be used with type parameters.
package constraints

// Signed is a constraint that permits any signed integer type.
type Signed interface { ... }

// Unsigned is a constraint that permits any unsigned integer type.
type Unsigned interface { ... }

// Integer is a constraint that permits any integer type.
type Integer interface { ... }

// Float is a constraint that permits any floating-point type.
type Float interface { ... }

// Complex is a constraint that permits any complex numeric type.
type Complex interface { ... }

// Ordered is a constraint that permits any ordered type: any type that supports the operators < <= >= >.
type Ordered interface { ... }

Replies

9 comments
·
49 replies

[Resolved: Decided not to do this, at least not yet. - rsc, July 28 2021]


On the issue, I had mentioned that another useful type constraint would be "Arithmetic", which contained any type with +, -, *, / operators defined on them. This would mean Integer | Float | Complex. An alternative name could be "Number", however that may be confusing because one may assume that "Number" is ordered when it is not.

3 replies
@rsc

rsc Jul 28, 2021
Maintainer Author

This is easy to add in a future release, but we haven't seen a need for it yet (except in tests).

@rsc

rsc Jul 28, 2021
Maintainer Author

The general consensus is that Number is better than Arithmetic if we do this (but not yet if at all).

@rsc

rsc Jul 28, 2021
Maintainer Author

Resolved: decided not to do this for now.

[Resolved: Decided to keep "constraints". - rsc, July 28 2021]


I'd prefer the singular form constraint as the package name, because in type parameter lists we refer to one constraint at a time: constraint.Ordered, constraint.Integer. It simply reads better in my opinion.

10 replies
@carlmjohnson

I thought about is, but that’s already the name of a testing library (not that the conflict matters per se). I really like types though. It feels very “Go” to me.

@deanveloper

I like “types” a lot more. Also - It’s not that I don’t like “constraints” for its length (even though I dont particularly like how long it is). However, I’d like to erase the difference between a “constraint” and an ordinary interface.

@jimmyfrasche

There's already go/types

@rsc

rsc Jul 28, 2021
Maintainer Author

Technically this could be "constraint"
We've got bytes, strings, errors, and so on because those cannot use the singular.
maps will not be able to use the singular either.
slices could, but having to remember "maps.Equal" vs "slice.Equal" is a bit weird.
It seems more consistent in this case to just use plural for all of these helping-builtins packages, even when they don't strictly need to be (slices, constraints).

@rsc

rsc Jul 28, 2021
Maintainer Author

Resolved: "constraints" is the color of this bike shed.

[Resolved: Will make docs clear and add a vet check. - rsc, July 28 2021]


Is adding types in the future going to cause compatibility problems? (This is reposting and merging two of my comments on the issue.)

A simple but perhaps too contrived example:

type MySigned interface { type int8, int16, int32, int64 }
func MyAbs[T MySigned](v T) T { ... }
func Abs[T constraints.Signed](v T) T { return MyAbs(v) }

(Of course presumably Abs is in some other package, and perhaps the type lists involved are more complex.) That compiles today, but not in a future Go version where constraints.Signed contains int128.

A perhaps more likely example (assuming #45346 is accepted):

func Abs[T constraints.Float](v T) v {
  switch T {
  case ~float32:
    return math.Float32FromBits(math.Float32Bits(v) &^ (1 << 31))
  case ~float64:
    return math.Float64FromBits(math.Float64Bits(v) &^ (1 << 63)) // math.Abs
  default:
    panic("unknown type")
  }
}

(I'm using syntax proposed in #45380 here for brevity, but since T cannot be of interface type I believe this code can be expressed equally well without it, as switch any(v).(type) + case interface{ ~float32 }.) Right now this can never panic; if float128 is added it can, although at least it still compiles.

It doesn't seem like this should block the addition of the constraints package, but I don't see an obvious way to work around it. Part of the problem here is that experienced Go programmers know what it takes to make existing Go types backwards-compatible: for structs it's fine to add methods and fields (assuming no unkeyed literals); for interfaces may add methods only if they have an unexported method; for functions you just can't change them. But I, at least, don't know those rules for type lists yet -- these examples suggest that, more like interfaces, they can't be changed without some (in this case unknown) setup.

20 replies
@benjaminjkraft

Yeah, of course this example is no more "interesting". The point is it's less obvious to a human: you aren't using the constraints package directly at all when you define MyValidAbs. You have to know that you are using it indirectly.

@rsc

rsc Jul 28, 2021
Maintainer Author

It sounds like we know what the rules need to be, and that we can write a vet check for them. That should be enough to keep people from accidentally writing code that would not be "int128-safe".

@rsc

rsc Jul 28, 2021
Maintainer Author

Resolved: will make sure we have a vet check for the expectations around these integer constraints.

@balasanjay

Out of curiosity, could the constraints package declare a local unexported type (maybe called futureInts or similar), and include that in the constraint?

That would ensure that attempting to assign constraints.Signed to any externally declared constraint would presumably not work.

(This is similar to the idea of writing interfaces with an unexported method)

@ianlancetaylor

My thinking is that a local unexported type would work, but I worry that it would be a cryptic wart for most future readers, to solve a problem that I think is quite unlikely and that we can in any case check for in other ways.

I have a question about Ordered. If there is a self-defined composite literal that does not support operators < <= >= > but methods like Less LessEqual, then how to let the type constrained in Ordered so that we can utilize any standard package which parameters are constrained using constraints.Ordered?

1 reply
@Merovius

It is not possible. A generic function can either be written use parameters or methods, but never both. If you want to support both predeclared types and composite types, you need to write two functions.

rsc
Jul 28, 2021
Maintainer Author

Update, July 28 2021

Based on the discussion, we will make the following changes to the proposed API:

  • Added constraints.Slice to support more readable slice type parameters, based on discussion in #47203.
  • Will make forward-compatibility requirements for Integer etc clear in docs and add vet check.
  • Decided to keep "constraints" as the name of the package.
  • Decided against Number (not enough evidence of need).

I have also added [Resolved] notes at the top of comments that we consider resolved, in addition to replying with text like "Resolved...".

@ianlancetaylor is going to update the proposal text.

0 replies

Given that we are adding constraints.Slice[Elem], would it also make sense to add constraints.Map[Key, Value] for #47330?

3 replies
@carlmjohnson

I was in the middle of typing this:

Following on from constraints.Slice, what about constraints.Map? It would be useful e.g. for dealing with http.Header and url.Values using the maps package.

@rsc

rsc Aug 4, 2021
Maintainer Author

Talked to @ianlancetaylor, and it seems like we should add Slice, Map, and Chan.

@ianlancetaylor

Now done, over a #45458.

rsc
Aug 4, 2021
Maintainer Author

Update, August 4 2021

Based on the discussion, we will make the following changes to the proposed API:

  • Add constraints.Map and constraints.Chan to match constraints.Slice.

@ianlancetaylor is going to update the proposal text.
It seems like we are getting close to likely accept. Perhaps next week.

I am also starting to wonder how long it will take me to learn to type the word constraints.

11 replies
@ianlancetaylor

@deanveloper Sorry, not quite seeing what you are getting at. Did you mean to add a "constraints." somewhere in there?

@ianlancetaylor

@deanveloper Oh, I see. Yes, I think that code like

type M[K any] map[K]int

should fail to compile. (It would work if it were K comparable.) Based on that, probably your example should also fail to compile.

@deanveloper

@ianlancetaylor I was mentioning the previous state of constraints.Map, wondering if using any for the map key would fail to compile.

@Merovius If that were the case, would I be able to write a constraint like type Foo[T any] interface { ~StringsOnly[T] } (where StringsOnly is something like type StringsOnly[T interface{ ~string }] interface { Bar(T) T })? That doesn't seem like a good idea.

@Merovius

@deanveloper I don't understand your example. I do think its type-set would be empty (StringsOnly[T] is a defined type and there are no types whose underlying type is a defined type - except the predeclared types), but I don't understand what it would be intended to do.

Either way, the problem is that a general proof of a type-set being empty is difficult. I wrote down a proof of that here. It might be possible under the latest restrictions. I haven't checked, but the restrictions where put in place to solve very similar problems, so it's not unlikely they would make it possible to determine if a type set is empty as well. Someone would have to sit down and write down an algorithm to do so (or a proof that we still can't) and we would then have to decide if that algorithm is sufficiently simple to end up in the spec - or sufficiently self-evident that we don't have to.

Meanwhile, there are no real downsides to allowing constraints even if their type set is empty. Even the most casual of tests (i.e. even just trying to call a function using that constraint) will immediately surface the problem, as that constraint can't be instantiated, so the function can't be called.

Lastly: This is probably not the place to have this discussion. Note that the immediate concern - making the type-parameter in Map comparable - is already addressed.

@urandom

@Merovius right, I meant comparable. Late night comments arent well testable

Presumably, the Slice, Map and Chan constraints should use approximation-elements? Otherwise we wouldn't much need them :)

1 reply
@ianlancetaylor

Yes, done. Thanks.

I believe most functions accepting channels should restrict the direction of communication. The Chan constraint as is, though, doesn't allow that - you have to choose between writing func F[E any](ch <-chan E), which excludes defined types, or write func F[C Chan[E], E any](ch C), which doesn't verify directionality.

We could add ReadOnlyChan and WriteOnlyChan. But even then, could func F[C ReadOnlyChan[E], E any](ch C) be instantiated with a plain chan T, or would it have to be explicitly converted to <-chan T? I'm genuinely asking, FWIW, I can't really think of the exact rules and how they would apply to this.

0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Discussions