Martin Sulzmann
Methods
Interfaces
Structural subtyping
Go’s structural subtyping versus Java’s nominal subtyping
Generics in Go
We can define methods on named types where we can use the “dot” notation to call methods.
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
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 requiredThe 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.
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.
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,
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.
Interface:
Collection of methods that share the same receiver
Interfaces are types
Structural subtyping:
Named type s is a structural subtype of interface
t if s implements all methods as declared by
t
Interface s is a structural subtype of interface
t if s declares all of t’s
methods 3 We will write s <= t to denote the structural
subtype relation among s and t.
Subtyping principle:
e is of type s and
s <= t then e can be used in any context
where tye type t is expectedThe receiver is left implicit in the interface declaration.
Interfaces are types.
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
rectangle <= shape
square <= shape
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.
An interface may consist of several (overloaded) methods. We can also extend an existing interface.
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?
This will not compile and yields a type error.
Neither rectangle nor square implement
the shapeExt interface
That is, neither rectangle nor square
are a structural subtype of shapeExt
The following works!
Why?
We can argue that *rectangle <= shapeExt and
*square <= shapeExt for the following reason
*rectangle implements the scale method,
see S1
rectangle implements the area method,
see A1
Any value receiver definition implies a pointer receiver.
Hence, *rectangle implements the area
method as well.
Hence, *rectangle <= shapeExt
Regarding 3. Consider
The above method call will be transformed to
Hence, we can argue that *rectangle (pointer to
rectangle) the area method as well.
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)
}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()
}// 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
ShapeExt <= Shape
ShapeExt2 <= Shape
where <= denotes the subtype relation.
This form of subtyping is referred to as nominal subtyping. The subtype relations are explicitly declared via class hierarchies.
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
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)?
Writing generic code (for example “collections”)
But this is dangerous cause we need run-time type casts!
Indeed, once the language supports “generic types” there is no
need for interface{} (or Object)
However, interface{} (or Object) may
come up in the translation (compilation) of programs.
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})
}“Generics” allow us to abstract over type parameters.
Thus, we can write more type-safe programs.
Some collection.
We use the most general type interface{}
We may want to have pairs of (string, float32), (string, string), …
Some convenience functions.
Some application.
var s string
var f float32
s = "Erdinger"
f = 0.9
product := pair{s, f}
var price float32
price = second(product).(float32) // Type assertions!string <= interface{}
float32 <= interface{}
product is of type pair
second yields a value of “any” type
We use a type assertion to case this value into a value of type
float32
Type assertions may fail!
The above type checks
But will fail at run-time
Generic collection.
S and T are type parameters
They can be instantiated for any type
In Go, type paramters are always constrained by a type bound
any = interface{}
Making the above code more type-safe.
var s string
var f float32
s = "Erdinger"
f = 0.9
product := pairG[string, float32]{s, f}
var price float32
price = secondG(product)There is no need for a type assertion
The above code type checks (statically = compile-time)
This guarantees that we will not fail (due to incompatible) at run-time.
Consider
package main
import (
"fmt"
)
type pair struct {
left interface{}
right interface{}
}
func first(x pair) interface{} {
return x.left
}
func second(x pair) interface{} {
return x.right
}
func testPair() {
var s string
var f float32
s = "Erdinger"
f = 0.9
product := pair{s, f}
// string <= interface{}
// float32 <= interface{}
var price float32
price = second(product).(float32) // Type assertions!
// price = first(product).(float32)
// Type checks but fails at run-time.
fmt.Printf("%f", price)
}
// Generics
type pairG[T any, S any] struct {
left T
right S
}
func firstG[T any, S any](x pairG[T, S]) T {
return x.left
}
func secondG[T any, S any](x pairG[T, S]) S {
return x.right
}
func testPairG() {
var s string
var f float32
s = "Erdinger"
f = 0.9
product := pairG[string, float32]{s, f}
var price float32
price = secondG(product)
// price = firstG(product)
// Fails to type check!
fmt.Printf("%f", price)
}
func main() {
testPair()
testPairG()
}object = struct + methods
Nominal subtype relations derived from class hierarchies
Inheritance (reuse of methods) 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()
}Method overloading.
Define your named types (structs, …).
Define (overloaded on the receiver) methods for named types.
Loosely coupled. Can easily add new structs and new methods.
Write generic code via interfaces and structural subtyping
Thanks to interfaces we can model abstract data types.
Interfaces + overloaded methods can be used to mimic algebraic data types + pattern matching.