Effective Not-Java: Type Safe Containers
Table of Contents
I can’t take credit for this joke, but I once heard someone remark that:
The best C++ book is Effective Java.
Comedy is better when there’s an element of truth. I find this statement both funny and — in some sense — true. But how is it true, exactly? Why does it resonate with me?
Good Advice is Good Advice #
I don’t think it has anything to do with C++ or Java. I would explain it like this:
- Effective Java is a good book that gives practical advice for writing Java code that avoids pitfalls.
- A lot of that advice is applicable to other languages, which may or may not have an equivalent text.
- Therefore, Effective Java can be a good book for other languages.
I thought about this recently while reading some Go code that made heavy use of the built-in integer types in its API, as well as type conversions between them internally. The details aren’t important; for this purpose, it’s actually better if you don’t know them. Imagine methods like:
func (i *Index) Read(in int64) (out uint32, pos int64, err error) {
// ...
}
What are in
, out
, and pos
? Are these values related? Are they convertible?
Well, as far as Go is concerned, they are:
whatAmI := in + pos // Add an "in" and a "pos" - does this represent anything?
out = uint32(in) // An unsigned 32-bit "out" from a signed 64-bit "in" - seems fun!
index.Read(pos) // Oops, passed a "pos" instead of an "in".
But in reality, none of these usages made sense. While I’m generally supportive of using built-in types and the standard library, this is the problem with unthinking application. Standard types come with a lot of functionality that may not map to your problem domain. Any mistake that can be made will be made; it’s just a matter of time.
Type Safe Containers #
Of course, if you’ve read Effective Java, you’ve seen this problem before.
One solution is a type-safe container (item #33 in the book, if you’re curious). The idea is simple.
Instead of an int
or Integer
, we use a generic type where the type parameter represents the
actual numeric value, and the outer box type represents the usage. For example:
FileOffset<Integer>
It’s clear from the type alone that this represents an offset into a file, which is stored numerically as an integer. Now it’s up to you whether arithmetic, conversions, and other operations make sense in your context.
Why don’t we see more of this in Go? Unlike Java, where the boxed type does come at a cost, there’s little downside to something like the following:
type FileOffset struct {
Int64 int64
}
There’s no boxing here; FileOffset
is 8 bytes.
An annoying consequence of this approach in Java is that you now need constructors or factory methods to create the type, and getter methods to access its numeric value:
var offset = FileOffset.of(42);
var value = offset.get();
Not so in Go. The code is also nicely self-documenting:
offset := FileOffset{42}
value := offset.Int64
You can ban unsafe conversions and arithmetic in your mainline code and consolidate supported operations into helpers. Consider the case above of converting a 64-bit value to a 32-bit value:
type FileOffset64 struct {
Int64 int64
}
type FileOffset32 struct {
Int32 int32
}
func ToOffset32[T FileOffset32 | FileOffset64](t T) (offset FileOffset32, ok bool) {
switch x := any(t).(type) {
case FileOffset32: // 32-bit offset; no conversion required
offset = x
ok = true
case FileOffset64: // 64-bit offset; safe if within 32-bit range
if math.MinInt32 <= x.Int64 && x.Int64 <= math.MaxInt32 {
offset = FileOffset32{Int32: int32(x.Int64)}
ok = true
}
}
return offset, ok
}
func main() {
offset64 := FileOffset64{math.MaxInt32}
offset32, ok := ToOffset32(offset64)
// ok = true, no issue
offset64.Int64 += 1
offset32, ok = ToOffset32(offset64)
// ok = false, value out of range
}
If you also handle built-in types like int
here, ToOffset32
becomes a general-purpose factory method even
if you want to assign a constant value.
Meet The New Problems, Same As The Old Problems #
So I’ll ask again — why don’t we see more of this in Go? Effective Java is a pretty good Go book as well.