Go Can Have Variant Types
…how and why to use them
Here I discuss my use-case for variant types in Go, how to implement them, and the pattern I have established for their safe use. I have included some real-world code rather than the traditional Go “animal farm” examples but for the short version skip to the TD;DR at the end.
Here I discuss my use-case for variant types in Go, how to implement them, and the pattern I have established for their safe use. I have included some real-world code rather than the traditional Go “animal farm” examples but for the short version skip to the TD;DR at the end.
Background
I am working on a large spare-time project which currently consists of a little over 7000 lines of Go, I expect that to double before it is released.
I started off inspired by an extant Java project which had related aims, but the Java style was very idiosyncratic and I found the code hard to read. For historical reasons I flirted with PL/1, and that might have been a goer if only it were a little more mature; unfortunately I hit bugs in the compiler which I didn't want to work around. Then I converted the code to good old plain C which was fine until I wanted platform-neutral threads and filesystem I/O. I know there are libraries to solve these problems, but I didn't want to learn large libraries to deal with them. Finally I have settled on Go and I am very happy with my choice which has accelerated the development of my project significantly.
Go offers me just about everything I could want in a general purpose programming language without being cluttered with features which I will never use. Amongst other things it is strongly-typed…
I started off inspired by an extant Java project which had related aims, but the Java style was very idiosyncratic and I found the code hard to read. For historical reasons I flirted with PL/1, and that might have been a goer if only it were a little more mature; unfortunately I hit bugs in the compiler which I didn't want to work around. Then I converted the code to good old plain C which was fine until I wanted platform-neutral threads and filesystem I/O. I know there are libraries to solve these problems, but I didn't want to learn large libraries to deal with them. Finally I have settled on Go and I am very happy with my choice which has accelerated the development of my project significantly.
Go offers me just about everything I could want in a general purpose programming language without being cluttered with features which I will never use. Amongst other things it is strongly-typed…
Why Variant?
Part of my project is rather like an interpreter, it decodes instructions and then processes them. There are several sensible ways of logically grouping the instructions; two of these ways are: by operand(s) and by functionality. In fact these are the two ways I handle the instructions - I parse by operand types, then process according to functionality group. There is not a one-to-one mapping between the two. I have a large set of operand types, and a smaller set of functions to handle them - I do not want to write functions for every type, I want my functions grouped by their, er, functionality.
Initially I had one large type which could contain all the possible instruction operand types and combinations…
Initially I had one large type which could contain all the possible instruction operand types and combinations…
type decodedInstrT struct { mnemonic string instrFmt int instrType int instrLength int disassembly string c, ind, sh, nl, f byte t string ioDev int acs, acd int skip string mode string disp8 int8 disp15 int16 disp16 int16 disp31 int32 offsetU16 uint16 bitLow bool immU16 uint16 immS16 int16 immU32 uint32 immS32 int32 immWord dg.WordT immDword dg.DwordT argCount int bitNum int }
…a bit of a monster! But it did the job, so why change it? The catch-all type above was dangerous because any given instance of it would have some fields set and others unused - it was entirely up to the programmer to ensure that the right fields were used in the right context, i.e. it was possible to set some fields, then send the struct to a function which expected other fields to be used. I was effectively circumventing Go's type safety for functions with my all-encompassing type.
interface{} To The Rescue!
I must confess that, much as I love Go, I am still waiting for an epiphany regarding its implictly satisfied interfaces, and
interface{}
feels far too much like C's * void
for my comfort. But although interface{}
can refer to anything you don't have to let it. Here is my new struct:type decodedInstrT struct { mnemonic string instrFmt int instrType int instrLength int disassembly string variant interface{} }
That's just the invariant part which is common to all instances. Immediately following I have a lot of these…type oneAccImmWd2WordT struct { acd int immWord dg.WordT } type oneAccImm3WordT struct { acd int immS32 int32 } type oneAccImmDwd3WordT struct { acd int immDword dg.DwordT } type oneAccMode2WordT struct { acd int mode string disp16 int16 bitLow bool } type oneAccMode3WordT struct { acd int mode string disp31 int32 } type oneAccModeInd2WordT struct { acd int mode string ind byte disp15 int16 } ...
Type-correctness is now enforced by a simple usage pattern for populating the struct…case oneAccImmWd2WordFmt: [...populate invariant portion...] var oneAccImmWd2Word oneAccImmWd2WordT // to hold the variant data [...populate variant portion...] decodedInstr.variant = oneAccImmWd2Word
And a simple pattern to use the data…
var oneAccImmWd2Word oneAccImmWd2WordT [...] case "XYZ": oneAccImmWd2Word = iPtr.variant.(oneAccImmWd2WordT) [...use the variant data...]
The type assertion .(oneAccImmWd2WordT)
ensures that we are using the right variant in the right place. It will panic if the programmer has not used the expected variant type.
TL;DR
Define your struct-with-variant-portion like this:
type invariantAndVariantT struct { ...invariant data... variant interface{} } type varType1 struct { ... } type varType2 struct { ... }
Populate it like this:var iAndV invariantAndVariantT ...populate invariant data... var varData1 varType1 // create the variant portion ...populate varData... iAndV.variant = varData1 // put it in the combined struct
Use it like this:var varT1 varType1 varT1 = iAndV.variant.(varType1) // this asserts that the expected variant was set ...use the VarT1 data...
Comments
Post a Comment