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: replace contract kind lists with methods on built-in types #33410

Closed
DeedleFake opened this issue Aug 1, 2019 · 12 comments
Closed
Labels
FrozenDueToAge generics Issue is related to generics LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@DeedleFake
Copy link

DeedleFake commented Aug 1, 2019

Just to clarify before I get started here: This is not a proposal for operator overloading. I completely agree with the arguments against operator overloading and do not want to see it in the language. Rather, this is essentially a proposal for the exact opposite.

The generics draft design, as it stands currently, allows two different features of types to be listed inside of contract specifications. You can list methods that the type must have, possibly with multiple options, and you can also list requirements on the underlying type. The primary purpose for the latter of these features is to allow certain operators to be used with types bound by the contracts inside of a function using it. For example,

contract Integer(T) {
  T int8, int16, int32, int64
}

func Min(type T Integer)(v1, v2 T) T {
  // Comparison is legal because it's legal for all the types in the contract.
  if v1 < v2 {
    return v1
  }
  return v2
}

However, I think that this actually completely unnecessary. I think that contracts can be simplified further by both removing the ability to specify underlying types and pre-defining methods that wrap each of the possible operators on a built-in type. For example, if there was a predefined method equivalent to func (v int) Less(v2 int) bool { return v < v2 }, then you could just do

contract Less(T) {
  T Less(T) bool
}

func Min(type T Less)(v1, v2 T) T {
  if v1.Less(v2) {
    return v1
  }
  return v2
}

and now any type that has support for a < operator is automatically covered as well as any user-defined types that have a method that fits the same pattern. This keeps contracts focused on the functionality, rather than the data layout. It also means that you can't create impossible contracts, as you can in the current design, as any set of methods with unique names is possible to define.

The hardest operation to define is probably type conversions. I'm not entirely sure how that should be handled, but one way would be to allow something like either typename(T) or T(typename) in the contract body to specify that a type can be converted to and from another one.

Edit: range could be a bit odd to use, too, especially because ranges over channels don't support the two-variable syntax for them.

All of the operator-wrapping methods could also be listed in the documentation of the builtin pseudo package for quick reference with go doc, possibly as methods on a fake type called Operator, or something.

@gopherbot gopherbot added this to the Proposal milestone Aug 1, 2019
@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Aug 1, 2019
@ianlancetaylor
Copy link
Contributor

Some things that must be considered with this approach are:

  • type conversions
  • untyped constants
  • for/range
  • unary vs. binary operators
  • index operations
  • assignment operations

We actually went through this approach in considerable detail before deciding on the approach in the current design draft. Personally I think the current design draft is simpler to understand and to use. But we'll see.

@bserdar
Copy link

bserdar commented Aug 1, 2019

I think contracts will be much more complicated if the focus is on the functionality in a language without operator overloading. There can be no types in go that implement < but not >.

For instance, once you require that a type must have + and - you limit the options to numeric types. I think it is more intuitive to write and read a contract that says "type T is a number" than writing "type T supports addition and subtraction".

@Freeaqingme
Copy link

Is this something that breaks BC, and must therefore be decided on before Go 2.0 ? If not, I'd personally prefer to first see how generics work out in practice, and only afterwards see if/what/how existing functionality like proposed here should be altered.

@pcostanza
Copy link

In my (highly uninformed) gut feeling, I think it's exactly the other way around: I believe contracts that enumerate type names could be sufficient, and contracts that list method names are redundant (due to the fact that interfaces already exist).

I could even imagine a more minimalistic approach: Just define a number of predefined contracts of meaningful subsets of predefined types, as they occur in the language specification (like Integer, FloatingPoint, Number, Assignable, Comparable, Ordered, Slice), and see how far we get with them. Maybe that's already far enough...

@DeedleFake
Copy link
Author

DeedleFake commented Aug 6, 2019

If you used interfaces in place of type names in contracts than multi-type contracts can get kind of awkward. For example, imagine that you want to write a contract for a Get() method. In the current draft, it's just

contract Get(T, K, V) {
  T Get(K) V
}

This clearly states exactly how all three types are involved in this. If you had to use interfaces, though, you'd have to do something like this:

type Getter(type K, V) interface {
  Get(K) V
}

contract Get(T, K, V) {
  T Getter(K, V)
}

That's pretty repetitive. And you can make it even more so by adding a constraint on one of those secondaries, like requiring K to match contracts.Numeric:

type Getter(type K, V contracts.Numeric(K)) interface {
  Get(K) V
}

contract Get(T, K, V) {
  T Getter(K, V)
}

or

type Getter(type K, V) interface {
  Get(K) V
}

contract Get(T, K, V) {
  contracts.Numeric(K)
  T Getter(K, V)
}

@pcostanza
Copy link

I should have been clearer: My suggestion is to have a list of predefined contracts, and not provide any means to define your own at all.

@target-san
Copy link

target-san commented Aug 13, 2019

@bserdar

I think contracts will be much more complicated if the focus is on the functionality in a language without operator overloading. There can be no types in go that implement < but not >.

I think OP talks about different scenario. Let's assume we have some binary tree map type. For this type, keys should be ordered. With current approach to contracts, only predefined types support comparison operations directly. Other types should invent their own machinery to support ordering - and we're left with two distinct "contracts". But if we have standard contract like this

type Order int

const (
    OrderLess Order = -1
    OrderEqual Order = 0
    OrderGreater Order = 1
)

contract Ordered(L, R) {
    func Compare(L, R) Order
}

and have built-in types support it (where necessary), we may define our tree map as

type SomeTreeMap(type K Ordered(K, K), type V) {
    // ...
}

and have support for any type which wants to be map key

@ianlancetaylor Kind of argument for such "wrapper" contracts. Sorry if I reiterated unknowingly on something already discussed.

@target-san
Copy link

Going further, I'd say that contracts could in fact look like interfaces, with lists of available methods instead of "allowed expressions". The latter approach is what C++20 land will have with concepts, and it's IMO a losing game. You don't get precise requirement, only some vague requirement "this expression must compile" - which can often be achieved in multiple ways, often unintended.

If we go even further, we may go to conclusion that such contracts which are described as sets of functions are very similar to interfaces - except interfaces don't need to know their static type. So maybe it's possible to extend interfaces with "sized" methods and have single language entity cover both static and dynamic polymorphism. The closest approach is Rust traits.

@ianlancetaylor
Copy link
Contributor

@DeedleFake I encourage you to move this beyond a suggestion to actually write down the complete list of methods that would be required. Otherwise this is difficult to evaluate fairly.

@DmitriyMV
Copy link

@DeedleFake sorry for the nudge, but is this still relevant with adjusted generics we have now?

@target-san
Copy link

@DmitriyMV Sorry for interfering, though I'm interested in this topic too. AFAIK Go generics still have type lists and no way to make both builtin and user-defined types support same operations like arithmetics, hash computation etc.

@ianlancetaylor
Copy link
Contributor

This proposal is clearly outdated. There are ideas that are still relevant, but they would have to be completely rethought with the accepted generics approach that uses interface types rather than contracts. I'm going to close this issue but feel free to open a new one that is reworked for the present day. Thanks.

@golang golang locked and limited conversation to collaborators Jan 25, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge generics Issue is related to generics LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

8 participants