Every 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.
Hmm, 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?
Because 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.
Agreed that devirtualization exists and improves performance, but it doesn’t address the semantic problem in that interfaces are about heterogeneity (and thus dynamic dispatch) while contracts are about homogeneity (which naturally lends itself to static dispatch). Additionally, devirtualization is a lot harder to implement (at least with any sort of useful comprehensiveness) as the compiler needs to prove to itself that the concrete type never varies which implies plumbing information across compilation units and (I think) breaking independent compilation of compilation units or at least making compilation dependent on some earlier whole-program-analysis pass.
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.