Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Will contracts replace interfaces?

Will contracts replace interfaces?

Based on a previous talk named "Understanding the interface", in this talk I:
- discuss the pros and cons of interfaces,
- introduce the concepts from the generics draft for Go2 type parameters and contracts, and
- compare the two approaches.

Francesc Campoy Flores

February 11, 2019
Tweet

More Decks by Francesc Campoy Flores

Other Decks in Programming

Transcript

  1. "In object-oriented programming, a protocol or interface is a common

    means for unrelated objects to communicate with each other" - wikipedia
  2. "In object-oriented programming, a protocol or interface is a common

    means for unrelated objects to communicate with each other" - wikipedia
  3. "In object-oriented programming, a protocol or interface is a common

    means for unrelated objects to communicate with each other" - wikipedia
  4. concrete types in Go - they describe a memory layout

    - behavior attached to data through methods int32 int64 int16 int8 type Number int func (n Number) Positive() bool { return n > 0 }
  5. type Positiver interface { Positive() bool } abstract types in

    Go - they describe behavior - they define a set of methods, without specifying the receiver io.Reader io.Writer fmt.Stringer
  6. type Reader interface { Read(b []byte) (int, error) } type

    Writer interface { Write(b []byte) (int, error) } two interfaces
  7. - writing generic algorithms - hiding implementation details - providing

    interception points why do we use interfaces?
  8. a) func WriteTo(f *os.File) error b) func WriteTo(w io.ReadWriteCloser) error

    c) func WriteTo(w io.Writer) error d) func WriteTo(w interface{}) error what function do you prefer?
  9. Cons: • how would you test it? • what if

    you want to write to memory? Pros: • ? a) func WriteTo(f *os.File) error
  10. Cons: • how do you even write to interface{}? •

    probably requires runtime checks Pros: • you can write really bad code d) func WriteTo(w interface{}) error
  11. Which ones does WriteTo really need? - Write - Read

    - Close b) func WriteTo(w io.ReadWriteCloser) error c) func WriteTo(w io.Writer) error
  12. “Be conservative in what you do, be liberal in what

    you accept from others” - Robustness Principle
  13. “Be conservative in what you send, be liberal in what

    you accept” - Robustness Principle
  14. Abstract Data Types Mathematical model for data types Defined by

    its behavior in terms of: - possible values, - possible operations on data of this type, - and the behavior of these operations
  15. Axioms: top(push(S, X)) = X pop(push(S, X)) = S empty(new())

    !empty(push(S, X)) Example: stack ADT
  16. func Size(s Stack) int { if s.Empty() { return 0

    } return Size(s.Pop()) + 1 } algorithms on Stack
  17. type Interface interface { Less(i, j int) bool Swap(i, j

    int) Len() int } a sortable interface
  18. type Reader interface { Read(b []byte) (int, error) } type

    Writer interface { Write(b []byte) (int, error) } remember Reader and Writer?
  19. func Fprintln(w Writer, ar ...interface{}) (int, error) func Fscan(r Reader,

    a ...interface{}) (int, error) func Copy(w Writer, r Reader) (int, error) algorithms on Reader and Writer
  20. “Be conservative in what you send, be liberal in what

    you accept” - Robustness Principle
  21. a) func New() *os.File b) func New() io.ReadWriteCloser c) func

    New() io.Writer d) func New() interface{} what function do you prefer?
  22. “Be conservative in what you send, be liberal in what

    you accept” - Robustness Principle
  23. Use interfaces to hide implementation details: - decouple implementation from

    API - easily switch between implementations / or provide multiple ones Hiding implementation details
  24. call dispatch Concrete types: static - known at compilation -

    very efficient - can’t intercept Abstract types: dynamic - unknown at compilation - less efficient - easy to intercept
  25. type Client struct { Transport RoundTripper … } type RoundTripper

    interface { RoundTrip(*Request) (*Response, error) } interfaces: dynamic dispatch of calls
  26. interfaces: dynamic dispatch of calls type headers struct { rt

    http.RoundTripper v map[string]string } func (h headers) RoundTrip(r *http.Request) *http.Response { for k, v := range h.v { r.Header.Set(k, v) } return h.rt.RoundTrip(r) }
  27. interfaces: dynamic dispatch of calls c := &http.Client{ Transport: headers{

    rt: http.DefaultTransport, v: map[string]string{“foo”: “bar”}, }, } res, err := c.Get(“http://golang.org”)
  28. const input = `H4sIAAAAAAAA/7q+8vqc61Ovz1W4Pun61OtLrq+4vpALEAAA//+1jQx/FAAAAA==` var r io.Reader = strings.NewReader(input) r

    = base64.NewDecoder(base64.StdEncoding, r) r, err := gzip.NewReader(r) if err != nil {log.Fatal(err) } io.Copy(os.Stdout, r) Chaining interfaces
  29. - writing generic algorithms - hiding implementation details - providing

    interception points why do we use interfaces?
  30. package parse func Parse(s string) *Func type Func struct {

    … } func (f *Func) Eval(x float64) float64 Two packages: parse and draw
  31. Two packages: parse and draw package draw import “.../parse” func

    Draw(f *parse.Func) image.Image { for x := minX; x < maxX; x += incX { paint(x, f.Eval(y)) } … }
  32. Two packages: parse and draw package draw import “.../parse” func

    Draw(f *parse.Func) image.Image { for x := minX; x < maxX; x += incX { paint(x, f.Eval(y)) } … }
  33. Two packages: parse and draw package draw type Evaler interface

    { Eval(float64) float64 } func Draw(e Evaler) image.Image { for x := minX; x < maxX; x += incX { paint(x, e.Eval(y)) } … }
  34. func do(v interface{}) { i := v.(int) // will panic

    if v is not int i, ok := v.(int) // will return false } type assertions from interface to concrete type
  35. func do(v interface{}) { switch v.(type) { case int: fmt.Println(“got

    int %d”, v) default: } } type assertions from interface to concrete type
  36. func do(v interface{}) { switch t := v.(type) { case

    int: // t is of type int fmt.Println(“got int %d”, t) default: // t is of type interface{} fmt.Println(“not sure what type”) } } type assertions from interface to concrete type
  37. func do(v interface{}) { s := v.(fmt.Stringer) // might panic

    s, ok := v.(fmt.Stringer) // might return false } type assertions from interface to interface
  38. func do(v interface{}) { switch v.(type) { case fmt.Stringer: fmt.Println(“got

    Stringer %v”, v) default: } } type assertions from interface to interface
  39. func do(v interface{}) { select s := v.(type) { case

    fmt.Stringer: // s is of type fmt.Stringer fmt.Println(s.String()) default: // s is of type interface{} fmt.Println(“not sure what type”) } } type assertions from interface to interface
  40. Many packages check whether a type satisfies an interface: -

    fmt.Stringer : implement String() string - json.Marshaler : implement MarshalJSON() ([]byte, error) - json.Unmarshaler : implement UnmarshalJSON([]byte) error - ... and adapt their behavior accordingly. Tip: Always look for exported interfaces in the standard library. type assertions as extension mechanism
  41. type Context interface { Done() <-chan struct{} Err() error Deadline()

    (deadline time.Time, ok bool) Value(key interface{}) interface{} } var Canceled, DeadlineExceeded error the Context interface
  42. var Canceled = errors.New("context canceled") var DeadlineExceeded error = deadlineExceededError{}

    type deadlineExceededError struct{} func (deadlineExceededError) Error() string { return "..." } func (deadlineExceededError) Timeout() bool { return true } func (deadlineExceededError) Temporary() bool { return true } errors in context
  43. var Canceled = errors.New("context canceled") var DeadlineExceeded error = deadlineExceededError{}

    type deadlineExceededError struct{} func (deadlineExceededError) Error() string { return "..." } func (deadlineExceededError) Timeout() bool { return true } func (deadlineExceededError) Temporary() bool { return true } errors in context
  44. var Canceled = errors.New("context canceled") var DeadlineExceeded error = deadlineExceededError{}

    type deadlineExceededError struct{} func (deadlineExceededError) Error() string { return "..." } func (deadlineExceededError) Timeout() bool { return true } func (deadlineExceededError) Temporary() bool { return true } errors in context
  45. if tmp, ok := err.(interface { Temporary() bool }); ok

    { if tmp.Temporary() { // retry } else { // report } } errors in context
  46. Adding methods to an interface breaks backwards compatibility. type ResponseWriter

    interface { Header() Header Write([]byte) (int, error) WriteHeader(int) } How could you add one more method without breaking anyone’s code? type assertions as evolution mechanism
  47. Step 1: add the method to your concrete type implementations

    Step 2: define an interface containing the new method Step 3: document it type assertions as evolution mechanism
  48. type Pusher interface { Push(target string, opts *PushOptions) error }

    func handler(w http.ResponseWriter, r *http.Request) { if p, ok := w.(http.Pusher); ok { p.Push(“style.css”, nil) } } http.Pusher
  49. Interfaces provide: - generic algorithms - hidden implementation - interception

    points Implicit satisfaction: - break dependencies In conclusion
  50. Interfaces provide: - generic algorithms - hidden implementation - interception

    points Implicit satisfaction: - break dependencies Type assertions: - to extend behaviors - to classify errors - to maintain compatibility In conclusion
  51. Type Parameters A new draft was proposed to support generic

    programming in Go. It adds type parameters to: - functions - types
  52. Type Parameters on types Interfaces are not always enough for

    container types: • container/heap ◦ Provides only high level functions (Push, Pop, etc) ◦ Requires the user to implement the Heap interface • container/ring ◦ Provides a type Element containing a interface{} ◦ Requires constant type conversions, loses compile time checks.
  53. type Stack(type T) []T func (s Stack(T)) Push(v T) Stack(T)

    { … } func (s Stack(T)) Top() T { … } func (s Stack(T)) Pop() Stack(T) { … } func (s Stack(T)) Empty() bool { … } Type Parameters on types
  54. func Max(type T)(vs []T) T { var max T for

    _, v := range vs { if v > max { max = v } } return max } A generic Max function
  55. A generic Max function Max(int)([]int{1, 3, 2}) // 3 Max(float64)([]float64{1.0,

    3.0, 2.0}) // 3.0 Max(string)([]string{"a", "c", "b"}) // "c" Max(bool)([]bool{true, false}) // ? Max([]byte)([][]byte{{1, 2}, {2, 1}}) // ?
  56. Type contracts provides a set of constraints on types. Sounds

    familiar? They look like functions: where each operation becomes a constraint. Type Contracts
  57. contract comparable(v T) { var b bool = v <

    v } func Max(type T comparable)(vs []T) T { … } Max([]int)([]int{1, 3, 2}) // 3 Max([]string)([]string{"a", "c", "b"}) // "c" A comparable type
  58. contract length(v T) { var n int = len(v) }

    func Length(type T length)(x T) int { return len(x) } Length([]int)([]int{1, 2, 3}) // 3 Length(map[int]int)(map[int]int{1: 1}) // 1 A type with length
  59. Interfaces and contracts define sets of types. Interfaces constrain on

    methods, contracts on anything. → all interfaces can be represented as contracts contract Stringer(x T) { type Stringer interface { var s string = x.String() String() string } } Interfaces vs Contracts
  60. Interfaces constrain on methods, contracts on anything. → not all

    contracts can be represented as interfaces contract comparable(v T) { var b bool = v < v } contract comparable(v T) { var b bool = v.Equal(v) } Interfaces vs Contracts
  61. Contracts are great, but Once instantiated they are concrete types,

    so unlike interfaces: • They hide no implementation • They don’t provide dynamic dispatching This is has some good effects: • the type system once compiled doesn’t change. • there’s no change to the reflect package.
  62. Remember the comparable contract? contract comparable(v T) { var b

    bool = v.Equal(v) } What is the type of the Equal method? • func (v T) Equal(x T) bool • func (v *T) Equal(x T) bool • func (v T) Equal(x interface{}) bool contracts are probably too smart
  63. Interfaces provide: - generic algorithms - hidden implementation - interception

    points Implicit satisfaction: - break dependencies Type assertions: - to extend behaviors - to classify errors - to maintain compatibility Contracts would provide: - generic algorithms - generic types Libraries for: - Concurrency patterns - Functional programming Remove duplication: - int, int32, int64 … float32, float64 … - bytes vs strings contracts vs interfaces
  64. Conclusion Interfaces are amazing and I still love them <3

    Type parameters are great! • A library of concurrency patterns? • Functional programming in Go? Contracts are interesting, but probably *too smart* for Go. I hope to see new iterations of the draft simplifying the concepts.