6 ms·
It seems as a proposal to overlap interfaces a lot. - Interfaces gather methods. The concrete type referenced through an interface doesn't need to know about t
by JulianMorrison 7y ago
It seems as a proposal to overlap interfaces a lot.
- Interfaces gather methods. The concrete type referenced through an interface doesn't need to know about the interface. Interfaces can be used to decouple operations from the details of the thing being operated upon.
- Contracts gather methods. The concrete type referenced through a contract doesn't need to know about the contract. Contracts can be used to decouple operations from the details of the thing being operated upon.
Disjunction: Interfaces are resolved at runtime and result in indirect calls. Contracts are resolved at compile time and their contract-nature is lost in object code, they just become direct calls. Oh and contracts can do operators.
It feels to me like the risk here is not more syntax but less orthogonality. There will be two features competing to be the standard way to do the contract-like things.
- logicchains 7y agoContracts support multiple "receivers". If I want to write a function that converts between two arbitrary types, I can write a `func convert(Type1 Type2 Convertible)(a Type1, b Type1){...}`, where Convertible in a contract with one method that takes a Type1 and receives a Type2. There's no way to do the same thing with interfaces as an interface can only reference a single type, a single receiver.
- jerf 7y agoAnd on the flip side, interfaces allow heterogeneous lists; []io.Reader can have several different distinct types that implement io.Reader in them, whereas the contract version would be "a slice of a type that implements io.Reader" but it would have to all be the same type. I won't deny they're not orthogonal, nor that we're probably going to see some confusion about which to use when from beginners, nor that there are probably interfaces in the wild that really ought to be contracts (I'm pretty sure I've got a couple myself, though I haven't fully checked until the proposal is stabilized), but neither do they quite stand in for each other. Perhaps if this was in the language from day one, more work would be put in to finding a way to make just one "interface/contract" feature with some kind of (probably confusing) parameter in it that lets it serve both purposes, but doing that today doesn't seem to be on the table.
- logicchains 7y agoRust and Haskell both use something called existential types to implement their versions of Go's interfaces. An existential type is basically a type meaning "any type that implements the given type-class/trait/concept", so you can use to create an array of different types that all satisfy the same contract. Type-classes and traits are roughly Haskell and Rust's equivalents of Go's concepts. Interestingly, Go contracts actually support a feature missing from Rust: multi-param contracts; contracts that refer to two or more types.
- steveklabnik 7y ago... and Rust does unify both of these use cases with traits, that is, what you're referring to is a "trait object", which is the same thing as Go with regards to interfaces. The difference is when you use a trait to bound a generic, in which case they get monomorphized and become "direct calls" in the OP's parlance. I haven't re-read it recently, but https://softwareengineering.stackexchange.com/questions/247298/how-are-rust-traits-different-from-go-interfaces https://softwareengineering.stackexchange.com/questions/2472... did contain a pretty great explanation of how these things relate. It's also worth noting that we also saw some confusion from people not realizing these two use cases for the same feature, and so added some syntax to differentiate the two.
- jerf 7y ago"An existential type is basically a type meaning "any type that implements the given type-class/trait/concept", so you can use to create an array of different types that all satisfy the same contract." I can't speak for Rust, but when I was first learning Haskell I was bitten by believing it worked that way, but it doesn't: Prelude> :t sum sum :: (Num a, Foldable t) => t a -> a Prelude> sum [1::Int, 2::Int] 3 Prelude> sum [1::Float, 2::Float] 3.0 Prelude> sum [1::Float, 2::Int] <interactive>:25:16: error: • Couldn't match expected type ‘Float’ with actual type ‘Int’ • In the expression: 2 :: Int In the first argument of ‘sum’, namely ‘[1 :: Float, 2 :: Int]’ In the expression: sum [1 :: Float, 2 :: Int] You can kinda make it work by wrapping it behind yet another type that basically boxes the different underlying types, but that causes its own problems (including, for instance, the fact that no amount of boxing will make this example work; sum is also demonstrating why Haskell won't do this). Basically in Haskell, either do it a different way (and there's usually a different way that will make it work better), or go to full on heterogeneous lists that don't even need the values to be the same type like HList, but unless I've missed an implication of one of the more recent type extensions, you can't have a simple container like this that has heterogeneous types based on their interface. (I know you can make complex ones, because I have myself, but not the way I'm fairly sure you meant.)
- comex 7y agoIf interfaces could have type parameters, then you could just do something like: func convert(Type2, Type1 ConvertibleTo(Type2))(a Type1, b Type1){…} However, that wouldn’t work as well if you wanted multiple methods with different receiver types, e.g. if you wanted to require convertibility in both directions.
- ithkuil 7y agoThis overlap also struck me as something worth considering. Just to add some more data points to your disjunction section: When talking about composite types using interfaces or type variables, the difference becomes more apparent: func Concat(type T stringer)(ts ...T) stringer vs func Concat(ts ...fmt.Stringer) string On the type-parameterized case once you pick the concrete type, all the elements of the slice must be of the same type. On the other hand this means that you can use existing slices. One of the most common questions I found when teaching Go to newcomers is "why can't I convert a slice of X where X implements Y into a slice of Y"? i.e.: type sint int func (s sint) String() string { return fmt.Sprint(int(s)) } func demo(vs []fmt.Stringer) { for _, v := range vs { fmt.Println(v.String()) } } func main() { a := []sint{1, 2, 3} s := make([]fmt.Stringer, len(a)) for i := range a { s[i] = a[i] } demo(s) } with contracts you can just pass a concrete instance of the composite type: func demo(type T fmt.Stringer)(vs []T) { for _, v := range vs { fmt.Println(v.String()) } } func main() { a := []sint{1, 2, 3} demo(a) } So, even putting aside runtime optimizations, there are perfectly valid use cases where you either want e.g. "a slice of any value as long as each value implements a given method" or "a slice of a concrete type which implements a given method". The former allows you to lift your value into the abstraction (e.g. you can pass your implementation of an io.Writer anywhere some code expects one). This is generally unidirectional; e.g. the external code doesn't give you back an instance of your own type (because in order to do it, you'd have to employ a runtime type assertion to get it back). The latter allows you to consume some abstraction on top of your data. This code can produce new values of your types (without hacks like factory interfaces) and the compiler knows about that.
- JulianMorrison 7y agoYeah, it looks like "contracts for homogeneous, interfaces for heterogeneous / pluggable" is a useful distinction. But what worries me is that it's an extremely subtle, implementation-understanding distinction. They are still extremely similar and their dissimilarity seems to be of the sort that will bite people. Beginners definitely, and sometimes even pros.
- 7y ago
- politician 7y ago"Specifying a required method looks like defining a method in an interface type, except that the receiver type must be explicitly provided." This seems fine to me, I don't feel that these features compete in the same way you're suggesting. With interfaces, the receiver type is not constrained; with contracts, it is explicitly provided. Contracts are resolved at compile-time while interfaces are resolved at runtime. That doesn't seem like a stretch, but I'll have to dig in more with the proposal. If anything, extending the range of const compile-time capabilities is a good thing.
- axaxs 7y agoI can't help but agree. Go, to me, was known for its shorthand cleverness in design - capital letter instead of public, for replaces whiles, etc. It was a breath of fresh air. This overlap -feels- a bit wrong when viewed in that context, in my opinion.
- munificent 7y agoThis is because interfaces and contracts are conceptually similar. They both give you a way to abstract over a concrete implementation of some set of operations. That in turn lets you use multiple different types that support those operations. However, they operate at different times. Interfaces are a runtime abstraction and contracts are at compile time. Interfaces are dynamic and contracts are static. This manifests itself in important ways: * Contracts give you a way to enforce homogeneity where interfaces permit heterogeneity. Say you want a function that takes a list of objects that you can call `Foo()` on. If you define that function as accepting a list of some Fooer interface, then the list you get at runtime may contain a mixture of all sorts of different types of objects, all of which implement Fooer. If your function excepts a list of a generic type implementing a Fooer contract then at runtime you get a list of objects all of the same type, one which supports supports Fooer. * Because of the above, there is a runtime overhead to working with interfaces. When you pass a concrete type to something expecting in interface, that value needs to be boxed. When invoke a method on it, you need some amount of dynamic or virtual dispatch at runtime. With contracts, you can generate code at compile time that calls directly into the chosen type because you know at compile time exactly which concrete type has been chosen. So I think you're right to point out the similarities. But I think having both and having them be similar is a good thing. It lowers the cognitive load while giving more things users can express. I don't think the features will compete any more than interfaces and generics compete in other languages. There are analogues of this elsewhere. C++ basically recapitulated this same history decades ago. Initially it only had virtual methods but no generics. You would see collection libraries that tried to define collections of arbitrary types using interfaces, but it never quite worked right. A list of ints isn't really a subclass of List<T> even though there is some polymorphism going on. Templates resolved that. Likewise, Java added generics because interfaces were not sufficiently expressive. Rust has traits and trait objects. The two features are very similar and reuse a lot of mechanism, but also let you do different things. Traits are basically like contracts and are instantiated and specialized at compile time with no runtime overhead. Trait objects are like interfaces or vtables. They give you runtime polymorphism at the cost of some performance.
- ori_b 7y ago> However, they operate at different times. This seems like it would be solvable by simply allowing interfaces to constrain generic parameters. Something like: func foo(type A Comparable, B OtherIface, C /* unconstrained */)(a A, b B) C { ... } You'd need to allow for type parameters on interfaces, of course: func lookup(type A Indexable(B), B Hashable)(set A, idx B) { ... } But there's no need, IMO, to separate interfaces and contracts.
- alexhutcheson 7y agoEvery object-oriented language that has generics has ended up with separate constructs and overlapping use-cases for compile-time dispatch vs. runtime dispatch. C++ has both templates and virtual methods, and there are many problems where you could comfortably use either. Likewise, in Java you can choose between using a generic class or defining a class hierarchy that depends on dynamic dispatch for behavior. The overlap seems to be a bit of "essential complexity" - if you support both compile-time and runtime dispatch, then the programmer has to choose which to use, and there are many cases where either would work.
- iainmerrick 7y agoHmm, I’m not sure that’s quite a fair representation of Java. Contracts are interfaces in Java. Using generics just gives you compile-time type-safety (partial safety, anyway) without having much effect on how things work at runtime. Using interfaces for generic type constraints feels very natural, even though there are some rough edges you can bump into when trying to do overly clever stuff. It’s surprising to me that Go wouldn’t just use interfaces as “contracts” in the same way. Why add a whole new class of entities?
- weberc2 7y agoBecause they have different semantics. Contracts are generic over multiple types and as /u/munificent mentioned elsewhere, they allow you to write code that enforces homogeneity. You can write a generic function that takes a slice of elements that have a Foo() method and all instances in the slice will have the same concrete type. This would allow the compiler to easily specialize those function calls, so no virtual function overhead. This means we could have an efficient, straightforward sort implementation that performs as well as a hand rolled implementation.
- pjmlp 7y agoJava compilers have been doing devirtualization for years with JIT and AOT tooling.