Martin Sulzmann
The topic of "types" is now covered by a separate set of notes.
We can define methods on named types where we can use the "dot" notation to call methods.
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
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.
We call methods by using the "dot" notation.
r1.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.
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 &
operator to build a reference to r.
r1.scale(3)
(&r1).scale(3)
The above statements are equivalent. Go will automatically perform the conversion.
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())
}
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
}
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.
Encapsulation and visibility
Mimicing inheritance via annonymous fields
...
Interfaces are contracts between expected behavior and actual implementation. In Go, we can build a common interface for overloaded methods. Each value of the interface type must support the methods specified by the interface.
type shape interface {
area() int
}
The receiver is left implicit in the interface declaration. In general, an interface may consist of several (overloaded) methods. We can also extend an existing interface.
type shapeSuper interface {
shape
scale()
}
The order among shape
and scale
is not relevant. The interface shapeScale
supports the scale
and the area
method.
Interfaces are types and values of an interface type must support the declared methods.
Consider the following function definitions.
func shapeTwo(sh1, sh2 shape) int {
return sh1.area() + sh2.area()
}
func shapeSuperTwo (sh1, sh2 shapeSuper) int {
sh1.scale(5)
sh2.scale(3)
return sh1.area() + sh2.area()
}
Recall that rectangle
supports scale
and area
but square
only supports area
. Hence, arguments for shapeTwo
can either be values of type rectangle
or square
whereas arguments can only be values of type rectangle
(because square
does not support scale
).
The function shapeTwo
subsumes the four earlier definitions we have seen.
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()
}
We can say that shapeTwo
is a generic function because shapeTwo
can be applied on values of different types.
For example, consider the following.
var r1 rectangle = rectangle{1, 2}
var s1 square = square{3}
x := shapeTwo(r1, s1) // applied on (rectangle, square)
y := shapeTwo(s1, s1) // applied on (square, square)
func shapes(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 shapes.
// Only difference is that we first need to create a slice to call shapes2.
func shapes2(shs []shape) int {
var a int = 0
for _, elem := range shs {
a = a + elem.area()
}
return a
}
Method names in an interface hierarchy must be distinct.
Interface hierarchies must be acyclic.
Method names can appear in distinct interfaces.
Via interfaces + method overloading, Go supports a (limited) form of "ad-hoc polymorphism".
package main
import "fmt"
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
}
type shape interface {
area() int
}
func shapeTwo(sh1, sh2 shape) int {
return sh1.area() + sh2.area()
}
// Variadic function
func shapes(shs ...shape) int {
var a int = 0
for _, elem := range shs {
a = a + elem.area()
}
return a
}
// Expects a list of shapes (slice).
// Pretty much the same compared to shapes.
// Only difference is that we first need to create a slice to call shapes2.
func shapes2(shs []shape) int {
var a int = 0
for _, elem := range shs {
a = a + elem.area()
}
return a
}
// extend the interface
type shapeSuper interface {
scale(int)
shape
}
func shapeSuperTwo (sh1, sh2 shapeSuper) int {
sh1.scale(5)
sh2.scale(3)
return sh1.area() + sh2.area()
}
func (s *square) scale(x int) {
s.length = s.length * x
}
func namedTypes() {
var r rectangle
var r1 struct {
length int
width int
}
// r = r1 // NOT OK
r = (rectangle)(r1) // OK cast required
fmt.Printf("%d", r.length)
}
func main() {
var r1 rectangle = rectangle{1, 2}
var s1 square = square{3}
fmt.Printf("%d \n", r1.area()+s1.area())
fmt.Printf("%d \n", shapeTwo(r1, s1))
fmt.Printf("%d \n", shapes(r1, r1, s1, s1, s1))
shs := []shape{r1, r1, s1, s1, s1}
fmt.Printf("%d \n", shapes2(shs))
fmt.Printf("%d \n", shapeSuperTwo(&s1, &s1))
// scale expects a reference as argument,
// therefore we need to pass in s1 by reference.
// Building the reference within shapeSuperTwo is not sufficient.
}
interface{}
Any interface. Similar to the type `Object` in Java.
We can perform some run-time type cast.
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.
* 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})
}
We build an abstract data type Set
.
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())
}
object = struct + methods
Inheritance and virtual methods as a means to write polymorphic programs
// 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(); }
}
Method overloading.
Define your named types (structs, ...).
Define (overloaded) methods for named types.
Loosely coupled. Can easily add new structs and new methods.
Interfaces to write polymorphic programs.
Go does not suppoert generics (a.k.a. parametric polymorphism).
Interfaces + overloaded methods can be used to mimic algebraic data types + pattern matching. See extended regular expression example.
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.