Comparing Erlang to Ruby: Gathering Rows in Tic Tac Toe

My first implementation of TTT in Ruby was pretty naïve, particularly my inflexible approach to collecting the contents of each square within a three-in-a-row. For example, lines 60-67 are simply a hard-coded aggregation of the eight possible combinations of three-in-a-row. If I wanted to make a 4x4 TTT I would have to create a separate list of combinations just for that. And if a client were to demand to be able to play TTT on any size board with a width from 3 to 300 (sometimes you just have to be zen and do things for the sake of doing them) then I would have to hard code 297 separate instances of winning combinations. Rather than burn out from repetitively simple rote tasks, I figured out a better way.

In this Ruby example, lines 83-85 are analogous to the hard-coded lines 60-62 in the previous example, except that this latter example will find all of the indices for a board of any width. To do the same thing in Erlang requires a bit of a different approach.

At the moment, this bit of Erlang code from lines 60-64 will take a board of 9 spaces and return a list of each of the rows. Thus, if we pass in an array like this [1,2,3,X,O,X,7,8,9] we will get: [[1,2,3], [X,O,X], [7,8,9]]. There’s a lot going on here, and if you don’t look closely enough, you might suspect that these five lines of code contain only one function, but if you look a bit more closely, you’ll see that there are actually TWO functions here.

What are those two functions, you ask? Erlang considers gather_rows/1 and gather_rows/2 to be two entirely different entities. The number after the slash is known as arity, or the number of arguments a particular function takes. The first function, gather_rows/1, takes a board object, currently represented by an array of 9 elements. What it returns, however, is not as simple as the [[1,2,3], [X,O,X], [7,8,9]] mentioned above. Instead, there is first a function call to the built-in function reverse/1 in the lists module that takes a list and reverses all of the elements in it. So we know that what comes into the function has to start out as [[7,8,9],[X,O,X],[1,2,3]]. This is the result of the function call gather_rows/2.

This function takes two arguments: the board, and an empty list. In Erlang parlance, the empty list is known as an accumulator -- it accumulates the results of a recursive algorithm. Because variables in Erlang are immutable, we have to figure out how to manipulate them without changing them. Thus, every time we “modify” the accumulator, we are not actually modifying it, rather we are creating an entirely different variable with the added information we put into it.

Notice on line 62 the two arguments that are being fed into gather_rows/2. An empty list, and a variable named Gathered (variables in Erlang must start with a capital letter). And what does it return? The variable Gathered, of course! But how the hell is that supposed to happen? The arguments we passed in originally were just the board, [1,2,3,X,O,X,7,8,9], and an empty list, [].

Enter, pattern matching. If an argument in a function head is a variable, it will accept whatever value you assign it when called. If an argument in a function head is an empty list, however, the argument entered when the function is called must also be an empty list. Therefore, when we pass in our board, which is NOT an empty list, the pattern does not match (i.e. [1,2,3,X,O,X,7,8,9] =/= []) and Erlang tries to match the next pattern, which WILL match because we have two variables (Rest and Gathered) in the function head that will be assigned the values we pass in. What does this function return? Another call to gather_rows/2! What is this devilish ouroboros??

Remember, as soon as the first argument is an empty list, the loop will terminate and release the information contained in the Gathered variable. The first argument on line 64 is a function call to another of Erlang’s BIF’s: nthtail/2. This is very similar to Ruby’s slice!(start, length) function. Erlang’s lists:nthtail(3, [1,2,3]) will evaluate to an empty list (hey, that’s what we’re looking for!), however lists:nthtail(3, [1,2,3,X,O,X,7,8,9]) will result in [X,O,X,7,8,9] -- that will be passed in as the first argument in the first recursive call of gather_rows/2.

The second argument will be the result of [lists:sublist(Rest, 3)|Gathered]. The lists:sublist/2 function is analogous to Ruby’s slice(start, length) function, which will return the elements in a list for the given parameters. In between the sublist function and the variable Gathered is a special character “|” (pipe), used for distinguishing the Head of a list from the Tail (snake reference number 2). In its current form, this list now has a new Head that has been prepended to the former Gathered list, and will be passed in recursively to assume the name Gathered again. Although this may look like we have overwritten the variable Gathered, upon further inspection you can see that we have called the function gather_rows/2 again, creating a new instance variable, Gathered, that exists in a parallel universe to the first. By prepending the combinations onto the front of the list we will end up with the backwards list that we wanted in the first place.

And now, we have come full circle (heh). After recursing through a couple times we end up spitting out the (backwards) gathered list of combinations we wanted in the first place because of the helpful pattern matching we set up. Then, by using the BIF reverse/1 we get [[1,2,3], [X,O,X], [7,8,9]]. And if you want to extrapolate this function for any size board, simply substitute the width of the board for the number 3.