Hacker News new | past | comments | ask | show | jobs | submit login

This 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.




Yeah, 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.


I wonder: why not just accepting interfaces as contracts (a subset), e.g. literally allowing

    func Foo(type T fmt.Stringer)(T) T
and allowing runtime values of type contract, e.g.

    contract SignedInteger(T) {
        T int, int8, int16, int32, int64
    }

    func Foo(x SignedInteger) bool { return x > 0 }

i.e. decoupling the runtime boxing vs the compile type parameter resolution from the "interface specification language".

I do understand that the need for the new "contract" syntax becomes apparent when you have contracts over multiple variables, but that doesn't mean in the single type variable degenerate case there is no overlap. Perhaps embracing that overlap would help people understand more quickly where the actual difference is (in the way types and function signatures uses them, rather than what operations they describe)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: