Martin Sulzmann
package main
import "fmt"
var x int
func hi(y int) {
fmt.Printf("hi %d\n",y)
}
func main() {
x= 1
hi(x)
fmt.Printf("hello, world\n")
}
var varName varType
varName
of Type varType
Printf similar to printf in C, see here for brief description of the C printf variant.
go run hello.go
gofmt hello.go
gofmt -w hello.go
Choose your editor (emacs, ...)
In most cases, our programs consist of a single file.
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.
The only control structure.
for i := 0; i < 5; i++ {
fmt.Printf("Value of i is now: %d \n", i)
}
Infinite loop with break
for {
if j > 5 {
break
}
fmt.Printf("Value of j is now: %d \n", j)
j++
}
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
fmt.Printf("Value of i is now: %d \n", i)
}
j := 0
for {
if j > 5 {
break
}
fmt.Printf("Value of j is now: %d \n", j)
j++
}
}
Unlike in C, Go strings are not null terminated. In Go, strings are represented by a struct consisting of
The length of the string.
A pointer to the first byte.
These implementation details are hidden from the user. The user can make use of array notation to access a specific position in the string. There is a primitive len
that computes the length of a string. Strings are immutable. This means they cannot be updated (but copied of course).
Next, we discuss a few examples that involve strings.
Printing a string.
var x string = "Hello"
y := "Hi"
fmt.Println(x)
fmt.Printf("%s \n", y)
Several ways to iterate over a string.
for i := 0; i < len(x); i++ {
fmt.Printf("%c", x[i])
}
for i, e := range x {
fmt.Printf("%c", e)
fmt.Printf("%c", x[i])
}
// Don't care notation _
for i, _ := range x {
fmt.Printf("%c", x[i])
}
for _, e := range x {
fmt.Printf("%c", e)
}
Via range
we obtain current position and element. The notation _
indicates that we don't care about the value.
Out of bounds check.
var s1 [3]string
s1[0] = "one"
s1[1] = "two"
s1[2] = "three"
// s1[3] = "four"
Short-hand array initialization
s2 := [3]string{"one", "two", "three"}
Iteration
for index, elem := range s1 {
fmt.Printf("%d %s \n", index, elem)
}
package main
import "fmt"
func main() {
var s1 [3]string
s1[0] = "one"
s1[1] = "two"
s1[2] = "three"
for index, elem := range s1 {
fmt.Printf("%d %s \n", index, elem)
}
s2 := [3]string{"one", "two", "three"}
fmt.Printf("%s", s2[0])
}
More flexible arrays (length may change)
s1 := make([]string, 3)
Short-hand
s2 := []string{"a", "b"}
Functions on slices, e.g. append
s2 := []string{"a", "b"}
s3 := append(s2, "c", "d", "e")
package main
import "fmt"
func printSlice(s []string) {
for _, elem := range s {
fmt.Printf("%s \n", elem)
}
}
func main() {
s1 := make([]string, 3)
s1[0] = "one"
s1[1] = "two"
s1[2] = "three"
printSlice(s1)
s2 := []string{"a", "b"}
s3 := append(s2, "c", "d", "e")
printSlice(s3)
}
func inc(i int) int
func myDiv2(x int, y int) (int, bool)
package main
import "fmt"
func inc(i int) int { return i + 1 }
// Direct reference to return values by name
// Only applies to tuple return values
func myDiv(x int, y int) (res int, status bool) {
status = false
if y == 0 {
return
}
res = x / y
status = true
return
}
func myDiv2(x int, y int) (int, bool) {
if y == 0 {
return 0, false
}
return x / y, true
}
func main() {
var res int
var status bool
res, status = myDiv(inc(3), 2)
fmt.Printf("Result = %d \n", res)
fmt.Printf("Status = %t \n", status)
res, status = myDiv2(1, 0)
fmt.Printf("Result = %d \n", res)
fmt.Printf("Status = %t \n", status)
}
We write a function to split a string into a prefix and a suffix once we see given byte.
func splitAt(x string, b byte) (string, string) {
var r1, r2 []byte
suffix := false
for i, _ := range x {
if !suffix {
if x[i] == b {
suffix = true
} else {
r1 = append(r1, x[i])
}
} else {
r2 = append(r2, x[i])
}
}
s1 := (string)(r1)
s2 := (string)(r2)
return s1, s2
}
We use slices r1 and r2 to collect the prefix and suffix. We check if we have seen the given element b yet, and then append elements in the original string to either r1 or 2. The element b is not added.
We require slices as append only operates on slices. As we wish to retrieve the result as strings, we need to include a type conversion.
Function splitAt returns two results, the prefix and the suffix. Multiple return results can be retrieved as follows.
r1, r2 := splitAt("hel:lo", ':')
fmt.Printf("%s %s", r1, r2)
Simple (increment) function.
func inc(i int) int { return i + 1 }
Functions can be arguments.
func apply(i int, f func(int) int) int {
return f(i)
}
func mapInt(f func(int) int, xs []int) []int {
for i, e := range xs {
xs[i] = f(e)
}
return xs
}
Functions can be return values.
func h1() {
fmt.Printf("Hi\n")
}
func hello2b(x int) func() {
return h1
}
func hello2(x int) func() {
return func() { fmt.Printf("Hello %d \n", x) }
}
Point to note. We introduce here an annonymous function (a "lambda") via the keyword func
.
package main
import "fmt"
func inc(i int) int { return i + 1 }
func apply(i int, f func(int) int) int {
return f(i)
}
func execute(f func()) {
f()
}
func hello() {
fmt.Print("Hello \n")
}
func hello2(x int) func() {
return func() { fmt.Printf("Hello %d \n", x) }
}
func mapInt(f func(int) int, xs []int) []int {
for i, e := range xs {
xs[i] = f(e)
}
return xs
}
func main() {
execute(hello)
execute(hello2(2))
fmt.Printf("%d \n", apply(1, inc))
xs := []int{1, 2, 3}
ys := mapInt(inc, xs) // P1
zs := mapInt(func(i int) int { return i + 1 }, xs) // P2
// Slices are internally represented via a pointer.
// At P1, ys and xs refer effectively to the same slice.
// Hence, at P2, updating xs also updates ys
fmt.Printf("%d %d \n", ys[0], zs[0])
}
Consider
func plus(x int, y int) int {
return x+y
}
The arguments to plus
must be both present.
In Go, it's possible to 'incrementally' supply addition with its arguments.
func add(x int) func(int) int {
return func(y int) int { return x + y }
}
Function add
expects an integer argument (left operand) and yields a function. This function expects another integer argument (right operand) and then yields the expected result.
Being able to supply function arguments incrementally gives us more flexibility. Here is a neat way to define the increment function.
func inc(x int) int {
return add(1)(x)
}
`add(1) yields a function from integer to integer
add(1)(x)
then yields the incremented x
value
Slight variation of the above.
plus := func(x int) func(int) int {
return func(y int) int { return x + y }
}
inc := plus(1)
In Go functions are first-class (that is, they can appear anywwhere). This is similar to OO where objects are first-class.
For example, in an OO language, we can call a method m1
on some object o1
. The result is an object on which we call another method m2
.
o1.m1().m2()
Is there a difference?
func plus1(x int, y int) int {
return x + y
}
func plus2(x int) func(int) int {
return func(y int) int {
return x + y
}
}
How to transform one function into the other?
package main
import "fmt"
func plus1(x int, y int) int {
return x + y
}
func plus2(x int) func(int) int {
return func(y int) int {
return x + y
}
}
func curry(f func(int, int) int) func(int) func(int) int {
return func(x int) func(int) int {
return func(y int) int {
return f(x, y)
}
}
}
func uncurry(g func(int) func(int) int) func(int, int) int {
return func(x int, y int) int {
return (g(x))(y)
}
}
func main() {
x1 := plus1(1, 2)
x2 := (plus2(1))(2)
p := uncurry(plus2)
x3 := p(1, 2)
p2 := curry(plus1)
x4 := (p2(1))(2)
fmt.Printf("%d %d %d %d", x1, x2, x3, x4)
}
Recall
func add(x int) func(int) int {
return func(y int) int { return x + y }
}
We define the following.
var f func(int) func(int) int
var g func(int) (func(int) int)
f
is a function of type func(int) func(int) int
.
The type func(int) func(int) int
states: Given a value of type int
, we return a (function) value of type func(int) int
.
In the type declaration of g
we wrap the return type with parantheses. That is, we find (func(int) int)
. There is no harm in adding these extra parantheses but they are not strictly necessary. Hence, f
and g
are of the same type.
Consider
g = add
fmt.Printf("%d", (g(1))(2))
fmt.Printf("%d", g(1)(2))
We find the function calls (g(1))(2)
and g(1)(2)
. Both calls are equivalent.
The call (g(1))(2)
is to be interpreted as follows:
First execute the subcall g(1)
.
As we know, g(1)
yields a function value of type func(int) int
.
Hence, we call this function with parameter 2
.
g(1)
is an example of a partial function application (because we obtain another function as the result).
To avoid clutter, the convention adopted in Go (and other languages that support higher-order functions) is to allow removal of parantheses by assuming that function application is left-associative.
This means, that expressions (g(1))(2)
and g(1)(2)
have the same meaning.
What's the output of the following program?
package main
import "fmt"
type Type0 func()
func main() {
var fn Type0
var x int = 2
fn = func() { fmt.Printf("%d \n", x) }
fn()
x = 3
fn()
}
For each call to fn
there is a different result!
Unlike pure functional programming languages, the Go language is impure. Pure means that for each function call and the same input arguments, we obtain the same output. This is also referred to as referential transparency.
type myint int
Introduces a new type named myint
. Values of type int
and myint
cannot be mixed.
The type myint
is structurally isomorphic to int
, so we can (type) convert one into the other.
Here is the complete example.
package main
import "fmt"
type myint int
func main() {
var i int
var j myint
j = 1
i = (int)(j)
// i = j
// yields a type error
j = (myint)(i)
fmt.Printf("%d", i)
}
type rectangle struct {
length int
width int
}
Defines a named type rectangle
that is structurally isomorphic to a struct of two ints where both ints can be accessed via field labels.
Construction of values.
var r1 rectangle = rectangle{1, 2}
var r2 rectangle = rectangle{width: 2, length: 1}
r3 := rectangle{width: 2, length: 1}
Either by position or by field reference.
Control structures
Arrays, slices
func add(x int) func(int) int {
return func(y int) int { return x + y }
}
// func add ... Function declaration, I'm a function with name add
// func(int) int Function type, from int to int
// func(y int) int { ...} Annonymous function, lambda binder and body
Play with the examples provided.
Implement the following functions. Sample solutions below.
// Yields the first element which satisfies the predicate.
// Otherwise, we return a dummy element.
// The first component of the resulting tuple indicates
// if any element has satisfied the predicate.
func any(p func(int) bool,xs []int) (bool,int)
// Map f over a list of integer values which then yields
// a list of booleans
func map(f func(int) bool,xs []int) []bool
// Filter out all elemements which satisfy the predicate.
func filter(p func(int) bool,xs []int) []int
package main
import "fmt"
// Yields the first element which satisfies the predicate.
// Otherwise, we return a dummy element.
// The first component of the resulting tuple indicates
// if any element has satisfied the predicate.
func any(p func(int) bool, xs []int) (bool, int) {
for _, x := range xs {
if p(x) {
return true, x
}
}
return false, -1
}
// Map f over a list of integer values which then yields
// a list of booleans
// 'map' predefined so use mapMy
func mapMy(f func(int) bool, xs []int) []bool {
var ys []bool
for _, x := range xs {
ys = append(ys, f(x))
}
return ys
}
// Filter out all elemements which satisfy the predicate.
func filter(p func(int) bool, xs []int) []int {
var ys []int
for _, x := range xs {
if p(x) {
ys = append(ys, x)
}
}
return ys
}
func main() {
b, x := any(func(x int) bool { return x > 1 }, []int{0, 1, 2, 3})
fmt.Printf("%b %d \n", b, x)
xs := filter(func(x int) bool { return x > 1 }, []int{0, 1, 2, 3})
for _, x := range xs {
fmt.Printf("%d \n", x)
}
}
The lambda calculus represents a minimal core language just consisting of functions.
Functions are first-class (can appear as arguments and as return values). Think of first-class objects!
Terms (expressions/programs) in the lambda calculus are defined by the following (abstract) EBNF syntax.
where
The above EBNF is ambiguous. For example, consider expression . Based on the above EBNF rules there are two possible interpretations.
A lambda function with formal parameter where is applied to itself in the function body.
The identity function applied to .
The first interpretation can be explained via the following (parse tree) derivation
whereas for the second derivation we find
In the above derivation, parantheses "(..)" are meta-symbols to highlight the difference among the two derivations.
In examples, we therefore may make use of parantheses to avoid ambiguities, for example .
The operational semantics of the lambda calculus is defined via a single rule, commonly referred to as -reduction.
In , we apply the substitution : In the body , replace each occurence of the formal parameter by the argument .
Commonly, we refer to as a lambda-abstraction and to as the lambda-binding.
For example, reduces to .
As it is common in programming languages, we can reuse the same variable names and apply the common rule that each use of a variable name refers to the closest binding site. For example, consider where the innermost occurence of refers to the inner lambda-binding.
Via some simply renaming we can always guarantee that variables are distinct. For example, can be renamed to .
We generally assume that lambda-bindings always introduce fresh, distinct variables.
Function application is left associative. Hence, the term is a short-hand for .
The body of a lambda abstractions extends to the right as long as possible. Hence, the term is a short-hand for .
computes all free variables in , i.e. those variables which are not bound by a lambda abstraction.
The definition by induction over the structure of is as follows:
In case of name clashes we must rename bound variables. Consider :
Examples (where refers to a term/expression)
There are two principles ways how to evaluate terms:
(\x-> ...) e
Call-by value (CBV) = AOR with the exception that we don't evaluate under "lambda" (i.e. inside function bodies)
Difference between CBV and AOR illustrated via an example. Instead of the plain lambda calculus we make use of Go.
func inc(x int) int { return x+1 }
func f(x int) int {
return inc(1+1) + x
}
func main() {
f(1+2)
}
Under AOR, we can reduce f(1+2)
to f(3)
and inc(1+1)
to inc(2)
.
Under CBV, we can only reduce f(1+2)
to f(3)
because inc(1+1)
is inside a function body.
Hence, evalution under CBV proceeds as follows.
f(1+2)
--> f(3)
--> inc(1+1) + 3
--> inc(2) + 3
--> 3 + 3
--> 6
In comparison, evaluation under AOR.
f(1+2)
= (\lambda x. inc(1+1) + x) (1+2)
^^^
innermost redex
--> (\lambda x. inc(2) + x) (1+2)
--> (\lambda x. inc(2) + x) 3
--> inc(2) + 3
...
--> 6
The motivation for CBV is to avoid evaluation of arguments unless they are needed to make progress during evaluation.
Consider the following variant.
func inc(x int) int { return x+1 }
func f(x int) int {
if x == 3 {
return 0
}
return inc(1+1) + x
}
func main() {
f(1+2)
}
For this case and the call f(1+2)
reducing inc(1+1)
to inc(2)
is unnecessary (and therefore wastefull) as we don't reach this program text during evaluation.
Outermost, leftmost, also called Normal Order Reduction (NOR)
Call-by name (CBN) = NOR with the exception that we don't evaluate under "lambda" (i.e. inside function bodies)
We consider evaluation of where denotes an evaluation step.
We omit the alpha-renaming step (we should actually rename bound variables but keeping the bound variables as they are is harmless for this example).
Call-by-name (CBN)
Call-by-value (CBV):
There are lambda terms whose evaluation will not terminate:
Termination under CBN is more likely than termination under CBV.
Let's abbreviate by . Then, the lambda term terminates under CBN but not under CBV.
CBV:
CBN:
call-by-need:
The difference to CBN is that once we evaluate and obtain the result , we immediately replace all other occurrences of by without actually evaluating these other occurrences.
Consider .
Function application is left-associative, therefore, the meaning of the above lambda term is .
Let's carry out the evaluation steps.
because
because
On the other hand, the lambda term evaluates to . Briefly, the evaluation steps are
How expressive is the lambda calculus? As expressive as a Turing machine?
Yes! But how? The lambda calculus looks rather simple. There are no numbers, booleans, no conditional, looping constructs.
Indeed, but as we will show next, we can encode data types (booleans, ...), conditional expressions, recursion.
The idea of the Church encoding (named after Alonzo Church):
The idea:
not x = if x then false else true
x or y = if x then true else y
x and y = if x then y else false
The actual encoding:
true =
false =
if then else =
which written in 'lambda notation' is
ite =
Example: if true then else equals
In detail:
=
by adding parentheses (recall that function application is left associative)
We evaluate . The expect result is . Let's see!
Several redexes. We choose the outermost redex
The idea.
...
The successor function.
The addition function.
What about recursion?
factorial n = if n == 0 then 1
else n * (factorial (n-1))
Idea:
This works!
More encodings (pairs, natural numbers) are possible. See Church encoding.