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.

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…

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…

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

Popular posts from this blog

Terminal Emulation - Fun with a Go GUI

Writing Better Go: Code or Data?

Lessons Learnt: Porting an application from Go to Ada