Go: Methods and Interfaces (Overloading and Structural Subtyping)

Martin Sulzmann

Overview

Methods

We can define methods on named types where we can use the “dot” notation to call methods.

Named types

type rectangle struct {
    length int
    width  int
}

Named types are types defined via type. Via the type keyword we introduce a (fresh) name for an existing type.

Structurally rectangle and the (unamed) type

struct {
    length int
    width  int
}

are the same but rectangle is a new type.

Hence, we cannot assign a variable of the above (unamed) type to a variable of type rectangle (and vice versa).

    var r rectangle
    var r1 struct {
        length int
        width  int
    }

    // r = r1  // NOT OK
    r = (rectangle)(r1)  // OK cast required

Method definitions

func (r rectangle) area() int {
    return r.length * r.width
}

The named type argument preceeds all other arguments (in this example, area has no further arguments). There is no self or this. Struct values are always referenced by name.

Method calls

We call methods by using the “dot” notation.

r.area()

In the above definition of method area on named type rectangle, the argument r is passed to area as a value. We must use call-by reference if we wish to update r’s field values.

Methods and call-by reference

func (r *rectangle) scale(s int) {
    r.length = r.length * s
    r.width = r.width * s
}

The * indicates that we pass r to scale by reference. Thus, the update is globally visible.

In Go, the compiler inserts the * and & operator automatically. For example,

r.scale(3)
(&r).scale(3)

The above statements are equivalent. Go will automatically perform the conversion.

Complete example

package main

import "fmt"

type rectangle struct {
    length int
    width  int
}

func (r rectangle) area() int {
    return r.length * r.width
}

func (r *rectangle) scale(s int) {
    r.length = r.length * s
    r.width = r.width * s
}

func main() {
    var r1 rectangle = rectangle{1, 2}
    var r2 rectangle = rectangle{width: 2, length: 1}
    r3 := rectangle{width: 2, length: 1}
    r3.scale(3)

    fmt.Printf("%d \n", r1.area()+r2.area()+r3.area())

}

Overloaded methods

Go supports method overloading where method dispatch is based on the “receiver” type. The receiver is simply the object on which the method is called.

type rectangle struct {
    length int
    width  int
}

type square struct {
    length int
}


func (r rectangle) area() int {
    return r.length * r.width
}

func (r square) area() int {
    return r.length * r.length
}

Limitations

Suppose we have several geometric objects (such as rectangle and square). Each geometric object supports the area method.

We wish to sum up the area of an arbitrary object. Our current best bet is to enumerate all the cases.

func sum1(r1 rectangle, r2 rectangle) int {
    return r1.area() + r2.area()
}

func sum2(r rectangle, s square) int {
    return r.area() + s.area()
}

func sum3(s square, r rectangle) int {
    return r.area() + s.area()
}

func sum4(s1 square, s2 square) int {
    return s1.area() + s2.area()
}

This is rather tedious. There is lots of code duplication and the programmer needs to manually select the appropriate “sum” function. As we will see shortly, interfaces allows us to provide for a common interface to objects that support common methods.

Interfaces and structural subtyping

Interface:

Structural subtyping:

Subtyping principle:

Geometric shapes example

type shape interface {
    area() int
}

The receiver is left implicit in the interface declaration.

Interfaces are types.

func sumArea(x, y shape) int {
    return x.area() + y.area()
}

What are possible values of type shape?

Any value of type s that is a structural subtype of shape? Any two values passed to the sumArea function must s

Recall that rectangle and square both implement the area method.

func (r rectangle) area() int {
    return r.length * r.width
}

func (s square) area() int {
    return s.length * s.length
}

Hence, values of type rectangle and square are also of the interface type shape. That is, we find

For example, consider the following.

    var r rectangle = rectangle{1, 2}
    var s square = square{3}

    x2 := sumArea(r, s)  // applied on (rectangle, square)
    x2b := sumArea(r, r) // applied on (rectangle, rectangle)

    fmt.Printf("%d %d \n", x2, x2b)

We say that sumArea is a generic function because sumArea can be applied on values of different types.

Geometric shapes that scale

An interface may consist of several (overloaded) methods. We can also extend an existing interface.

type shapeExt interface {
    shape
    scale()
}

Here is a function that makes use of the extended shape interface.

func sumAreaScaleBefore(n int, x, y shapeExt) int {
    x.scale(n)
    y.scale(n)
    return x.area() + y.area()
}

Can we call sumAreaScaleBefore with rectangles and squares?

Recall the earlier method definitions.

func (r rectangle) area() int {     // A1
    return r.length * r.width
}

func (s square) area() int {        // A2
    return s.length * s.length
}

func (r *rectangle) scale(x int) {  // S1
    r.length = r.length * x
    r.width = r.width * x
}

func (s *square) scale(x int) {     // S2
    s.length = s.length * x
}

What about the following?

sumAreaScaleBefore(3, r, s)

This will not compile and yields a type error.

The following works!

sumAreaScaleBefore(3, &r, &s)

Why?

We can argue that *rectangle <= shapeExt and *square <= shapeExt for the following reason

  1. *rectangle implements the scale method, see S1

  2. rectangle implements the area method, see A1

  3. Any value receiver definition implies a pointer receiver.

  4. Hence, *rectangle implements the area method as well.

  5. Hence, *rectangle <= shapeExt

Regarding 3. Consider

    var rPtr *rectangle = &r
    rPtr.area()

The above method call will be transformed to

    (*rPtr).area()

Hence, we can argue that *rectangle (pointer to rectangle) the area method as well.

Structural subtyping boils down to set membership

Among interfaces, the subtype relation is structural. Hence, the following interface declarations are all equivalent.

type shapeExt interface {
    shape
    scale(int)
}

type shapeExt2 interface {
    scale(int)
    shape
}

type shapeExt3 interface {
    scale(int)
    area() int
}

type shapeExt4 interface {
    area() int
    scale(int)
}

Complete example

package main

import "fmt"

// Example

type rectangle struct {
    length int
    width  int
}

type square struct {
    length int
}

func (r rectangle) area() int {
    return r.length * r.width
}

func (s square) area() int {
    return s.length * s.length
}

func (r *rectangle) scale(x int) {
    r.length = r.length * x
    r.width = r.width * x
}

func (s *square) scale(x int) {
    s.length = s.length * x
}

type shape interface {
    area() int
}

type shapeExt interface {
    shape
    scale(int)
}

func sumArea(x, y shape) int {
    return x.area() + y.area()
}

func sumAreaScaleBefore(n int, x, y shapeExt) int {
    x.scale(n)
    y.scale(n)
    return x.area() + y.area()
}

func test() {
    var r rectangle = rectangle{1, 2}
    var s square = square{3}
    var rPtr *rectangle = &r

    x0 := rPtr.area()
    // (&rPtr).area()
    fmt.Printf("%d \n", x0)

    x1 := r.area() + s.area()

    fmt.Printf("%d \n", x1)

    x2 := sumArea(r, s)  // applied on (rectangle, square)
    x2b := sumArea(r, r) // applied on (rectangle, rectangle)

    fmt.Printf("%d %d \n", x2, x2b)

    pt := &r

    x3 := pt.area()
    // Implicit conversion to
    // (*pt).area()
    //
    // Hence, any "value" receiver also implies the corresponding "pointer" receiver.

    fmt.Printf("%d \n", x3)

    //  x3 := sumAreaScaleBefore(3, r, s)
    //
    // "rectangle does not implement shapeExt (scale method has pointer receiver)"
    // same applies to square

    x4 := sumAreaScaleBefore(3, &r, &s)
    // Works because any method definition for a value,
    // implies a method definition for the pointer.

    fmt.Printf("%d \n", x4)

}

func main() {

    test()

}

Structural subyping versus nominal subtyping (Java)

// Java example (pseudo-code)
//////////////////////////////////////

class Shape {
  int area();
}

class ShapeExt extends Shape {
 scale(int);
}

class ShapeExt2 extends Shape {
 scale(int);
}

From the above class declarations we derive

where <= denotes the subtype relation.

This form of subtyping is referred to as nominal subtyping. The subtype relations are explicitly declared via class hierarchies.

“Any” Interface and type assertions

    interface{}

The “any” interface. For any type t we find that t <= interface{}. This is similar to the type Object in Java.

A value of type interface{} can be anything. So, we need to perform some run-time type casts to access the actual value.

func any(anything interface{}) {
    switch v := anything.(type) {
    case int:
        fmt.Printf("some int %d \n", v)
    case rectangle:
        fmt.Println(v)
        r := anything.(rectangle)
        fmt.Printf("length = %d, width = %d \n", r.length, r.width)
    default:
        fmt.Println("don't know")
    }

}

We can also cast to a specific type, see anything.(rectangle)

Such a cast may fail

We can catch failure via

    r, ok := anything.(rectangle)

ok equals false in case the cast fails

BTW, Go automatically performs a break after each case.

Why would you need interface{} in Go (or Object in Java)?

Complete example

package main

import "fmt"

type rectangle struct {
    length int
    width  int
}

func any(anything interface{}) {
    switch v := anything.(type) {
    case int:
        fmt.Printf("some int %d \n", v)
    case rectangle:
        fmt.Println(v)
        r := anything.(rectangle)
        fmt.Printf("length = %d, width = %d \n", r.length, r.width)
    default:
        fmt.Println("don't know")
    }

}

func main() {
    any(1)

    any(rectangle{1, 2})

}

Translating methods, interfaces and structural subtyping

Translating overloaded methods

Each method definition can be uniquely identified via

Translating method definitions is easy. Simply introduce unique function names.

For our running example, we find the following.

func area_Rec(r rectangle) int {
    return r.length * r.width
}

func area_Sq(s square) int {
    return s.length * s.length
}

Translating interfaces and structural subtying

How to translate generic functions that make use of interfaces? Recall

func sumArea(x, y shape) int {
    return x.area() + y.area()
}

Run-time method lookup

Dictionary translation (inspired by Haskell’s type class translation scheme)

Both translation methods rely on interface{}!

Run-time method lookup (by example)

func area_Lookup(x interface{}) int {
    var y int

    switch v := x.(type) {
    case square:
        y = area_Sq(v)
    case rectangle:
        y = area_Rec(v)
    }
    return y

}

func sumArea_Lookup(x, y interface{}) int {
    return area_Lookup(x) + area_Lookup(y)
}

Dictionary translation (by example)

type shape_Value struct {
    val  interface{}
    area func(interface{}) int
}

func sumArea_Dict(x, y shape_Value) int {
    return x.area(x.val) + y.area(y.val)
}

Calling sumArea

    area_Rec_Wrapper := func(v interface{}) int {
        return area_Rec(v.(rectangle))

    }

    area_Sq_Wrapper := func(v interface{}) int {
        return area_Sq(v.(square))

    }

    rDictShape := shape_Value{r, area_Rec_Wrapper}

    sDictShape := shape_Value{s, area_Sq_Wrapper}

    sumArea_Dict(rDictShape, sDictShape)

Wrapper functions are needed for the following reason.

Complete source

package main

import "fmt"

// Example

type rectangle struct {
    length int
    width  int
}

type square struct {
    length int
}

func (r rectangle) area() int {
    return r.length * r.width
}

func (s square) area() int {
    return s.length * s.length
}

func (r *rectangle) scale(x int) {
    r.length = r.length * x
    r.width = r.width * x
}

func (s *square) scale(x int) {
    s.length = s.length * x
}

type shape interface {
    area() int
}

type shapeExt interface {
    shape
    scale(int)
}

func sumArea(x, y shape) int {
    return x.area() + y.area()
}

func sumAreaScaleBefore(n int, x, y shapeExt) int {
    x.scale(n)
    y.scale(n)
    return x.area() + y.area()
}

func test() {
    var r rectangle = rectangle{1, 2}
    var s square = square{3}

    x1 := r.area() + s.area()

    fmt.Printf("%d \n", x1)

    x2 := sumArea(r, s)

    fmt.Printf("%d \n", x2)

    pt := &r

    x3 := pt.area()
    // Implicit conversion to
    // (*pt).area()
    //
    // Hence, any "value" receiver also implies the corresponding "pointer" receiver.

    fmt.Printf("%d \n", x3)

    //  x3 := sumAreaScaleBefore(3, r, s)
    //
    // "rectangle does not implement shapeExt (scale method has pointer receiver)"
    // same applies to square

    x4 := sumAreaScaleBefore(3, &r, &s)

    fmt.Printf("%d \n", x4)

}

// Introducing unique function names for overloaded methods

func area_Rec(r rectangle) int {
    return r.length * r.width
}

func area_Sq(s square) int {
    return s.length * s.length
}

// "value" method implies "pointer" method
func area_RecPtr(r *rectangle) int {
    return area_Rec(*r)
}

func area_SqPtr(s *square) int {
    return area_Sq(*s)
}

func scale_RecPtr(r *rectangle, x int) {
    r.length = r.length * x
    r.width = r.width * x
}

func scale_SqPtr(s *square, x int) {
    s.length = s.length * x
}

// Run-time method lookup

func area_Lookup(x interface{}) int {
    var y int

    switch v := x.(type) {
    case square:
        y = area_Sq(v)
    case rectangle:
        y = area_Rec(v)
    }
    return y

}

func sumArea_Lookup(x, y interface{}) int {
    return area_Lookup(x) + area_Lookup(y)
}

func test_Lookup() {

    var r rectangle = rectangle{1, 2}
    var s square = square{3}

    x1 := area_Rec(r) + area_Sq(s)

    fmt.Printf("%d \n", x1)

    x2 := sumArea_Lookup(r, s)
    // rectangle <= interface{}
    // square <= interface{}

    fmt.Printf("%d \n", x2)
}

// Dictionary translation

type shape_Value struct {
    val  interface{}
    area func(interface{}) int
}

type shapeExt_Value struct {
    val   interface{}
    area  func(interface{}) int
    scale func(interface{}, int)
}

// shapExt <= shape
func fromShapeExtToShape(x shapeExt_Value) shape_Value {
    return shape_Value{x.val, x.area}
}

func sumArea_Dict(x, y shape_Value) int {
    return x.area(x.val) + y.area(y.val)
}

func sumAreaScaleBefore_Dict(n int, x, y shapeExt_Value) int {
    x.scale(x.val, n)
    y.scale(y.val, n)
    return x.area(x.val) + y.area(y.val)
}

func test_Dict() {
    var r rectangle = rectangle{1, 2}
    var s square = square{3}

    // 1. Plain method calls

    x1 := area_Rec(r) + area_Sq(s)

    fmt.Printf("%d \n", x1)

    x2 := sumArea(r, s)

    fmt.Printf("%d \n", x2)

    pt := &r

    x3 := area_Rec(*pt)
    // Implicit conversion from pointer to value

    fmt.Printf("%d \n", x3)

    //  x3 := sumAreaScaleBefore(3, r, s)
    //
    // "rectangle does not implement shapeExt (scale method has pointer receiver)"
    // same applies to square

    // 2. Calling sumArea

    // Wrapper functions are needed for the following reason.
    // (a) area_Rec has type func(rectangle) int
    // (b) We need to store area_Rec in the "area" dictionary entry which has type func(interface{}) int
    // (c) We cast area_Rec to the approrpriate type

    area_Rec_Wrapper := func(v interface{}) int {
        return area_Rec(v.(rectangle))

    }

    area_Sq_Wrapper := func(v interface{}) int {
        return area_Sq(v.(square))

    }

    rDictShape := shape_Value{r, area_Rec_Wrapper}

    sDictShape := shape_Value{s, area_Sq_Wrapper}

    x4 := sumArea_Dict(rDictShape, sDictShape)

    fmt.Printf("%d \n", x4)

    // 3. Calling sumAreaScaleBefore

    area_RecPtr_Wrapper := func(v interface{}) int {
        return area_RecPtr(v.(*rectangle))

    }

    area_SqPtr_Wrapper := func(v interface{}) int {
        return area_SqPtr(v.(*square))

    }

    scale_RecPtr_Wrapper := func(v interface{}, x int) {
        scale_RecPtr(v.(*rectangle), x)

    }

    scale_SqPtr_Wrapper := func(v interface{}, x int) {
        scale_SqPtr(v.(*square), x)

    }

    // Construct the appropriate interface values
    rDictShapeExt := shapeExt_Value{&r, area_RecPtr_Wrapper, scale_RecPtr_Wrapper}

    sDictShapeExt := shapeExt_Value{&s, area_SqPtr_Wrapper, scale_SqPtr_Wrapper}

    x5 := sumAreaScaleBefore_Dict(3, rDictShapeExt, sDictShapeExt)

    fmt.Printf("%d \n", x5)

    // 4. Calling sumArea with a shapeExt value

    x6 := sumArea_Dict(fromShapeExtToShape(rDictShapeExt), fromShapeExtToShape(sDictShapeExt))

    fmt.Printf("%d \n", x6)
}

func main() {

    test()

    test_Lookup()

    test_Dict()

}

Gimmick: Variadic functions in Go

func sumAreas(shs ...shape) int {
    var a int = 0
    for _, elem := range shs {
        a = a + elem.area()
    }
    return a
}

We can define arguments with a variable number of arguments (of the some type) and iterate over them via a for loop.

In the above we don’t care (_) about the index position.

Variadic functions simply provide some syntactic sugar compared to using slices as arguments.

// Expects a list of shapes (slice).
// Pretty much the same compared to sumAreas.
// Only difference is that we first need to create a slice to call shapes2.
func sumAreas2(shs []shape) int {
    var a int = 0
    for _, elem := range shs {
        a = a + elem.area()
    }
    return a
}

Abstract data types via interfaces/methods

An abstract data type is defined in terms of its behavior. For example, operations performed on this type etc. Implementation details (of this type) are left abstract (hidden).

We build an abstract data type Set via interfaces. The below is an example where we use recursive interfaces. Recursive means that the interface type appears in some type declaration of some method.

package main

import "fmt"

type Set interface {
    empty() Set
    isEmpty() bool
    insert(int) Set
}

type SetImpl []int

func (xs SetImpl) empty() Set {
    return (SetImpl)([]int{})
}

func (xs SetImpl) isEmpty() bool {
    return len(xs) == 0
}

func (xs SetImpl) insert(x int) Set {
    ys := append(xs, x)
    return (SetImpl)(ys)
}

// This is a function, not a method.
func mkEmptySet() Set {
    xs := []int{}
    return (SetImpl)(xs)
}

func main() {
    // We consider sets of integers here
    s := SetImpl{1, 2, 3}

    fmt.Printf("%b", s.isEmpty())
}

Important.

Consider

func (xs SetImpl) empty() Set {
    return (SetImpl)([]int{})
}

We must apply the cast (SetImpl)([]int{}).

[]int{} is of type []int but this type is not compatible with Set.

SetImpl satisfies Set and therefore SetImplSetSetImpl \leq Set.

Hence, isEmpty returns a value of type Set.

Algebraic data types and pattern matching via interfaces/methods

Algebraic data types are build by composition via other types. Examples are tuples and structs. Tuples and structs correspond to AND. For example, consider

type Result struct {
    val int
    status bool
}

The struct Point consists of a value of type int and a value of type bool.

Further examples of algebraic data types are sum types (also referred to as disjoint union, variant type). Values of sum types are created via distinct constructors. Hence, a sum type corresponds to OR. Languages with support for sum types usually allow for a form of pattern matching via which we can analyse and extract the data connected to each value case of a sum type.

Go has no direct support for sum types and pattern matching. However, we can emulate both with the help of interfaces/methods.

Maybe (Option) type

The maybe type (also kown as the option type is typical example of a sum type. The maybe type supports two value cases: Either nothing (representing failure) or just something (representing success). In Go, the maybe type can be represented as follows.

type Nothing struct{}

type Just struct {
    val int
}

type Maybe interface {
    isNothing() bool
    isJust() bool
    fromJust() (int, bool)
}

func (x Nothing) isNothing() bool {
    return true
}

func (x Just) isNothing() bool {
    return false
}

func (x Nothing) isJust() bool {
    return false
}

func (x Just) isJust() bool {
    return true
}

func (x Nothing) fromJust() (int, bool) {
    return 1, false
}

func (x Just) fromJust() (int, bool) {
    return x.val, true
}

We introduce named types Nothing and Just to represent the two cases (failure and success). The Maybe interface and method instances for Nothing and Just imply that Maybe is a sum type with constructors Nothing and Just. Pattern matching in Go is emulated via overloaded method definitions.

Linked lists

As another example of a sum type, we consider linked lists.

type Nil struct{}
type Cons struct {
    elem int
    next List
}

type List interface {
    len() int
}

func (x Nil) len() int { return 0 }

func (x Cons) len() int {
    return 1 + x.next.len()
}

There two constructors. Nil to create the empty list and Cons via which we add an element to an existing list. What is interesting here is that in the type definition for Cons we declare that next is some List where List is any type that supports the len method.

Expression language

Via algebraic data types we can describe context-free/EBNF grammars.

Consider the following simple expression language

N ::= ... | -1 | 0 | 1 | ...

E ::= N | E + E | E * E

and its representation in Go represented as an algebraic data type.

type Exp interface {
    eval() int
}

type Num int
type Mult [2]Exp
type Plus [2]Exp

func (x Num) eval() int {
    return (int)(x)
}

func (e Mult) eval() int {
    n1 := e[0].eval()
    n2 := e[1].eval()
    return n1 * n2
}

func (e Plus) eval() int {
    n1 := e[0].eval()
    n2 := e[1].eval()
    return n1 + n2
}

We not only represent the expression language in Go. In addition, we also provide for some methods that operate on expressions. In the above, we introduce an eval method that evaluates expressions.

Abstract syntax trees (ASTs)

As we will discuss in more detail later (see syntax analysis), the above expression grammar represents an abstract representation of the syntactic structure of expressions. As we know from theoretical computer science, derivations that verify that an expression is well-formed (part of language described by the above grammar) can also be represented as trees. Hence, the representation of the expression grammar in Go allows us to represent abstract syntax trees (ASTs).

Complete examples

Maybe and linked lists

package main

import "fmt"

type Nothing struct{}

type Just struct {
    val int
}

type Maybe interface {
    isNothing() bool
    isJust() bool
    fromJust() (int, bool)
}

func (x Nothing) isNothing() bool {
    return true
}

func (x Just) isNothing() bool {
    return false
}

func (x Nothing) isJust() bool {
    return false
}

func (x Just) isJust() bool {
    return true
}

func (x Nothing) fromJust() (int, bool) {
    return 1, false
}

func (x Just) fromJust() (int, bool) {
    return x.val, true
}

func myDiv(x int, y int) Maybe {
    if y == 0 {
        return Nothing{}
    }

    return Just{x / y}
}

// Linked lists
type Nil struct{}
type Cons struct {
    elem int
    next List
}

type List interface {
    len() int
}

func (x Nil) len() int { return 0 }

func (x Cons) len() int {
    return 1 + x.next.len()
}

func main() {

    // maybe
    r, _ := myDiv(3, 2).fromJust()
    fmt.Printf("\n %d", r)

    // lists
    list := Cons{1, Cons{2, Cons{3, Nil{}}}}

    fmt.Printf("\n %d", list.len())

}

Expression language

package main

import "fmt"

type Exp interface {
    eval() int
}

type Num int
type Mult [2]Exp
type Plus [2]Exp

func (x Num) eval() int {
    return (int)(x)
}

func (e Mult) eval() int {
    n1 := e[0].eval()
    n2 := e[1].eval()
    return n1 * n2
}

func (e Plus) eval() int {
    n1 := e[0].eval()
    n2 := e[1].eval()
    return n1 + n2
}

// Helper functions to build ASTs by hand

func number(x int) Exp {
    return Num(x)
}

func plus(x, y Exp) Exp {
    return (Plus)([2]Exp{x, y})

    // The type Plus is defined as the two element array consisting of Exp elements.
    // Plus and [2]Exp are isomorphic but different types.
    // We first build the AST value [2]Exp{x,y}.
    // Then cast this value (of type [2]Exp) into a value of type Plus.

}

func mult(x, y Exp) Exp {
    return (Mult)([2]Exp{x, y})
}

func main() {

    ex := plus(number(1), mult(number(2), number(3)))

    fmt.Printf("\n %d", ex.eval())
}

Summary

Traditional OO approach

// OO sketch Java style

class Shape {
  int area();
}

class Square : Shape {
  int x;
  int area() { return x * x; }
}

class Rectangle : Shape {
  int x, y;
  int area() { return x * y; }
}


// Virtual method resolution
int shapeTwo(Shape sh1, sh2) {
    return sh1.area() + sh2.area()
}

Another example. AST representation and some virtual method for evaluation.

// OO sketch

class Exp {
   int eval();
}

class Num : Exp {
   int x;
   Num(int y) { x = y ; }
   int eval( return x; }

class Plus : Exp {
   Exp left, right;
   Plus(Exp x, y) { left = x; right = y; }
   int eval() { return left.eval() + right.eval(); }
}

class Mult : Exp {
   Exp left, right;
   Plus(Exp x, y) { left = x; right = y; }
   int eval() { return left.eval() * right.eval(); }
}

Go’s approach

Method overloading.

Thanks to interfaces we can model abstract data types.

Interfaces + overloaded methods can be used to mimic algebraic data types + pattern matching.

Expression Problem

Consider the above AST representation in Java. It is easy to add a new case such as subtraction.

class Sub : Exp {
   Exp left, right;
   Sub(Exp x, y) { left = x; right = y; }
   int eval() { return left.eval() - right.eval(); }
}

There is no need to change any of the existing cases!

What about adding some new functionality? For example, a pretty print method. We will need to update all cases to include this new functionality.

In language like Haskell, it is easy to add new functionality (without affecting existing code). But once we add a new case, we need to change all existing functions that operate on these cases.

Challenge: Can we add new cases and new functionality without having to change (and therefore) recompile existing code?

This challenge is known as the Expression Problem. There is no straightforward solution to the Expression Problem in a language like Haskell, Java or Go. But there are encodings for the Expression Problem. We might come back to this topic later.