Tasty: My First Go Program

Aug 16, 2016   #batch2  #golang 

It’s the second day of my second batch of Recurse Center; this morning I got sucked into learning Go to solve a warm-up problem. Each day, Rose (one of the facilitators) writes two small programming problems on the board. Solving them is meant to take maybe 30min max, and get your day started on the right foot. In this post, I’ll be solving the first problem of the day in Go.

The Problem: Doubling A Recipe

The problem is named “Tasty” and involves scaling the ingredients for a recipe. You’re given a number and a list of ingredients with amounts. You’re to output the list of ingredients with the amounts scaled up by number (e.g. if the number is 2, you should double the recipe).

To make things a little more interesting, you also need to normalize the units – each amount should written in the largest unit for which the amount is at least one whole unit. For example, 4 teaspoons should be written as 1.333333 tablespoons because 3 teaspoons is 1 tablespoon and 1.333333 is at least 1. You only need to support teaspoons, tablespoons, cups, and quarts. [Yes, Recurse Center is in the US.]

Smaller Unit Larger Unit
3 t 1 T
16 T 1 c
4 c 1 q

Rose also gave an example of the input and output. The type literals here are the types I’ll define later in this post.

input := Recipe{
		"mayo":        Amount{2, CUP},
		"brown sugar": Amount{8, TABLESPOON},
		"cayenne":     Amount{2, TEASPOON},
}
doubled := MultiplyRecipe(2, input)

//doubled should be:
correct := Recipe{
		"mayo":        Amount{1, QUART},
		"brown sugar": Amount{1, CUP},
		"cayenne":     Amount{1.3333333333333333, TABLESPOON},
}

I have no idea what instructions or result would go with such an ingredients list, but apparently it is delicious enough to double. The goal is to write the MultiplyRecipe function.

My Solution in Go

The rest of this post is going to detail my solution to the problem, focusing on the many things I learned about Go in the process. This is my first Go program from scratch – meaning I’m not counting blindly copy-pasting my way through a web app tutorial. Don’t trust me on matters of Go style or why the syntax that worked for me worked. I’m doing my best, but I’m a Go newbie.

Defining The Types

Because it doesn’t depened on any other types, I’ll show you my Unit type first. It’s basically an enum, since I want to represent teaspoons, tablespoons, cups, and quarts only. These don’t really need any data attached to them – they’re just 4 associated constants.

Go doesn’t have enums, as I discovered when I googled “enum in go”. Luckily, the search results also included instructions on how to construct an enum-like type, using the const keyword thing and a typealias.

type Unit int
const(
 	TEASPOON Unit = iota
 	TABLESPOON
 	CUP
 	QUART
)

I think type Unit int is basically a typealias. I’m defining the name Unit to be a type that’s just an int (a primitive type). This is a little different from typealiases in some other languages – Go will refuse to compare a Unit to an int because the types have different names. For example, TEASPOON == 4 would result in a compile error.

In general, the type keyword seems to use the formula type NewTypeName TypeDefinition. The code above has int used as the type definition, and in a little bit, I’ll show you a struct as the type definition.

There’s two more keywords in this example – const and iota – which work together. The const keyword/function/whatever takes a list (formated the same way as import, one item per line) of constant names and values, and defines them as constants. A line entry can contain CONSTNAME TYPE = VALUE; it can also apparently just contain the name, if you want to use the same type and value that you used on the first line.

iota is a magic variable that’s special to const blocks. At the start of each block it’s equal to zero; it increases by one with each line/const definition. That means the above code will have TEASPOON = 0, TABLESPOON = 1, etc.

Setting the type of TEASPOON to Unit (rather than the default of int) makes the constants (the enum values) have their own type, so they can’t be compared to normal ints. I named my constants in ALLCAPS because that’s what I’m used to seeing in C/C++; I don’t know if that’s normal in Go.

I’ll show you the Amount type next because it only depends on Unit. Also, it’s the struct type, which is the next keyword I want to show you. An Amount represents a number of units – like 3 Tablespoons or 1.3333333 cups. There are two obvious parts to an amount – a number (e.g. 3) and a unit (e.g. teaspoons). The number is a float, so it can represent values like 1.3333333. The unit will be the enum I just showed you.

It seems a bit ceremonious to insist on definig a type for something that’s just a pair of a number and unit, but Go doesn’t really have tuples. You can return a tuple of values from a function (which might make you think you have tuples), but they’re not actually a primitive/built-in type. Instead, I’ll define a struct with two fields.

type Amount struct {
 	Quantity int
 	Unit Unit
}

Go structs are very similar to C structs; they both just have a list of named properties, with each property having a type. You can access the fields by name, i.e.:

a := Amount{3, TABLESPOON}
a.Quantity // == 3
a.Unit // == TABLESPOON

The := is for declaring a new variable name without manually specifying the type of the variable. (You can use = to assign a new value to an existing variable.)

There’s only one more type: the Recipe. This is a list of ingredients, not a full recipe, but I’m calling it a recipe anyway. You don’t have to edit the instructions to scale a recipe up afterall. I’m modelling the list of ingredients as a map from the name of the ingredient to the amount.

type Recipe map[string]Amount

This was easier than I expected. Go has a built-in map type with straight-forward (but not intuitive) syntax. You specify a map type using the form map[KEYTYPE]VALUETYPE. My definition of Recipe is another typealias (like `Unit). I just wanted a more problem-specific word to refer to this type.

The syntax for a Recipe literal turns out to be pretty nice:

original := Recipe{
	"flour": Amount{1,CUP},
	"sugar": Amount{2, TEASPOON},
	"butter": Amount{2, TABLESPOON},
}

I really like how easy this is to read. While “readability of literal representations” isn’t the most important thing when defining a type, it is a really nice feature. The {} braces after the type names (e.g. Recipe{) indicate a constructor. The Recipe constructor takes a map literal (KEY : VALUE,...) and the Amount constructor takes its fields in the order they were defined (number then unit).

Writing the Functions

After setting up the types to model the data, it’s time to actually write the function. Normally, these influence each other more, but in this case the types were largely determined by the problem specification. The scaling “algorithm” has two stages:

  1. scaling up each amount
  2. normalizing the units

Since the scaling function is also the MultiplyRecipe function in my implementation, stage 2 is just a helper function normalizeUnit. I know those names look super inconsistent – one is UpperCamlCase and the other is lowerCamlCase. This is a Go thing – exported names are upper case and unexported/private ones are lower case. As in, not just a style convention, but the capitalization of the names is what causes them to be exported. It’s a bit weird.

The scaling function will iterate over the ingredients and multiply the number part of each amount by the scaling factor. Then the function will call the normalize function on each scaled amount. Once this has been done for all the amounts in the recipe, the new recipe will be returned.

// Multiplies each amount in the Recipe by multiple, then normalizes the units.
func MultiplyRecipe(multiple float64, original Recipe) Recipe {
	scaled := make(Recipe)
	for key, value := range original {
		var n Amount = value
		n.Quantity = multiple * value.Quantity
		n = normalizeAmount(n)
		scaled[key] = n
	}
	return scaled
}

Functions in Go have type signatures that include the argument types and the return types. MultiplyRecipe takes a float64 and a Recipe and returns a Recipe. normalizeAmount takes an Amount and returns an Amount. Putting a comment on the line above the function definition means that comment will be associated with this function in the documentation produced by godoc.

The make function initializes an instance of a given type – make(Recipe) gives me a map that’s ready to have keys added to it. The range keyword seems to cause a for loop to be able to iterate over a data structure – on a map, it produces (key, value) pairs; on a list, it produces (index, value) pairs. If you only put one loop variable between for and :=, then you only get the first value from the pair (e.g. indexes on a list). If you only want the second value of the pair, you need to use an underscore as the name of the variable you don’t want (e.g. for _, value := range mymap).

// The unit will be upgraded if the amount is at least one whole of a higher unit.
// Conversions: 3 t = 1 T; 16 T = 1 c; 4 c = 1 q
func normalizeAmount(a Amount) Amount {
	if a.Unit == TEASPOON && a.Quantity >= 3 {
		a.Unit = TABLESPOON
		a.Quantity = a.Quantity / 3
	}

	if a.Unit == TABLESPOON && a.Quantity >= 16 {
		a.Unit = CUP
		a.Quantity = a.Quantity / 16
	}

	if a.Unit == CUP && a.Quantity >= 4 {
		a.Unit = QUART
		a.Quantity = a.Quantity / 4
	}
	return a
}

There’s no new syntax to comment on here. All the equality and boolean-and syntax is familiar from C/C++ and similar languages.

Testing The Solution

While I could run an example input and output through my functions pretty easily, I also wanted to setup “real” unit tests. I’m glad I did because Go’s built-in go test stuff is actually pretty cool. It handles both unit tests and documentation examples – examples that are run as tests so that you never accidentally break them without noticing. Considering how prevalent an issue outdated examples are, making it easy (i.e. a default) to test examples is a great design choice.

Once I get a unit test and an example passing, I’ll declare thie problem solved. (I know, Dan, I should use generated tests. Like, thousands of them.1) I’m just going to use the one example from the spec for both the test and the example; this will make it a bit easier for you to follow the two different syntaxes.

The example and test setups are pretty similar and feel to me like slightly different ways of doing the same thing. The example function takes no arguments and you can only test terminal output. This makes sense, since example code that prints out the values is helpful – you can see that the code worked when you ran it. The test function takes a special testing.T object; all I’ve found to use that for is throwing an error in a way the testing framework expects.

Because go test works largely on convention, I’m going to tell you a bit more about our setup in the filesystem. All of the code so far is in one file, in a package called tasty. It’s in a folder called tasty in my Go workspace’s src directory.2 My Go workspace is a directory. It is whichever path my environment variable $GOPATH is set to. The $GOPATH is extremely important here – you want to be at that path when you run go test tasty. And you want your code to be in the folder $GOPATH/src/tasty. This is the only way go test will find the code to test.

Our code so far is in a file called tasty.go. Our test and example code will go in the file tasty_test.go. The test file needs to end in _test so that go test knows to look in there. Inside that file, there will be a series of functions. Some of them will have names that start with Test (the unit tests) and others will have names that start with Example (the code examples).

The names of the unit test functions are no more crucial than test names in any other language – they just help you find the test that failed and understand what you were testing when you try to read the code. The names of the example functions are another convention – ExampleFunctionName, where FunctionName is the name of the function being documented. godoc -ex=true tasty will show print out the documentation for all the exported functions in the tasty package – i.e. MultiplyRecipe. The -ex=true makes the example code print out in the terminal; by default, it’s only visible in the website verison.

Even though learning all of these conventions and getting my files in just the right place is annoying, I’m glad Go does most things this way. The one thing that required configuration, setting $GOPATH, was so annoying that I don’t want to have to configure anything Go-related if I can help it.3

Now that you’ve gotten a ton of background on go test, here’s the actual file, which contains one example and then one test:

package tasty

import (
	"testing"
)

// Double a simple recipe.
func ExampleMultiplyRecipe() {
	original := Recipe{
		"mayo":        Amount{2, CUP},
		"brown sugar": Amount{8, TABLESPOON},
		"cayenne":     Amount{2, TEASPOON},
	}
	doubled := MultiplyRecipe(2, original)
    PrintRecipe(doubled)
    // Output:
    //1 c   brown sugar
    //1.3333333333333333 T   cayenne
    //1 q   mayo

}

func TestDoubling(t *testing.T) {
	// Sample Input
	var original = Recipe{
		"mayo":        Amount{2, CUP},
		"brown sugar": Amount{8, TABLESPOON},
		"cayenne":     Amount{2, TEASPOON},
	}

	var correct_doubled = Recipe{
		"mayo":        Amount{1, QUART},
		"brown sugar": Amount{1, CUP},
		"cayenne":     Amount{1.3333333333333333, TABLESPOON},
	}

	// Double the amounts
	var doubled = MultiplyRecipe(2, original)

	// Check all keys in doubled are correct
	for key, value := range doubled {
		if value != correct_doubled[key] {
			t.Error("For ", key, ", expected ", correct_doubled[key], " got ",
				value)
		}
	}

	// Check all keys in correct_doubled are correct
	// catches i.e. missing keys in doubled
	for key, value := range correct_doubled {
		if value != correct_doubled[key] {
			t.Error("For ", key, ", expected ", correct_doubled[key], " got ",
				value)
		}
	}
}

The // Output: comment lets me specify what should be printed to stdout by this example code. The actual output will be checked against my provided output by go test tasty.

$ go test github.com/astrieanna/rose-warmups/tasty
ok  	github.com/astrieanna/rose-warmups/tasty	0.005s

This problem was relatively straight-forward to solve – as it was designed to be. Most of my time writing the code was learning Go – how to declare types, how to arrange my files, how to write tests and get go test to run them. I’m pleased with this first experience with writing Go code (except for having to setup $GOPATH), so I expect to write more code in Go during my batch.

This will probably be the first of many pivots in my plans, since I wasn’t especially interested in writing Go code when I was thinking of things to do here. It was just the easy-language-with-types that came to mind when I wanted to solve a quick problem.

Acknowledgements

Thanks to Dan Luu for reading the drafts of this post and giving useful feedback. Thanks to Rose Ames for creating the Tasty problem and sharing it.

Footnotes


  1. I thought thousands was a large number of tests. Dan’s reaction to reading that sentence makes me think that he thinks that’s a low number of tests. ↩︎

  2. This is a simplification. My go workspace is in $HOME/mygo; my folder for this project is $HOME/mygo/src/github.com/astrieanna/rose-warmups/tasty. I don’t think that the full name of my github repo is relevant to the main part of this blog post. ↩︎

  3. This was problematic for two reasons: I didn’t know what it was supposed to be set to, and I didn’t know that you need to set it in .profile on a mac, not .bashrc to get it to actually be set in a new Terminal window. While I feel like my lack of understanding was a problem here, I read multiple explanations to figure this out such that it works, but it only took my one resource to understand test and example conventions. ↩︎