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

Open
DeedleFake opened this issue Aug 1, 2019 · 8 comments

Comments

@DeedleFake
Copy link

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

@gopherbot gopherbot added the Proposal label Aug 1, 2019

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Aug 1, 2019

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Aug 3, 2019

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

This comment has been minimized.

Copy link

commented Aug 6, 2019

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link

commented Aug 6, 2019

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Aug 15, 2019

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants
You can’t perform that action at this time.