Video Coming Soon...
19: The Type System: Closures
WARNING This exercise is in DRAFT status, so there may be errors. If you find any, please email me at help@learncodethehardway.com so I can fix them.
You're now going to learn about something that seems complicated, but isn't actually anything new. I'm warning you about this because everyone who tries to learn Closures seems to freak out and give it this complexity that isn't there. I'm going to walk you through the logic of how they work, and hopefully I can explain it simply so you understand it.
Step 1: A Function Has a Name
Do you remember when we made a function that adds two numbers like this:
func Add(a int, b int) int {
result := a + b
return result
}
In this code the function is named Add
, and it has two variables named a
and b
. In this function I assign the result of a + b
to the variable named result
. I'm allowed to do this because a
has a name, b
has a name, and Go allows me to use anything with a name to create a new variable with a new name.
Given that Add
is also a name then is there any reason I can't do this:
// all of these work
my_add := Add
another_add := Add
add_stuff := Add
If Go is a logical language then there shouldn't be any reaons why I couldn't do that. Add
has a name, and just like a
and b
I can assign that to another variable.
YOU DO IT Stop right now and try this out to prove to yourself that you can actually assign a function to another name.
Step 2: Calling a Function by Any Name
When you call a function you add the ()
(parenthesis) to the end of the functions name, list any parameters, and Go runs it for you returning the result. If I do this:
ten := Add(5,5)
Then the variable ten
will be the number 10
.
If I am allowed to assign the function Add
to a different variable, and I get no errors, then doesn't that mean I can also call that new varible the same way? This should work right?
my_add := Add
ten := my_add(5,5)
If Go is a logical language, and it allows me to assign a function to a new variable, then it'd have to let me call that new variable, otherwise the entire operation is pointless.
YOU DO IT Stop right now and call your newly named functions to prove to yourself that this does actually work.
Step 3: Functions Have a Type
If every variable in Go has a type--like a
above is int
--then it makes sense that a function would also have a type. What would that type be? For our Add
function it's this:
var my_add func(int,int) int
my_add = Add
Let's break this down one thing at a time so you see how it's actually read:
var
-- Declare a variable.my_add
-- Name of variable.func
-- Start of the typefunc
, but that's not enough.func
types are more complex than justint
.(
-- Same as in your function's definition, as in the same as when you defined theAdd
function.int
-- You don't need the parameter name, just its type, and the first parameter isint
.,
--Add
has two parameters, so we need a,
(comma).int
--int
again since that's the type of the second parameter.)
-- Just like with theAdd
function, we end the parameter list with a)
(close parens).int
-- The return value ofAdd
isint
, so we write theint
here.
The parts of this line from 3-9 are what we call a "function signature." Just like with your handwritten signature it's a specific form that identifies this function as unique compared to other functions. In the case of my signature it's unique all the time.
Step 4: Function Signatures Can Have type
Remember when we created an Animal
struct?
type Animal struct {
Name string
Age int
// and so on
}
The type
keyword creates a new type name for the struct
here. If I can do that with a struct
, then couldn't I also do that with the func (int,int) int
above?
type MathFunc func (int, int) int
If that works, then that new type name can be used in a variable declaration just like with struct
:
type MathFunc func (int, int) int
var my_add MathFunc
my_add = Add
result := my_add(5, 5)
YOU DO IT Stop now and create a variable like this, with a
type
and use it.
Step 5: Passing a Function to a Function
Great, we've established that you can do the following:
- You can assign a function to a new name just like anything else in Go.
- This new name can be called, just like the original function.
- This means that functions also have types like an
int
orstruct
. - If a function has a type signature, then we can use the
type
keyword to name it for later (and to avoid figuring out complex function signatures).
This seems to indicate that anything I cand do with a variable I can do with a function, so what about passing a function to another function? Let's try it:
View Source file ex19a/main.go Only
package main
import (
"fmt"
)
type MathFunc func (int, int) int
func Add(a int, b int) int {
return a + b
}
func Sub(a int, b int) int {
return a + b
}
func TenMath(math MathFunc) int {
return math(10, 10)
}
func main() {
using_add := TenMath(Add)
fmt.Println("using_add is", using_add)
using_sub := TenMath(Sub)
fmt.Println("using_sub is", using_sub)
}
In this code I'm doing nothing we haven't covered, the only new part is:
func TenMath(math MathFunc) int {
return math(10, 10)
}
func main() {
using_add := TenMath(Add)
}
If everything is true so far, then the TenMath
function should work because:
- Go lets us create a new
type
for a function's signature. - Go lets us assign a function to a variable.
- We can declare that variable with the
type
. - Functions take parameters that are variables.
- The parameters to a function have a type.
- That means I can give the parameters to a function using the
type
ofMathFunction
just like I would witha int
. - And that means, I can pass any function that has the same signature as
MathFunction
to another function as a parameter.
STOP At this point you should take a massive break and then study this code for as long as it takes to grasp this concept. It's incredibly simple and is a consequence of giving functions the same capabilities as any other variable. I recommend you spend time breaking, changing, and recreating this code until you get that you can pass functions to functions like this.
Step 6: Anonymous Functions
The next piece of this puzzle is anonymous functions, which is simply a function that doesn't have a name. These can be assigned directly to a variable or passed on the spot to a function. For example:
add := func (b int) int {
return 10 + b
}
result := add(5) // should be 15
As you can see, you create an anonymous function the same way as other funtions, but you remove the name. If I wrote:
func AddTen(b int) int {
// ...
}
Then to anonymize it I remove the AddTen
. I then have to assign that to a variable or pass it as a value or it's not much use.
Step 7: Returning a Function from a Function
Are you seeing a trend with this lesson? Almost anything you can do with an int
or struct
you can do with a function. There are some restrictions, but for the most part, the functions in Go are what we call "first class functions." That means they aren't special fixed entities but as variable as anything else in the language.
For the next piece, remember our original Add()
function:
func Add(a int, b int) int {
result := a + b
return result
}
This returned result
to the caller right? We've established that you can do almost anything with functions that you can do with other variables, so why not returning one? More importantly, can we define a function inside another function? Let's try it:
View Source file ex19b/main.go Only
package main
import (
"fmt"
)
type MathFunc func (int) int
func MakeAdder() MathFunc {
add := func (b int) int {
return 10 + b
}
return add
}
func main() {
add_ten := MakeAdder()
ten_plus_ten := add_ten(10)
fmt.Println("ten_plus_ten is", ten_plus_ten)
ten_plus_5 := add_ten(5)
fmt.Println("ten_plus_5 is", ten_plus_5)
}
This code is the culmination of what we've discovered so far about first class functions. The most important part is this:
func MakeAdder() MathFunc {
add := func (b int) int {
return 10 + b
}
return add
}
Let's break this part down line-by-line:
func MakeAdder() MathFunc {
-- I declare theMakeAdder
function returns a function with the typeMathFunc
. This is the same as writingfunc MakeAdder() int
but instead ofint
I put myMathFunc
type.add := func (b int) int {
-- This is our anonymous function style from the previous section. It takes a singleint
and returns...return 10 + b
-- 10 +b
to add 10.}
-- Ends our inner function stared withadd :=
.return add
-- Finally, ifadd
is a variable like any other, and we told Go thatMakeAdder
returns a typeMathFunc
then this is allowedl.}
-- Ends ourMakeAdder
function.
Once we have this we can use this to create any number of functions, but this isn't too usefule. What is useful is Closures.
Step 8: And Therefore, a Closure Is...
The final piece of this puzzle is a question:
If you are allowed to create functions inside other functions (see
add := ...
), can that function use variables from outside it? How would returning such a function even work?
Let's make a small modification to the MakeAdder
function so it takes a parameter to demonstrate this:
func MakeAdder(a int) MathFunc {
add := func (b int) int {
return a + b
}
return add
}
When you make this change you have to change the main()
code to call MakeAdder(10)
instead.
The only change I made is MakeAdder(a int)
is now a parameter that lets you configure the add
function so it can add any number. Then I changed the return
code to be return a + b
instead of return 10 + b
. That means the function MakeAdder
returns is now using the variable a
instead of 10
.
But wait, won't that crash? When a function normally returns any of its parameters (in this case a
and b
) disappear. Trying to use them after would normally not be possible or cause a crash in most languages, but in Go and languages with "first class functions" this is possible because of closures.
A closure is:
- A function (child) defined inside another function (parent)...
- That references (uses) variables from the parent function...
- Which causes Go to "pack up" the variables being used and attach them to the returned function so the child function keeps working.
In this case, Go does this:
- Sees the parameters
a
andb
toMakeAdder
(parent). - Sees the creation of
add
(child). - Sees that
add
(child) is using thea
variable fromMakeAdder
(parent). - Attaches it to
add
so that it continues to exist after thereturn add
at the end. - Now any time you call
ten_plus_ten
you only pass 1 variable, butten_plus_ten
still has access to the originala
fromMakeAdder
.
This is the only slightly hidden thing in all of this that's not something other variables get. Everything else so far has been a consequence of functions being treated with the same respect as variables (thus the name "first class", like on a plane). This feature is needed though because without it functions defined inside other functions wouldn't work right.
STOP! Once again, take time to understand this as it's subtle and simply done for you without any extra syntax. Try making your own
MakeX
function that makes these kinds of closures until you understand what's going on.
The Code
You're now ready to try another example of closures, and as usual I won't be breaking this down because there's nothing new in here.
View Source file ex19/main.go Only
package main
import (
"fmt"
)
type CountFunc func() int
func MakeCounter(starting_at int) CountFunc {
counter := func() int {
starting_at++
return starting_at
}
return counter
}
func main() {
from_ten := MakeCounter(10)
for i := 10; i < 20; i = from_ten() {
fmt.Println(i)
}
}
In this code I create a MakeCounter
to demonstrate another aspect of closures. The variables that are attached to closures are persistent inside the closure, so you can to repeatedly generate a sequence.
The Practice
- Break It -- There's many ways to break this code, but try returning a function with one signature when the return type is different. You should also definitely cause Go to crash with a recursive function as described in the Tail Call Optimization section.
- Change It -- I think you should spend as much time as possible to change this code and experiment with closures until you get it. Try making new closures that do things like cycle through a
slice
of names or mutate astruct
in some way. - Recreate It -- I really feel like you will benefit greatly from recreating this code, but also from devising your own idea and making it from scratch.
Study Drills
- How insane can you go with this? Could you make a closure that makes closures that use other closures?
- How would you convert the
fib()
function to use a loop instead of the broken recursion?
Register for Learn Go the Hard Way
Register today for the course and get the all currently available videos and lessons, plus all future modules for no extra charge.