In my previous blog post I had a brief mention about the Sapir-Whorf hypothesis, and how it blind-sided me to an obvious design. In this blog post I’ll retell yet another story of how Sapir-Whorf blind-sided me yet again - just moments, actually. I’m getting sick of this shit, yo.
Sapir-Whorf, Briefly
Briefly, the Sapir-Whorf hypothesis states that the language you speak influences the way you think. The proper term for it is “linguistic relativity”. It comes in two flavours: strong and weak. The strong hypothesis states that the language you speak determines the way you think. The weak hypothesis states that the language you speak influences the way you think.
I personally speak a number of languages. I think in more than languages though - sometimes in my mind I create my own language to be able to express my thought to itself. But from an anecdotal point of view, language does more or less constrain what can and cannot be expressed, and expression or lack thereof may lead to a lack of thinking.
For example, there is no generic “yes” or “no” in Chinese - at least not in the same way English has. And that leads questions to be framed in a certain way in Chinese. Sometimes I find that when I phrase certain problems in different languages, different answers emerge.
What I Was Attempting
What I was attempting was to get testing/quick
to generate constrained Value
s for a number of tests I’m upgrading in Gorgonia, which is a library for deep learning in Go.
For the uninitiated, testing/quick
is a package in Go’s standard library that allows you to do quickcheck style tests. I’ve come to rely on it quite a lot, and I think it’s amongst other things, one of the most underrated libraries in the stdlib.
What it does is it generates a random value to be used in your testing functions. So for example, if I want to test an Add
function, what I’d test are the guaranteed properties of addition:
- Associativity - (a + b) + c = a + (b + c)
- Commutativity - a + b = b + a
- Identity - a + 0 = a
In the above tests, a
, b
and c
should be randomly generated. This way you can guarantee that the Add
function will work for any given values.
Quick Checking
Go’s trick to quickcheck-style testing lies in the Generator
type. Any type implementing a method Generate(r *rand.Rand, size int) reflect.Value
will be able to generate a random value. Of course you have to define what comes out of the Generate
method. There are some shortcomings (namely the Go package does not do shrinking), but that’s something I can live with (there are alternative libraries like GOPTER, which follows the QuickCheck methods more, but can be quite finnicky to use).
The quick.Check
function takes a function, usually of this form:
func fnName(a, b, c Type) bool {
// testing code here, return true if ok, false if not.
}
The number of arguments the function takes is arbitrarily as many as you’d like, but the standard practice is to test with one or two only. The constraint of course is that Type
has to implement a Generator
interface or is a type that testing/quick
knows how to generate random values for.
So for custom types, you have to implement your own Generator
. This is typically fine, until you want to test functions that take interface types.
Pieces of a Puzzle
So here we are. Here are the pieces of the puzzle:
The Definitions
type A struct{ a, b int }
type B [2]int
type Fooer interface {
Foo()
}
// A and B implements Fooer
func (a A) Foo(){}
func (b B) Foo(){}
// Add is a function that takes two Fooers and returns a Fooer
func Add(a, b Fooer) Fooer {
// assume it returns something
}
The Task
Write a quickcheck style test for Add
. For this, just write a test for commutativity. It’d look something like this:
func testCommutativity(a, b Fooer) bool {
// fill in the blanks
}
The Question
The question is how you would generate a Fooer and pass it into a quick.Check
function? Let this simmer in your mind for a bit.
Exploration of Solution Space
One of the answers I had to this was quite simple:
type Fooer interface {
Foo()
quick.Generator
}
The problem was I didn’t want pollute the interface with the Generator
interface. In Go, it’s preferable to have small interfaces (and I’m a major sinner of that as it is), and adding a Generator
interface to it simply adds to the surface area of the interface. So that was an instant no-go for me.
Another solution was to wrap the Fooer
interface in a struct, and then have methods on the struct:
type Fooer2 struct {
Fooer
}
func (f Fooer2) Generate(r *rand.Rand, size int) reflect.Value { ... }
This turns out to be a pretty good answer. But upon stumbling on to this answer, it highlighted how blindsided I was by the syntax, and how much the syntax affected the way I solved things.
The Answer
An alternative answer that I eventually settled on was quite simple:
type FooerGenerator struct{}
func (g FooerGenerator) Generate(r *rand.Rand, size int) reflect.Value {
// randomly generate A or B
}
Why didn’t I come to this sooner? I suspect it’s because syntax matters. The syntax makes me prefer one way of thinking over another - I’m treating A
and B
as object. And hence I’d struggle to think of anything outside the box. The flip side of this is when I go back to writing Python or Java or Scala I’m so used to the Go object model that I treat object classes as objects.
Another reason why I hadn’t come to this was because my mind was fixated on this function signature: func testCommutativity(a, b Fooer) bool
. Fact is sometimes I do forget that I have control over what I wrote. Wrapping it into a struct meant I had to write this: func testCommmtativity(a, b Fooer2) bool
which then broke the dyke, and let the flood of new ideas through.
Polyglotism
In the previous blog post I’ve mentioned about how this syntax func (r receiver) methodname(arg argType) retType
leads to a mindset that is very linked to the idea of object-oriented programming. Now, I’m NOT knocking on Go’s syntax (which I believe to have one of the best UX for developers). This is more a knock on the human cognitive processes (specifically mine) that biases towards and favours previously known patterns (OOP-style methods).
I’m tempted to say that this is a something that happens to most other people as well, but I don’t have enough data to support this statement. Having said that, I do think that there are certain benefits to polyglotism. Between the first (and most obvious) solution to (the one where the interface is polluted) the accepted answer, there were a few others in between. I had simply asked myself “how would I solve this in Language X”. Those thoughts led to a long windy journey of thinking, and I even amused myself with some contemplations on changes that can be made on Go (I do have a relatively wild imagination).
Often the advice I’ve given and received is “Don’t do Language X’s patterns if you are programming in Language Y”. That is generally sound advice. But also one that is short-sighted. Without exploring how I would have solved the problem in other languages I wouldn’t have come to the obvious solution. What is needed, in my opinion is a diversity of thinking tools. Being fluent in more than one programming language allows for that. Being fluent in only one language locks you into one way of thinking (at least for me).
Conclusion
The point of this post I guess, is to remind myself that solution spaces are often wider than I’d expect. Being a polyglot helps in exploring various pockets of the solution space. I’d like to hear from you and see what you think. Thoughts?