Thursday, April 6, 2017

Go types and assignability

Yesterday, during our weekly coding dojo, we had a bit of a puzzling moment when we realized one of the rules of assignability in Go.

A coding dojo is a suitable place for these "aha" moments, because we have freedom to try things and take the time to understand details that we could possibly otherwise let go and simply never come across.

On the assignability of values with identical underlying types

When writing table tests in Go, it is a common idiom to have a slice of structs to hold data for each test case. Our struct had a field of a custom type, a tic-tac-toe Game, and the underlying type was a slice of slice of ints ([][]int).

We experimented with different ways of writing a composite literal to create a value holding all test cases including the Game field.

This was what we eventually committed after the dojo: tictactoe_test.go#L6-L18. However, a single Git commit does not capture all the alternatives we tried.

This Go Playground snippet shows how we can use Game and [][]int interchangeably in the context of an assignment, and why we can do it I highlight in this quote from The Go Programming Language Specification:
A value x is assignable to a variable of type T ("x is assignable to T") in any of these cases:
  • x's type is identical to T.
  • x's type V and T have identical underlying types and at least one of V or T is not a named type.
  • T is an interface type and x implements T.
  • x is a bidirectional channel value, T is a channel type, x's type V and T have identical element types, and at least one of V or T is not a named type.
  • x is the predeclared identifier nil and T is a pointer, function, slice, map, channel, or interface type.
  • x is an untyped constant representable by a value of type T.
Game and [][]int have identical underlying types ([][]int), as follows from the definition of types:
Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its type declaration.
And [][]int is not a named type, thus satisfying the assignability rule.

The rule applies recursively!

Yes, it also works if we have intermediate named types, for example if we would define Game as a slice of Row.

What does not work

It is as well interesting to explore what is not allowed by the assignability rules.

Two named types with the same underlying type are NOT directly assignable

We need at least one of the types involved in an assignment to be unnamed, see how this snippet failed to compile.

Note, however, that an explicit conversion is possible.

I think this is reasonable, because two named types can have completely different method sets, and we don't want implicit type conversions... well, the reason why I said assignability was "puzzling" at the very beginning is that it would be probably okay if a [][]int literal was not assignable to a Game. I suspect the reason that assignment is possible has to do with either convenience or some implication for the type system I have not thought of.

So that's the summary of one of the things we learned in the dojo yesterday. Did you know about it? Was it useful?