Learning a new concept from real-world code
In my day job, I am a Site Reliability Engineer on the OpenShift Dedicated team at Red Hat. OpenShift Dedicated SREs run managed OpenShift clusters for our customers, and at the scale we work, doing so manually would be impossible, so we make heavy use of Kubernetes Operators to automate our tasks natively within OpenShift.
My background is not in programming, and all the programming skills I’ve picked up I’ve learned on the job. When I joined my team at Red Hat, I brought decent experience with Python and shell scripting, and some Ruby. I only had a very small amount of experience with Go, but that is all my team uses. All of our operators are written in Go. My team is great, though. They’re very supportive, and helping me learn the new language alongside all the other institutional knowledge I need to be a part of the organization.
What this means, though, is sometimes I run into things that I’ve never seen before, and have no idea what they are. Sometimes it is complex or advanced topics that I haven’t learned yet. And sometimes, like you’ll read about today, it is something small that I just missed somehow in the course of my learning.
“Hrm. What is that?”
My team maintains the AWS Account Operator, a Kubernetes operator that automates the creation and management of Amazon WebServices accounts that house our customers’ OpenShift clusters. Recently, I was looking into an issue with how conditions are updated on some of the Kubernetes custom resources the operator uses. While looking through the code base of the operator, I came across some Go syntax I was not familiar with:
// The applicable code, in context:
// https://github.com/openshift/aws-account-operator/blob/cb98b2fa174672e082cd6e52959e19878c640b50/pkg/controller/utils/conditions.go#L15
type UpdateConditionCheck func(oldReason, oldMessage, newReason, newMessage string) bool
This was not something I’d come across before, and my brain did its best to try to rationalize what I was seeing: “Clearly, someone is missing a bracket here.”
Good ol’ brain followed up on that brilliant insight a few seconds later with: “Or, no that’s…wait. Um… what is this?”
Quality troubleshooting brain! Eventually, though, it back-tracked into some solid reasoning: “This is a type, and it is named UpdateConditionCheck
, and types can be strings and slices – even custom things defined by structs. But this type is a function, I think. Some in-line function.”
Great job, brain! After a little Googling, it turns out a type can be a function! Appropriately enough, they’re even called “function types”. At this point, I know they exist, but I don’t really understand how they work, yet.
So back to the code, the UpdateConditionCheck type is a function. But what does that mean? I returned to Google to learn more, and found “Go function type, reusable function signatures” by Jaga Santagostino, and it certainly looked promising. The article is short and sweet, and an attempt to explain how function types work using a code example.
Reading through the example, I have a Greek to Me moment: I feel like it is saying something clearly, but looking at it I can’t quite make heads or tails of it. Even being unable to quite grasp what is going on in the article in front of me, though, it is obvious that the issue here is my skill level with this new programming language, and not a poor example, that much is apparent.
So, I learn better by teaching, (and studies [1] show many others do, too). Now that I work from home and with people smarter than I am, my teaching is entirely limited to blog posts. In order to really understand this, I would need to write about it, so I opened up gedit
and started taking notes for a blog post about function types.
And this line RIGHT HERE is where history catches up with reality for me, so every word after this is happening as I learn it. (That’s a weird feeling. I bet it could be a blog post unto itself. Meta thing is meta.)
How it works
Jaga’s example is a good example, confused as I was at first. The pieces began to fall into place for me when I started writing, because in order to explain it, I needed to go back and break it all down into pieces that could be explained and digested mentally. For example, looking at the function type from the post:
type myFunctionType = func(a, b string) string
OK, that’s a function definition as a type. The type myFunctionType
is a function with the signature[2] func(a, b string) string
. Now, any function that fits that recipe – two strings in, one out, will satisfy that type. That is, any function with two string parameters in and one string returned can be of type myFunctionType[3]
. And that type can then be used by another function – in the case of Jaga’s example, functionTypeConsumer
:
func functionTypeConsumer(fn myFunctionType) {
s := fn("hello", "world")
fmt.Println(s)
}
So we can see the type myFunctionType
is being passed to the new function as fn
and then being called when set to the variable s
. In this case functionTypeConsumer
is aptly named. The type myFunctionType
has to be used by a function, and functionTypeConsumer
it is.
Applying it to real life
So, back to work:
type UpdateConditionCheck func(oldReason, oldMessage, newReason, newMessage string) bool
This is just another way of writing a function type. It could easily have been written the same as myFunctionType
:
type UpdateConditionCheck = func(oldReason, oldMessage, newReason, newMessage string) bool
Right below UpdateConditionCheck
in that same file are two functions who have a signature that matches the UpdateConditionCheck
type:
// UpdateConditionAlways returns true. The condition will always be updated.
func UpdateConditionAlways(_, _, _, _ string) bool {
true
}
// UpdateConditionNever return false. The condition will never be updated,
// unless there is a change in the status of the condition.
func UpdateConditionNever(_, _, _, _ string) bool {
return false
}
These functions are fulfilling the same job as fn myFunctionType
from Jaga’s blog post – they’re going to be called by another function – the “consumer” of the UpdateCondiditionCheck
function type.
Don’t be thrown off by the underscores there: (_, _, _, _ string)
. That’s just a way of saying the function will be passed four strings as input, but doesn’t assign them a variable because it is not going to be using them. This way, that function fits the signature of the type, but doesn’t have to explicitly do anything with the input.
The “consumer” function is a little further down in the file:
// The applicable code, in context:
// https://github.com/openshift/aws-account-operator/blob/cb98b2fa174672e082cd6e52959e19878c640b50/pkg/controller/utils/conditions.go#L35-L44
func shouldUpdateCondition(
oldStatus corev1.ConditionStatus, oldReason, oldMessage string,
newStatus corev1.ConditionStatus, newReason, newMessage string,
updateConditionCheck UpdateConditionCheck,
) bool {
if oldStatus != newStatus {
return true
}
return updateConditionCheck(oldReason, oldMessage, newReason, newMessage)
}
It is slightly more complicated than the example consumer function, but it can be rewritten to look a little more similar:
func shouldUpdateCondtion(oldStatus corev1.ConditionStatus, oldReason, oldMessage string, newStatus corev1.ConditionStatus, newReason, newMessage string, updateConditionCheck UpdateConditionCheck) bool {
if oldStatus != newStatus {
return true
}
s := updateCOnditionCheck(oldReason, oldMessage, newReason, newMessage)
return s
}
That looks a little more like the example from Jaga’s post. There’s some extra logic at the beginning to return true if a specific condition is met, but otherwise it is almost the same. It uses the function type UpdateConditionCheck
which has been passed into the consumer function as updateConditionCheck
, and it is called by setting it to the variable s (as with the example function).
Note: Don’t be thrown off by the arguments to the function type in our example above, either. In this case, the function type is just taking its arguments from the arguments provided to the consumer function, whereas the example function had “hello” and “world” hard-coded as arguments.
Elsewhere in the AWS Account Operator, shouldUpdateCondition
is called with some arguments, the last of which is either UpdateConditionAlways
or UpdateConditionNever
. These are functions with the same signature as the UpdateConditionCheck
type function. It is those functions (UpdateConditionAlways
or UpdateConditionNever
) that our consumer function uses to evaluate the value of s
(or, in the original version of the code, before we rewrite it to be more like Jaga’s example code, immediately evaluate and return the value).
Conclusion
There we have it: I’ve just finished typing this post and I understand how function types work, how Jaga’s example code works, and how this particular bit of code in the AWS Account Operator works. Hopefully this has helped you understand function types as well, and if not, maybe write a blog post about it! I would definitely read it.
Footnotes
- Learning better by teaching: https://digest.bps.org.uk/2018/05/04/learning-by-teaching-others-is-extremely-effective-a-new-study-tested-a-key-reason-why
- Signature? I think signature is the right word here
- A function type can be declared with other function signatures, as well – it doesn’t have to be func(a, b string) string:
type one = func(a, b string) string
type foo = func(x, y int) bool
type myfunc = func(a string, x int, b bool) rune
…whatever.