Don’t Worry About Anything, Just Call Us
Callbacks are great and always have been. Long ago, we didn’t know we were injecting control or inverting dependencies; we just used them. When closures appeared, I did not see a huge difference: a little state here, a little context there. I have found my Closures Killer App: remote cleanup
Here is an example in Go:
In the calling code (with dependencies on DataDog, Redis, etc.)
type cleanupSignature func(aName, aType string) func()
cleanupFactory := func(aName, aType string) func() {
if _, found := SpanFromContext(context); !found {
span, _ := datadog.SpanFromContext(context, aName, aType)
return func() {
span.Finish()
}
} else {
return func(){}
}
}
baz := foo(cleanupFactory, bar)
// ...
In the called code (with no dependencies on context, DataDog, etc.)
func foo(cleanupFactory cleanupSignature, zap int) string {
cleanupFunction := cleanupFactory("my name", "my type")
// this is like a `finally`, but lives at the top of a function
defer cleanupFunction()
// ...
The cleanupFactory is more than a factory: in the example, it might create and start a DataDog Span. It closes (encapsulates) the caller’s context and the reference to DataDog. It decides what cleanup its actions need, possibly being no actions at all. If the factory creates and starts a Span, the function it returns encapsulates the Span reference needed to clean up. The host function has no knowledge or access to this value
Rather than the function foo()
holding extra values returned from the factory to control if and how the cleanup function works, the factory always returns only a simple function which foo()
blindly calls. If nothing needs doing, the cleanup function is a nop. If it needs to do things, the cleanup function has encapsulated the values it needs to do them rather than requiring foo()
to pass arguments
from a story by Nick Potts