I've recently been trying to learn Go properly - as I find myself in awkward position of having to use it at work.
I've been finding these videos by Matt Holiday very helpful. They expect a reasonable amount of existing knowledge, and as a result can dive into the weeds a bit, which is grear if you want to know enough to be really dangeorus.
Anyway, when he introduces Go's Maps, I found myself utterly horrified.
// Declare but don't intialise a map
var missingMap map[string]int
// We are able to read values from it?!
fmt.Println(missingMap["test"]) // 0
missingMap["test"] = 5
// If we try and write to it the program hard crashes?!?!>!/
So maps are nullable. But when they are null we can still read from them. And also when they are null if we try to write to them the program will panic.
In what world does this make sense? In what world is this behaviour useful or desirable? I already disliked, but had accepted, all the null pointers floating around in language. But randomly having some operations work on things that don't yet exist is a step too far for me.
There is another more interesting thing about maps that disturbed me. That is, what gets returned when you read a key from a map, and there is no value for that key.
m := make(map[string]int)
fmt.Println(m["test"]) // 0
Based on the datatype in the map, go tries to pick a sensible 0 value. Some examples
We even get the same functionality when working with complicated data structures, like maps! And we can recursively get nil values from nil values
mm := make(map[string]map[string]int)
fmt.Println(mm["test"]) // map[]
fmt.Println(mm["test"]["test"]) // 0
My first response, like with so many things, was to hate this behaviour and direct psychic attacks at everyone involved in Go on any level. In my eyes these nil values are different to something not being present. It is often the case that you might want to record that something has a value of Zero, and not get this mixed up with never having recorded a value at all.
Thankfully go does give us an escape route. Reading from a map actually returns two values.
m["test"] += ""
var x, y = m["test"]
var xx, yy = m["missing"]
fmt.Println(x, y) // "" true
fmt.Println(xx, yy) // "" false
So we can tell if a 'zero' value is present because we added it by looking at the boolean value that gets returned alongside the value that we read from the map.
It's a relief to know that Go's maps do have the ability to tell us whether a key exists within them - but we are left with the question.
So, we've seen that Go's maps return a zero value when we read from them. But why would you want that? I assume that the core reason is that this design makes it easier to accumulate values inside a map.
If you write JavaScript you've probably written something like this
arrayOfThings.reduce((acc, x) => {
if (acc[x]) {
acc[x] += 1
} else {
acc[x] = 1
}
return acc
}, {})
We're building up a map of some data, but at each step we need to check whether there is already a value present to know what to do. If writing to JavaScipt objects behaved like go maps we could simplify the above to
arrayOfThings.reduce((acc, x) => {
acc[x] += 1
return acc
}, {})
If we are good wordcel programmers, we'll have realised that what go is doing bears some similarity to monoids. Monoid are a set of things with a way to combine things, and a 'zero' element. We see them formalised and used widely in languages like Haskell and PureScript.
Monoids have the following properties
All the nil elements returned by Go's maps have these properties. Even when we declare a map of maps, our nil element is an empty map. We can join maps together (manually), and adding an empty map to another map leads to no changes.
You might have notice that I skipped over something a bit weird a few paragraphs ago. I said that True was a zero element. But surely False is equivalent to Zero. Well it depends on whether you are using and
or or
as your fundamental operation. If you care about and
then True is zero, if you care about or
then False is zero.
What was the point of this article? I don't know.