Skip to content

Instantly share code, notes, and snippets.

@vkorbes

vkorbes/blog.md Secret

Created March 15, 2018 00:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vkorbes/3ffc736c6fe4f00412e1b3c15210ae18 to your computer and use it in GitHub Desktop.
Save vkorbes/3ffc736c6fe4f00412e1b3c15210ae18 to your computer and use it in GitHub Desktop.

memdec - Writing the silliest Go API ever

I've been interviewing with a few companies the past month, and as part of these interviews, the other day I was asked an API as a take-home code challenge. I had a lot of fun doing it, so I thought I'd make an articles/videos series on the subject.

I'm obviously not gonna use the exact same API from the interview, which left me with the question of what to do for these.

I then I came up with the dumbest, greatest idea I ever had!

First, some background:

There's a thing called the World Memory Championship, where among other things people compete in memorizing the order of shuffled decks of cards.

I've learned the technique and it's amazingly fun. No, really, I swear!

For real, it's awesome. I love it. The problem is, it's a pain to learn. And most of it isn't even complicated, it's just a lot of work. For some time know I've wanted to make an app to aid in this process, so let's do that. Let's write the API for this fictional app to use.

I'm calling it memdec. Meaning memorize decks, memory decoder, whatever.

A warning: The goal here is to learn cool stuff and to have tons of fun doing it. I'm not gonna stress about doing things the best possible way they can technically be.

So let's get to it.

The Dominic system

Before we can start writing any code, or even just the spec, we need to be clear on what we want to achieve.

My goal here is to aid in learning and practicing the Dominic system techniques. It's a mnemonic system, that is, a file system for your brain. Let me explain briefly how it works:

  • The main principle is that your brain isn't good at all at recording loose information, but it is really good at remembering who did what, where.
  • Hence we're gonna encode information in that format. Our bytes are gonna be who, doing some action, someplace.
  • For memorizing decks of cards that means we'll need to start with card suits and card "numbers" (in quotes because we'll include A, K, Q and J there).
  • We'll use suits and numbers to create a quick alphabet, which we can then turn into initials, which we can then associate with people and actions.
  • For example, ♣8 in my system is CK, and that to me is Cosmo Kramer.
  • Every person should also have a unique, corresponding action. I always picture Kramer smoking and drinking simultaneously.
  • Lastly, we need a route. That's just a sequence of places with a well defined order. It could be the path from your house to the nearest market, for example, or anything else. I use with a walthrough of the Las Vegas strip from Fallout: New Vegas.
<iframe src="https://giphy.com/embed/qaoutfIYJYxr2" width="480" height="356" frameBorder="0" class="giphy-embed" allowFullScreen></iframe>

via GIPHY

In sum, our smallest unit of action is someone, doing something, someplace. Then you just string those together along your route 'til you run out of cards. That's not only infinitely easier than memorizing loose information, it's also a lot of fun. (And for brevity, I usually use two pairs of person+action per location in a route.)

Let's see this in action, take the sequence ♥Q, ♥7, ♦2, and ♠J. To me that's Mozart (♥Q) moonwalking (♥7) towards an amused David Bowie (♦2), who's eating cereal (♠J). Let's say those are cards 45–48 in the deck, that means they'll be doing that at the entrance of The Tops Casino.

And just to clarify. Those are my characters, performing the actions I chose, along my route. When you do it you'll create your own alphabet, so it'll be all tailored to you.

Anyway, enough of this for now. I'll make a video of this whole process in action to better illustrate sometime. For now, watch this for a demonstration:

https://www.youtube.com/watch?v=qFBPQhB1GMQ

Features

Now that we know what we want to work with, let's get down to specifics.

When we're done with our app, I want it to be able to:

  • Do the basic "memorize a deck of cards thing." That is: create a deck, shuffle it, give me the cards one by one, time how long I took to memorize them, and then let me guess what the cards were and tell me how accurate my guesses were.
  • Help users create the scaffolding for the "main operation" above. That is, provide tools to make it easier to create the initials, the characters, the actions, the routes. These are a pain to create the first time, so let's make that easier.
  • Have user accounts, save people's learning progress, and then show them how they're getting better, which cards they get wrong the most, and all sorts of stats like that.
  • With that in hand we can have leaderboards and fancy social things.

That's it for the API. That's all I can possibly see us doing for now. Once that is done the back-end will be finished, and it'll be time for front-end work.

Structure

This is obviously gonna be a multi-part series. For the foundational part of the project, I want to stick to four following core functions. User stories:

  • As a user, I can create a new, shuffled deck.
  • As a user, I can draw the next card, and keep doing that 'til I've seen them all.
  • As a user, I can fetch info about a deck.
  • As a developer, I can list all decks created. We're gonna use this for testing during development, but it shouldn't be available for end-users later.

And then we can tack the fancy stuff on top of this later. So with these above in mind, we're gonna need:

Models:

Our decks will need to be uniquely identifiable, hold a bunch of shuffled cards, and we need to have a way of knowing which card we need to show next.

Cards only need a suit and number.

Let's start with that. package models:

https://gist.github.com/8916a6e87e670edb49fac7a24ef94dc7

You might have noticed some bson there, which is usually associated with MongoDB—more on that below.

Controller:

Our controller needs to deal with the main functionality from our user stories, that is: create a deck, show info from a deck, show next card, and list all.

But before we get our hands dirty, some planning:

  • Both the show info and the show card functions will need to fetch the deck object from the database. So let's have an extra function to do just that and avoid writing the same code twice. I'm calling it fetchDeck.
  • Creating a new deck is a bit of a pain. There's a whole bit of logic for shuffling, then a whole bit of logic from creating a new, unique ID, and then the usual stuff of pushing it to the database and dealing with http. I'll split these into three: newID for the ID, freshDeck for shuffling, and then just Create for the rest.
  • Also we need a controller object to keep the database connection in.

So let's just sketch them out, without any content for now. package ctrl:

https://gist.github.com/eae0e7e70068d1f9b4be8edb4aeb7435

Database:

For the database we'll be using Mongo, and the reason is... because.

I don't know. It's easy, I've using it before, and I have an mLab account just sitting there. 🤷

We can re-think it later, but for now, package db:

https://gist.github.com/d783bb6eb019e6a469bbc8e756314d92

Most of it should be self-explanatory. IsUnique will be called by newID in the controller.

Main:

And lastly, our main package should be pretty simple. It'll be a bunch a http.NewServeMux() call, then a bunch of HandleFuncs to route the requests to the right functions in our controller, and a http.ListenAndServe() call at the end.

We'll get to it when it's time.

Code

Now for the actual code. I wrote a quick prototype which we're gonna use to get started. It's far from perfect and we'll probably refactor the whole thing ten times over as the series goes along. Lemme show you.

package db

We'll have six functions here: Init, GetDeck, AddDeck, IsUnique, IncrementLastShown, and GetAllDecks. Starting from the simplest, with commentaries below:

https://gist.github.com/0e4d35d782644df3278b968496518f90

func Init starts a session with the Mongo driver and checks for errors. Pretty simple. The arg there should be in mongodb://user:password@yourdatabase.com:12345/dbname format.

https://gist.github.com/9d3129218128844f3fb77504bbac8ddd

You'll be seeing db.DB("memdec").C("decks") everywhere. It means we're choosing the database "memdec", and the collection "decks" within it. Other than that it's pretty simple again, func AddDeck inserts a deck object into that collection.

https://gist.github.com/d38db0b0c22ba37b52267d5360534119

func GetAllDecks asks the database to find an empty map (read: everything), and returns all results as a list of decks.

https://gist.github.com/c0398b39ee64428f6fb5c5dbe1aa555d

func GetDeck asks the database for a deck with a matching id, and returns it.

https://gist.github.com/7efc0a675990e0e5c67f30573a1125fb

func IsUnique checks whether the randomly generated ObjectId we'll get in our controller (below!) has already been used in the database. It's probably really super unlikely, but eh, I'd rather be safe.

The Limit method makes so I get one result, max. Ideally we'll get zero, so if we get one it'll return false already, no need for more. The Count method gives us the number of results, which given the constraint we just saw, will have only two possible values: zero or one.

https://gist.github.com/1d6a3b4afed2a5c4c9cc8fe9be0f52a2

func IncrementLastShown is interesting. The database call is similar to the other ones: we're finding a deck based on the id, then applying a modification. That modification is a bson map, and the way it works is we're taking the modification described in the plusOne map, and outputting the end result to deckCheck—which we're not really using for anything. (Is there an Apply that returns nothing?)

And for plusOne, our mgo.Change object, we're creating an update that increases the lastshownindex field by one. ReturnNew means deckCheck will be populated with the new value, not the pre-existing one.

package ctrl

Now for the controller is where the actual meat and potatoes are!

For starters we'll need the Controller object, an initializer for it (NewController), and seven methods: Create, newID, freshDeck, Info, NextCard, fetchDeck, and ListAllDecks. These will process the http requests we receive, make calls to package db, and return the results via http.

I'll try and go from the simplest parts to the more complex ones:

https://gist.github.com/e5d4a7e49eb8148801c405cbbbd1f922

Controller is an object that carries our database session information.

https://gist.github.com/0bf4b23b33f5a45d0c9f41bc6b6330ac

func NewController returns a new controller...

https://gist.github.com/da1c6c817e2fe9acabd332953a5acd59

func ListAllDecks makes a call to GetAllDecks from package db, and sends the results back as JSON in the http response.

https://gist.github.com/e11e10bdb60d8b112d34069421bbe8dc

func Info takes the last element from the URL (e.g. www.address.com/not_this/nope/this_one!) and fetches a deck that has that id. We'll see function fetchDeck below. Then it sends the results back with http.ResponseWriter.

https://gist.github.com/17dbb47177f13ece77dee4886a984e83

func fetchDeck checks that the argument supplied is a valid ObjectId, then fetches from the database whatever deck holds it.

https://gist.github.com/874648927795f7733fe5681b564a10bd

func NextCard is slightly more convoluted than what we've seen so far.

First it grabs the id and gets the corresponding deck from the database.

Then it checks whether all cards have already been shown. If they have we reply saying there are zero cards remaining, and that's that.

Otherwise it will 1. call IncrementLastShown to tell the database that we're about to show the next card, and 2. send back to the user the actual next card, and the number of cards remaining still.

It bears repeating: the way we're doing things people are only allowed to see each card once. And once you've seen them all, that's it. No repeats.

https://gist.github.com/2690274a4525ac28c25eb5efb7797750

func Create was supposed to be super convoluted, but we're breaking it down into parts. So here we're getting a new deck with freshDeck, and passing it on to our database with db.AddDeck. If everything works correctly, we show the user the id of their new deck.

https://gist.github.com/15e91600302f0617cbd6cff4befeade1

func freshDeck. In parts: First we make an empty deck object. Then we create a new id for it with func newID, which we'll see below. Next iterate through every suit and number to populate our deck with all 52 cards, and lastly we shuffle it.

That shuffling code is a bit convoluted—we'll probably swap it later for the new rand.Shuffle method that was introduced in Go 1.10.

https://gist.github.com/0d5a7d083826de1cfa055df56ffd6afc

func newID has nothing new: it created a new ObjectId, then uses db.IsUnique to check that it is... unique.

main

https://gist.github.com/cc7addcb32cd4c8904023b3c75bc9965

And finally, our main function brings everything together:

  • Initialize the database.
  • Get a new controller.
  • Create a new router.
  • Route http requests to the corresponding functions in the controller.
  • Sit down, grab some coffee, and wait for http connections.

But will it blend?

It should work now. Here's the repo with the lot of it all together.

Let's see:

https://gist.github.com/0e7e4338d379396650360152153c637d

That makes sense since we didn't set and endpoint for /.

Now let's try creating a deck:

https://gist.github.com/16c9fb36baa35eb3145b72b785d73691

Cool. That big ugly number there is our deck ID.

Now let's draw a card:

https://gist.github.com/5c80222b33010eb67ae1a93fa90fbbe0

Yip. All good.

And let's grab the whole deck:

https://gist.github.com/eaf983830e0d2bdd0999c285ab0b6f4e

Perfect.

I mean, according to plan. When we're finished users won't be able to see the full cards list until after they're done guessing what they memorized. But it's good for now.

You might have noticed though, that all our errors are panic(err). That's ugly, we'll fix it next time. Here's what happens:

https://gist.github.com/9eb1121c1bf91c1d22d6e3c5b9e8b3b6

When we send an incorrect request like that (it was expecting an ObjectId, not a "its-gonna-crash"), it panics. Thankfully it's just that one thread that panics, not the whole app, but still.

Here's what it looks like on the other terminal:

https://gist.github.com/28a30c4342dc230db5218b4ca36eaf9b

Yip. Panic.

Oh, and if you get an error like the one below when you're trying to run this at home, it's because you need to change the default mongodb://user:password@yourdatabase.com:12345/dbname for actual working credentials.

It's not difficult to run mongo on your machine if you'd like. I use mLab for convenience; they have a free tier.

https://gist.github.com/cb94479cc3118ba211a84beb337bcd20

Next steps

We got our first working prototype! Alright! Yeah!

While this was super fun to write, it's far from what it should be. So let's see what we can improve next:

  • We need to add timestamp fields to our decks so we can time how long it took the user to memorize all the cards.
  • We could add flags to make our program more convenient to use.
  • And we could also keep our mongo credentials in a separate file.
  • There's also the issue that right now our controller depends on that package db specifically. Meaning if we wanna switch it for one full of fake calls to use for testing, it'll be a hassle. So we might wanna mess around with dependency injection there.
  • And a real nerdy nit: When we're adding cards to newDeck.Cards there we're reallocating the underlying array a bunch of times and that's inefficient. It bugs me. It's an easy fix but I wanna take some time to explain the why of it.

So let's get to that next time, and afterwards we'll see how it goes.

Thanks for reading, and see you next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment