Martin Sulzmann
package main
import "fmt"
func main() {
var x int
x = 1
y := x + 1
fmt.Printf("y = %d", y)
}
The type of y
is inferred by the right-hand side.
Pretty convenient!
Languages like Haskell support full type inference where the types of functions can be inferred.
Type inference refers to the process of automatically inferring the type of program text (variables, expressions, functions, ...).
Type checking refers to the process of automatically checking if the provided (user-annotated) type information matches the progrm text.
Both methods can either be carried out statically (at compile-time) or dynamically (at run-time).
Both methods rely on typing rules that specify the conditions under which some program text is type correct. For example, for an assignment statement we demand that the types of the expressions of the left-hand and right-hand side must be identical. There is an entire branch of computer science devoted in the formalization of such typing rules and how to effectively implement via type checking/inference.
Here are some sample typing rules describing the well-typing of arithmetic expressions.
G denotes a set of type/variable declarations of the form
{x1 : int, ..., xn : int}
x : int is an element in G
(Var) -------------
G |- x : int
i is a natural number
(Int) ---------------
G |- i : int
G |- e1 : int G |- e2 : int
(Add) ---------------------------------
G |- e1 + e2 : int
Above to be read as "if-then" rules. The part above the ---
is the precondition (premise) and the part below is the postcondition (conclusion).
We refer to G |- e : int
as a judgment stating that in type environment G
, the expression e
has type int
.
We take a brief look at type checking/inference in Go. Consider the following example.
package main
import "fmt"
func f(x int) int {
var y int
y = x + 1
return y
}
func f2(x int) int {
y := x + 1
return y
}
func main() {
var x int
x = 1
fmt.Printf("%d \n", f(x))
fmt.Printf("%d \n", f2(x))
g := f
y := 1
fmt.Printf("%d \n", g(y))
}
Via x := e
we indicate that on the left-hand side we introduce (declare) a fresh variable x
whose value is determined by the right-hand side e
. No explicit type annotations are provided as the type of x
can be determined by inferring the type of e
.
How does this (simple) form of type inference work? We can assume that the types of all variables on the right-hand side is known. So, we check by following the typing rules of the right-hand side expression is type correct. As part of this type checking process, we also infer the expression's type.
For example, consider the expression from above.
x + 1
where x is of type int
We type check x + 1 as follows.
We observe the structure of the expression.
In our case, we find addition with x and 1 as operands.
So rule (Add) from above applies.
We can conclude that x + 1 is of type int if
x is of type int and 1 is of type int.
This holds!
Notice something?
By the way, we infer the type of x + 1!
We traverse the structure of the expression (structural induction)
by going from the outer level (addition) to the inner level (operands).
What is full type inference? If we could omit all type annotations. In Go we still need to provide of function definitions. In other languages, e.g. Haskell, the type of function definitions is optional. We say that such languages support full type inference.
Let's consider our running example where all type annotations are omitted.
func f3(x) {
y = x + 1
return y
}
Let's attempt to reconstruct the missing type annotations by inspection of the program text.
1. Consider x + 1
Based on the above typing rules we deduce that
x + 1 is of type int and therefore
x is of type int as well.
2. Consider y = x + 1
The types of left-hand and right-hand side must match.
Hence, we deduce that y is of type int.
3. return y
implies that f3's return type is int
Putting the pieces together, we deduce the following.
func func3(x int) int {
var y int
y = x + 1
return y
}
Is that the only type we can give to this program text. Well, Go also supports float32. So, another possible annotation would be the following.
func func3(x float32) float32 {
var y float32
y = x + 1
return y
}
The program text x + 1
typechecks under the assumption that the integer constant 1
can be coerced into a floating point number.
The above, several types that can be inferred, highlights one of the challenges of type inference. The quest is to design a programming language that enjoys principal type inference. By principal we mean, type inference yields a most general type that subsumes all other types. For more details on this subject, please refer to the research literature.
We can define methods on named types where we can use the "dot" notation to call methods.
type rectangle struct {
length int
width int
}
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())
}
Encapsulation and visibility
Mimicing inheritance via annonymous fields
...
Overloading. Define a function, method, operator with the same name in the same scope.
Go supports method overloading where method dispatch is based on the "receiver" type. Consider the example
type Int struct { val int }
func (i Int) plus(x int) int {
return i.val + x
}
type MyFloat32 float32
func (f MyFloat32) plus(x float32) float32 {
return (float32(f)) + x
}
The name plus
is overloaded because there exist two definitions for plus
.
(Overloaded) methods takes as argument a receiver type which is the argument preceeding the method name.
These receivers must be user-defined types (introduced via the type
keyword).
The type MyFloat32
is a new type but is ismorphic to the built-in type float32
.
The choice which method is called (method dispatch) depends on the receiver type (and only the receiver type).
The calling convention for methods in Go resembles the common "dot" notation used in OO language. For example, consider
i := Int {1}
r1 := i.plus(1)
f := MyFloat32 (1.0)
r2 := f.plus(1.0)
Here is another example.
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
}
At run-time, the choice which definition of area
to select is based on the receiver type (leading argument of the method definition).
Interfaces are contracts between expected behavior and actual implementation.
In Go, we can build a common interface for overloaded methods. Consider
type ADDInt interface {
addInt(int) float32
}
Interfaces can appear as arguments and return values.
func useAdd(a ADDInt, x int) float32 {
return a.addInt(x)
}
Actual implementations are provided as overloaded methods
func (i Int) addInt(x int) float32 {
return float32 (i.val + x)
}
func (f MyFloat32) addInt(x int) float32 {
return (float32(f)) + float32 (x)
}
and the concrete instances are selected based on the arguments provided for interfaces.
a1 := useAdd(Int{2},2)
a2 := useAdd(MyFloat32 (2.0),2)
In Go, interfaces can be nested and actual implementations added at any time.
type MULTInt interface {
multInt(int) float32
ADDInt
}
func useAddMult(a MULTInt, x int) float32 {
return a.addInt(x) + a.multInt(x)
}
The Go type system guarantees that actual implementations are available.
func (i Int) multInt(x int) float32 {
return float32 (i.val * x)
}
m1 := useAddMult(Int{2},2) // type checks
m2 := useAddMult(Float32 (2.0),2) // yields type error!
We encounter a type error because the MultInt instance on type Float32
can not be satisfied (because there is no such definition for multInt
).
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".
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})
}
type shape interface {
area() int
}
Some functions which assume a shape behavior.
func shapeTwo(sh1, sh2 shape) int {
return sh1.area() + sh2.area()
}
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.
Some concrete instances.
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
}
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
}
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))
}
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)
}
func test0(x Set) bool {
return x.isEmpty()
}
func test1() Set {
xs := []int{}
return (SetImpl)(xs)
}
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()
}
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.