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

Row, Row, Row Your Go

Row, Row, Row Your Go

A discussion of polymorphism in Go for the St. Louis Go Meetup (2019-05-22)

Rebecca Skinner

May 22, 2019
Tweet

More Decks by Rebecca Skinner

Other Decks in Technology

Transcript

  1. Row, Row, Row your Go
    Design Patterns for Structural Polymorphism in Go
    Rebecca Skinner
    @cercerilla
    May 22, 2019
    1

    View Slide

  2. CC-BY-SA 4.0
    2

    View Slide

  3. 1
    TLDR
    3

    View Slide

  4. Ad Hoc Composable Interfaces
    You can define ad-hoc interfaces in Go, and use them for input
    or output parameters of functions.
    4

    View Slide

  5. Ad Hoc Composable Interfaces
    You can define ad-hoc interfaces in Go, and use them for input
    or output parameters of functions.
    type A interface { }
    type B interface { }
    func takesAB ( x interface {A; B} ) { }
    func returnsAB ( ) interface {A; B} {
    return struct { } { } }
    4

    View Slide

  6. Interfaces For Field Access
    You can also use interfaces to allow access to aribtrary nested
    fields in a structure. This works with embedded structures too.
    5

    View Slide

  7. Interfaces For Field Access
    You can also use interfaces to allow access to aribtrary nested
    fields in a structure. This works with embedded structures too.
    type T struct {
    A int
    B string
    F func ( int ) string
    }
    type HasA interface {A( ) int }
    type HasF interface {F ( ) func ( int ) string }
    5

    View Slide

  8. Ad-Hoc Interfaces with Accessor Interfaces
    You can combine these into something that resembles
    structural polymorphism, and maybe even extend the idea to
    row types.
    6

    View Slide

  9. Ad-Hoc Interfaces with Accessor Interfaces
    You can combine these into something that resembles
    structural polymorphism, and maybe even extend the idea to
    row types.
    % func callF ( x interface {HasA ; HasF } ) string {
    return x . F ( ) ( x .A ( ) )
    }
    6

    View Slide

  10. Can We Run With This?
    Can We Extend This Idea Into
    Something Powerful?
    7

    View Slide

  11. Of Course We Can’t
    8

    View Slide

  12. frame
    Instead, Let’s Look At Why Go’s
    Type System Is Broken
    9

    View Slide

  13. 2
    The Polymorphism Problem
    10

    View Slide

  14. Making Simple Things Easy
    A good language should make easy simple things
    easy, and hard things possible.
    11

    View Slide

  15. But For Go
    Go feels like
    Simple Complex
    Easy ×
    Hard ×
    12

    View Slide

  16. 2.1
    An Identity Crisis
    13

    View Slide

  17. Simple. Easy. Impossible
    The identity function takes a value and returns it.
    14

    View Slide

  18. Why?
    The identity function can be very useful when working with
    high-order functions.
    15

    View Slide

  19. Silly Example
    % type idFunc = func ( int ) int
    func ReverseMapInt ( f idFunc , i n t s [ ] int ) [ ] int {
    out := [ ] int { }
    for _ , i := range i n t s {
    out = append ( [ ] int { f ( i ) } , out . . . )
    }
    return
    }
    func IntID ( i int ) int { return i }
    func IntSucc ( i int ) int { return i + 1 }
    func main ( ) {
    i n t s := [ ] int {1 , 2 , 3}
    fmt . Println ( ReverseMapInt ( IntSucc , i n t s ) )
    fmt . Println ( ReverseMapInt ( IntID , i n t s ) )
    } 16

    View Slide

  20. output
    % go run ∗. go
    [4 3 2]
    [3 2 1]
    17

    View Slide

  21. 2.2
    A Generic Problem
    18

    View Slide

  22. Problem
    Write an identity function that works for any input value.
    19

    View Slide

  23. % # ruby
    def id ( a ) return a ; end
    20

    View Slide

  24. % # ruby
    def id ( a ) return a ; end
    % # python
    from typing import TypeVar , Generic
    A = TypeVar ( ’A ’ )
    def id ( input : A) −> A:
    return input
    20

    View Slide

  25. % # ruby
    def id ( a ) return a ; end
    % # python
    from typing import TypeVar , Generic
    A = TypeVar ( ’A ’ )
    def id ( input : A) −> A:
    return input
    % −
    − haskell
    id a = a
    20

    View Slide

  26. % # ruby
    def id ( a ) return a ; end
    % # python
    from typing import TypeVar , Generic
    A = TypeVar ( ’A ’ )
    def id ( input : A) −> A:
    return input
    % −
    − haskell
    id a = a
    % (∗ ocaml ∗)
    l e t id a = a ; ;
    20

    View Slide

  27. % # ruby
    def id ( a ) return a ; end
    % # python
    from typing import TypeVar , Generic
    A = TypeVar ( ’A ’ )
    def id ( input : A) −> A:
    return input
    % −
    − haskell
    id a = a
    % (∗ ocaml ∗)
    l e t id a = a ; ;
    % / / rust
    fn id ( input : A) −> A { return input ; }
    20

    View Slide

  28. % # ruby
    def id ( a ) return a ; end
    % # python
    from typing import TypeVar , Generic
    A = TypeVar ( ’A ’ )
    def id ( input : A) −> A:
    return input
    % −
    − haskell
    id a = a
    % (∗ ocaml ∗)
    l e t id a = a ; ;
    % / / rust
    fn id ( input : A) −> A { return input ; }
    % (∗ sml ∗)
    fun id ( x : ’ a ) : ’ a = x ;
    20

    View Slide

  29. % # ruby
    def id ( a ) return a ; end
    % # python
    from typing import TypeVar , Generic
    A = TypeVar ( ’A ’ )
    def id ( input : A) −> A:
    return input
    % −
    − haskell
    id a = a
    % (∗ ocaml ∗)
    l e t id a = a ; ;
    % / / rust
    fn id ( input : A) −> A { return input ; }
    % (∗ sml ∗)
    fun id ( x : ’ a ) : ’ a = x ;
    % / / Javascript
    function id ( x ) { return x ; }
    20

    View Slide

  30. % # ruby
    def id ( a ) return a ; end
    % # python
    from typing import TypeVar , Generic
    A = TypeVar ( ’A ’ )
    def id ( input : A) −> A:
    return input
    % −
    − haskell
    id a = a
    % (∗ ocaml ∗)
    l e t id a = a ; ;
    % / / rust
    fn id ( input : A) −> A { return input ; }
    % (∗ sml ∗)
    fun id ( x : ’ a ) : ’ a = x ;
    % / / Javascript
    function id ( x ) { return x ; }
    % / / Typescript
    function id (x : T ) : T { return x ; }
    20

    View Slide

  31. % # ruby
    def id ( a ) return a ; end
    % # python
    from typing import TypeVar , Generic
    A = TypeVar ( ’A ’ )
    def id ( input : A) −> A:
    return input
    % −
    − haskell
    id a = a
    % (∗ ocaml ∗)
    l e t id a = a ; ;
    % / / rust
    fn id ( input : A) −> A { return input ; }
    % (∗ sml ∗)
    fun id ( x : ’ a ) : ’ a = x ;
    % / / Javascript
    function id ( x ) { return x ; }
    % / / Typescript
    function id (x : T ) : T { return x ; }
    % / / Java
    class I d e n t i t y { public static T id (T x ) { return x ; } }
    20

    View Slide

  32. And in Go
    21

    View Slide

  33. Polymorphism
    Go lacks the ability to directly express this type of
    polymorphism. Our only choices are to avoid this type of
    constructor, or else to fall back to using go generate or working
    with interface{}.
    22

    View Slide

  34. Reflection
    Reflection and interface{} give us an escape hatch of sorts
    from the type system. Most of the problems in this talk can be
    “solved” by lifting programs into the unityped space, but we will
    avoid this due to the difficulties in working with excessively
    reflective code.
    23

    View Slide

  35. 3
    The Many Shapes of Polymorphism
    24

    View Slide

  36. Types of Polymorphism
    This is just an example of one type of polymorphism, let’s look
    at a few others that are very common in modern languages:
    25

    View Slide

  37. 3.1
    Parametric Polymorphism
    26

    View Slide

  38. Parametric Polymorphism
    Parametric polymorphism is what we usually think of when we
    think of polymorphism. It let’s us write functions that can work
    for any type of value. For example:
    27

    View Slide

  39. Parametric Polymorphism
    Parametric polymorphism is what we usually think of when we
    think of polymorphism. It let’s us write functions that can work
    for any type of value. For example:
    • id
    27

    View Slide

  40. Parametric Polymorphism
    Parametric polymorphism is what we usually think of when we
    think of polymorphism. It let’s us write functions that can work
    for any type of value. For example:
    • id
    • Higher order functions like map and fold
    27

    View Slide

  41. Parametric Polymorphism
    Parametric polymorphism is what we usually think of when we
    think of polymorphism. It let’s us write functions that can work
    for any type of value. For example:
    • id
    • Higher order functions like map and fold
    • Higher Ranked Polymorphic Functions (e.g. polymorphic
    quantification over closures)
    27

    View Slide

  42. Parametric Polymorphism
    Parametric polymorphism is what we usually think of when we
    think of polymorphism. It let’s us write functions that can work
    for any type of value. For example:
    • id
    • Higher order functions like map and fold
    • Higher Ranked Polymorphic Functions (e.g. polymorphic
    quantification over closures)
    • Generic data structures
    27

    View Slide

  43. Generics as Parametric Polymorphism
    In properly parametric polymorphism, the type of the function is
    quantified over a set of types. Template generics don’t work
    that way since semantically they are more like
    metaprogramming to generate overloaded function, but in
    practice they fill a similar role in many languages.
    % func id ( type T ) ( val T) T { return val ; }
    28

    View Slide

  44. Return Type Polymorphism
    Return type polymorphism is a special case of parametric
    polymorphism where we determine which implementation of
    our function to use based on a return value type.
    % func parse ( type T ) ( unparsed string ) (T , error ) ;
    29

    View Slide

  45. 3.2
    Ad-Hoc Polymorphism
    30

    View Slide

  46. Ad-Hoc Polymorphism
    Ad-hoc polymorphism allows us to have different
    implementations of a function for different types. Function
    overloading is the typical example of ad-hoc polymorphism.
    31

    View Slide

  47. Interfaces As Polymorphism
    Interfaces are a sort of ad-hoc polymorphism that is useful and
    prevelant in idiomatic go code.
    32

    View Slide

  48. Postels Law
    Be conservative in what you send, and liberal in what
    you accept.
    33

    View Slide

  49. The Robustness Principle for APIs
    Accept interfaces and return concrete types.
    34

    View Slide

  50. Interface Based Ad Hoc Polymorphism
    % type AdHoc interface {AdHoc ( ) string }
    type A struct { }
    func ( a ∗A) AdHoc ( ) string { return "A. AdHoc" }
    type B struct { }
    func ( b ∗B) AdHoc ( ) string { return "B. AdHoc" }
    func polymorphic ( adhoc AdHoc) string {
    return adhoc . AdHoc ( )
    }
    func main ( ) {
    fmt . Println ( polymorphic ( ( ∗A) ( n i l ) ) )
    fmt . Println ( polymorphic ( ( ∗B) ( n i l ) ) )
    }
    35

    View Slide

  51. Compare With Haskell
    % class AdHoc a where adHoc : : Proxy a −> String
    data A
    data B
    instance AdHoc A where adHoc = const "adHoc @A"
    instance AdHoc B where adHoc = const "adHoc @B"
    polymorphic : : AdHoc a => Proxy a −> String
    polymorphic = adHoc
    main : : IO ( )
    main = do
    l e t a = Proxy @A
    b = Proxy @B
    putStrLn ( polymorphic a )
    putStrLn ( polymorphic b )
    36

    View Slide

  52. Limits of Go’s Interfaces
    The Limit’s of Go’s Interfaces
    37

    View Slide

  53. Return Type Polymorphism
    % class FromStr a where fromStr : : String −> a
    data A = A deriving Show
    instance FromStr A where fromStr = const A
    newtype B = B { runB : : Int } deriving (Show, Num)
    instance FromStr B where fromStr = const (B 0)
    main = do
    print $ fromStr @A " hello "
    print $ ( fromStr @B " world " ) + 1
    38

    View Slide

  54. In Go
    % type FromStr interface { fromStr ( string ) FromStr }
    type A struct { }
    func ( a A) fromStr ( s string ) FromStr { return A{ } }
    type B struct { x int }
    func ( b B) AddNum( y int ) B {
    return B{ b . x + y }
    }
    func ( b B) fromStr ( s string ) FromStr {
    return B{0}
    }
    func main ( ) {
    fmt . Println (A { } . fromStr ( " hello " ) )
    fmt . Println (B{ 0 } . fromStr ( " world " ) . AddNum( 1 ) )
    }
    39

    View Slide

  55. And in Go
    40

    View Slide

  56. Return Type Polymorphism
    % class Addable a where add : : a −> a −> a
    instance Addable Int where add = (+)
    data Point2D = Point2D { _x : : Int
    , _y : : Int } deriving Show
    instance Addable Point2D where
    add ( Point2D x1 y1 ) ( Point2D x2 y2 ) =
    Point2D ( x1 + x2 ) ( y1 + y2 )
    type ShowAdd a = (Show a , Addable a )
    polymorphic : : (ShowAdd a ) => a −> a −> String
    polymorphic a b = show ( add a b )
    main = do
    l e t a = 1 : : Int ; b = 2 : : Int
    putStrLn $ polymorphic a b
    l e t a = Point2D 1 2; b = Point2D 3 5
    putStrLn $ polymorphic a b 41

    View Slide

  57. 3.3
    Subtype Polymorphism
    42

    View Slide

  58. Subtype Polymorphism
    In subtype polymorphism we have a partial ordering over
    interfaces.
    A1≤:B1 A2≤:B2
    B1→A2≤:A1→B2
    43

    View Slide

  59. Interface Embedding as Subtype Polymorphism
    % type B1 interface { }
    type A1 interface {B1}
    type B2 interface { }
    type A2 interface {B2}
    type F1 interface { f1 (B1) A2}
    type F2 interface { f2 (A1) B2}
    func f2_from_f1 ( b1 A1) B2 { }
    44

    View Slide

  60. 4
    An Introduction to Row Types and Go
    45

    View Slide

  61. What Are Row Types?
    What are row types?
    46

    View Slide

  62. What Are Row Types?
    Row types provide a polymorphic abstraction over records.
    They allow us to express operations over a set of fields.
    47

    View Slide

  63. What Are Row Types?
    Row types provide a polymorphic abstraction over records.
    They allow us to express operations over a set of fields.
    τ : {ℓ0 : τ0, ℓ1 : τ1, ρ}
    47

    View Slide

  64. So What Are Row Types?
    So What Are Row Types?
    48

    View Slide

  65. So What Are Row Types?
    Row polymorphism is a way to make functions that are
    polymorphic over the fields in a record. On the surface, it’s
    similar to structural subtyping, since it allows us to abstract over
    the structure of a record.
    49

    View Slide

  66. Structural Typing
    Structural Typing?
    Go Can Do That
    50

    View Slide

  67. Structural Typing
    Structural typing uses the “shape” of a value to infer it’s type.
    We’ve seen a lot of examples of this pattern already.
    51

    View Slide

  68. Structural Typing
    Structural typing uses the “shape” of a value to infer it’s type.
    We’ve seen a lot of examples of this pattern already.
    interfaces!
    51

    View Slide

  69. Recall
    Remember
    52

    View Slide

  70. Ad Hoc Composable Interfaces
    You can define ad-hoc interfaces in Go, and use them for input
    or output parameters of functions.
    53

    View Slide

  71. Ad Hoc Composable Interfaces
    You can define ad-hoc interfaces in Go, and use them for input
    or output parameters of functions.
    % type A interface { }
    type B interface { }
    func takesAB ( x interface {A; B} ) { }
    func returnsAB ( ) interface {A; B} {
    return struct { } { } }
    53

    View Slide

  72. Interfaces For Field Access
    You can also use interfaces to allow access to aribtrary nested
    fields in a structure. This works with embedded structures too.
    54

    View Slide

  73. Interfaces For Field Access
    You can also use interfaces to allow access to aribtrary nested
    fields in a structure. This works with embedded structures too.
    type T struct {
    A int
    B string
    F func ( int ) string
    }
    type HasA interface {A( ) int }
    type HasF interface {F ( ) func ( int ) string }
    54

    View Slide

  74. Ad-Hoc Interfaces with Accessor Interfaces
    You can combine these into something that resembles
    structural polymorphism, and maybe even extend the idea to
    row types.
    55

    View Slide

  75. Ad-Hoc Interfaces with Accessor Interfaces
    You can combine these into something that resembles
    structural polymorphism, and maybe even extend the idea to
    row types.
    func callF ( x interface {HasA ; HasF } ) string {
    return x . F ( ) ( x .A ( ) )
    }
    55

    View Slide

  76. 4.1
    From Structural Types to Row Types
    56

    View Slide

  77. The Difference between Structural and Row Types
    Structural types seem similar to row types on the surface, but
    there are some differences.
    57

    View Slide

  78. Quantification
    Row types should support quanitification over fields. It’s not
    sufficent to have: {ℓ0 : int, ℓ1 : string} Instead we need to be
    able to express the notion of interfaces over fields.
    58

    View Slide

  79. Field Level Constraints
    func rowFunc ( x interface {
    GetF ( ) interface { AsInt ( ) int } } ) int {
    return x . GetF ( ) . AsInt ( )
    }
    func main ( ) {
    fmt . Println ( rowFunc ( Foo { } ) )
    }
    59

    View Slide

  80. 5
    The Fundamental Problem
    60

    View Slide

  81. Type Erasure
    Interfaces ̸= Constraints
    Even when we can construct appropriate constraints based on
    our ad-hoc and structural polymorphism, the ergonomics are
    poor due to the differences between interfaces and constraints.
    61

    View Slide

  82. Interfaces are Surjective
    Lifting values to interfaces is an epimorphism. Without
    resorting to runtime type reflection we don’t know what type we
    put into an interface:
    type A struct { }
    type B struct { }
    type C interface { }
    func ifaceFunc ( c C) C { return c }
    func main ( ) {
    fmt . Println ( ifaceFunc (A { } ) )
    fmt . Println ( ifaceFunc (B { } ) )
    }
    62

    View Slide

  83. Non-Constructive Interfaces
    Interfaces can’t construct values of the type that implement
    them. This limits us to interfaces that work on already
    constructed values.
    63

    View Slide

  84. 6
    Summary
    64

    View Slide

  85. Summary
    • There are a lot of different types of polymorphism
    • Go’s interfaces give us a lot of them
    • Go’s type system is a lot more flexible than you might thing
    • But doing interesting stuff has poor ergonomics
    • Generics would resolve a lot of these problems
    • But even syntactic sugar over a few of these idioms would
    make go more usable
    65

    View Slide

  86. 7
    Questions?
    66

    View Slide