The empty interface in function types


The empty interface in function types

5 minute read

I was coding recently and noticed duplicated logic scattered across my code. The difference in matching code segments was a single line where a function was being called.

The functions being called all had similar signatures. They took no parameters and returned a single value. The only difference was the type of their return value.

In an attempt to refactor the duplicated logic I created the function type f:

type f = func() interface{}

I extracted the repeated logic into a function which took a function of type f as a parameter.

Since the empty interface can hold values of any type I figured that f could act as a generic function type.

func do(fn f) interface{} {
    // logic ...
    x := fn()
    // more logic ...
    return x
}

func fnReturningString() string {
    return "Houston, we have a problem"
}

func fnReturningInt() int {
    return 42
}

func main() {
    do(fnReturningString) // doesn't compile
    do(fnReturningInt) // neither does this
}

Running the code above gets you the following compiler errors:

cannot use fnReturningString (type func() string) as type func() interface {} in argument to do
cannot use fnReturningInt (type func() int) as type func() interface {} in argument to do

This initially seemed weird to me.

f returns a single value, the empty interface. fnReturningString and fnReturningInt also return a single value, albeit a different type. Since a string and and an int can be assigned to the empty interface I expected the code to work.

I investigated why it didn’t work and in the rest of this post I explain my findings.

Types and interfaces

To help understand why let’s first refresh our knowledge of types and interfaces.

Go is statically typed. Every value has a static type which is known and fixed at compile time: string, []int, *bytes.Buffer and so on. Unless two variables share the same underlying type they cannot be assigned to one another.

var i int = 5
var s string = "5"
s = i // this isn't Python - compilation obviously fails

type MyStr string
var ms MyStr = "7"
s = string(ms) // OK - same underlying type and a conversion

Interfaces are a special category of types which represent a fixed set of methods. They can store any concrete value so long as the value implements the interface’s methods.

They are no different to regular types in that they are also statically typed. An io.Reader interface is always of type io.Reader regardless of the concrete value it holds.

var r io.Reader = &os.File{}
var rw io.ReadWriter = &os.File{}

r = rw

r.Write([]byte("Yada, yada, yada...")) 
// r.Write undefined (type io.Reader has no field or method Write)

In the code above we can assign rw to r because io.ReadWriter clearly satisfies the io.Reader interface. However we’re not then able to call Write on r. This is because r is an io.Reader and always will be – regardless of the value assigned to it.

To call Write we’d have to use a type assertion which exposes the underlying concrete value.

r.(*os.File).Write([]byte("I see"))
r.(io.ReadWriter).Write([]byte("dead people"))

The empty interface

Arguably the greatest source of confusion among Go newcomers is the empty interface.

It represents the empty set of methods and since all values have zero or more methods they all implement it. This means that we can assign any value to an empty interface variable.

var empty interface{}
empty = "5" // empty contains a string
empty = []int{1, 2} // empty contains a slice of ints 

empty is statically typed. It is of type interface{} and always will be, just as a bool value is of type bool. It is the concrete value and type inside the interface which can change.

Whenever we assign a concrete value to an interface it gets converted to an interface during runtime. This is what happens for example every time you call fmt.Println and friends.

There’s an excellent post by Russ Cox if you’d like to dive deeper into interfaces.

Function types

The Go spec defines a function type as the set of all functions with the same parameter and result types.

In the beginning of this post I defined f:

type f = func() interface{}

With our newfound knowledge of interfaces and function types we can now formally define f as:

The set of all functions that take no parameters and return a single result of type interface{}.

The important point here is that f returns a static type interface{} and not any type as I had initially assumed. The fact that any value can be assigned to interface{} is not relevant. Doing so changes the concrete value and type stored inside the interface, not the result type of f.

This is clearly different than the type of fnReturningString and fnRetunrningInt.

We can confirm this using the reflect package:

fi := func() interface{} { return "back to" }
fs := func() string { return "the future" }

fmt.Println(reflect.TypeOf(fi))
// Output: func() interface {}

fmt.Println(reflect.TypeOf(fs))
// Output: func() string

fmt.Println(reflect.TypeOf(fi) == reflect.TypeOf(fs))
// Output: false

What can we do?

I couldn’t use f as intended because the functions assigned to it did not have a matching type! It’s also one of the reasons why you can’t assign a []T to an []interface{}.

Still, I’ve got duplicated code that I’d like to eliminate. What options do we have?

Matching return types

We can make our functions match f by changing their return types.

func fnReturningString() interface{} {
    return "I have mixed feelings about this"
}

func fnReturningInt() interface{} {
    return 99
}

While this works it’s not ideal. If the functions were coming from another package we wouldn’t be able to modify them. Moreover we’d be losing type safety and the self-documenting property that comes with it. Is there a better way?

Anonymous functions

We could wrap our functions in an anonymous function whose type matches f.

type f = func() interface{}

func do(fn f) interface{} { ... }

func fnReturningString() string {
    return "Hakuna matata"
}

func main() {
    do(func() interface{} { return fnReturningString() })
}

There’s definitely a higher cognitive cost to pay here when reading the code. But we do get to keep the original definitions untouched and they’re clear on what they return. I prefer this way, though I would keep the do function unexported if at all possible.

Reflecting

There’s also a third way where do takes a parameter of type interface{} (instead of func() interface{}) . This way we can pass any function as a parameter. We’d then need to use reflection to expose the underlying function.

I leave it as an exercise for the reader but I’d like to point out that this approach has many flaws. Reflection should be used as a last resort and we’ve already found better solutions.

Generics

Maybe, soon?

Conclusion

Every value in Go has exactly one type that is known and fixed at compile time. Interface values are no exception, they too are statically typed.

Functions are defined by their set of parameter and result types. A function returning an int is a different type from a function returning an interface{}. Just like it’s different from a function returning a []bool.

I couldn’t assign my functions to f as they were different function types. This might’ve been obvious to some, but it wasn’t for me. It is now though!


Discussion: HN, Reddit



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *