Tuples Implementation / Property Conversion

Building on last lesson, we are going to spend some time implementing Tuples in JavaScript to learn how to read mathematical properties off of the web and translate them into specs for unit tests.

We're working on inlining examples! Until then, please find the examples here on GitHub.

Tuples Implementation

In the lesson on Tuples I introduced what a tuple was, how it was used, and we gave some examples and demonstrations. While I was looking for interesting speaking points, I noticed that Wikipedia listed out the properties of a Tuple, namely those that set it apart from a Set, which we'll learn about in a later lesson. I thought it would be a good exercise to transform properties of a data structure from mathematical notation and rules, to tests for an implementation in JavaScript.

So I've created this project, which will be available with the lesson files, and I have the mocha test framework installed with the chai assertion library. If you have any experience in RSpec, or any spec-like test framework, this should all look pretty familiar to you. It's not too far off from xunit frameworks either.

Okay so we are going to describe our custom Tuples here in this tuple_spec.js file. Let's see what's up first in Wikipedia. It tells us that the identity of two n-tuples is, and then shows us some mathematical notation representing two tuples. When you're not used to reading this kind of notation, or many math-oriented articles in general, this kind of text tends to cause eyes to gloss over. This example here is rather simple once you squint at it.

The subscript numbers start at one by convention here, but if we pretend they start at 0, it looks a lot like accessing each element of an array up to whatever number n is. So we can call the tuple on the left here A, and the tuple on the right B for simplicity's sake. So it's trying to tell us that tuple A is equal to tuple B, if every element of A is equal to every corresponding element of B.

Okay, that sounds more like something in standard developer parlance, let's go ahead and codify that in our tests.

So we'll create another nested block and say that we are describing equality, and then test that it is equal to another when length and ordered elements are the same. Next we'll test these individually so that we have negative cases. So we'll test that it must have the same length, next that it must have the same elements, then finally it must have the same order of elements.

Alright so now let's work on these other properties that distinguish it from a set. Number one states that we can have duplicate elements in a tuple, so that if I want a tuple containing 1,2,2,3, that is perfectly fine. So we'll write a new test outside of the Equality block that it can have duplicate elements. Onto property 2, tuple elements are ordered. We address this in the first test, but having a negative test here will be helpful. Finally, that a tuple has a finite amount of elements. This is kind of a given with JS arrays, but to make it a bit more interesting, lets make it a property that our tuples are immutable as they are in many other languages like Python or Elixir. And just as one final verification for ourselves, lets test that it can be destructured like normal arrays can in JS to keep convenience.

Great, so let's get going on the implementations. We're going to call beforeEach in the top-most scope and say that we want our test subject to have a Tuple assigned with the values (1,2,'a').

import mocha;
import chai;

describe('Tuple', function() {
  beforeEach(function() {
    this.subject = new Tuple(1,2,'a');
  });

  describe('Equality', function() {
    it('must have the same length', function() {
        const tup = new Tuple(1,2,'a','b');

        expect(tup).toEqual(this.subject);
        expect(this.subject).toEqual(tup);
    });

    it('must have the same ordered elements');
  });

  it('can have duplicate elements');

  it('is immutable');
})

Let's skip our first test since this tests the happy case to start with, and go to testing that our tuples must have the same number of elements to be equal. So since our subject has three elements, we'll set an expectation that our subject will not equal a new Tuple with the same first three elements, plus a fourth b.

Let's run this....and it fails that tuple is not defined so lets open up index.js and in here we are going to declare a new class named Tuple. Running the spec again tells us that it can't read the length property on our tuple, so. They wound up being equal because we have no constructor to help set them up differently so they are the same. So in index.js we'll set up the constructor to take in all arguments as a variable named args using JS's rest operator.

First we'll set this.length to equal args.length so that we can have the length property available later. Next we'll iterate over all of them with forEach and we'll pass in a function that gets the current argument and its index as arguments using the fat arrow syntax so we keep the context of our this variable pointing at our class instance. Then we will assign position in the array on this to the argument. If this syntax is confusing, just know that in JS, arrays are objects and I'm leveraging that quirkiness here. You can think of it as just assigning elements to an array as normal.

Now when we run the test we get a greenlight. Next we'll move on to the next test. I'm going to copy our assertion from above, paste it, and since we are testing that our subject must have the same elements as another tuple, we're going to let our tuple keep 1 and 2, but we'll replace the third argument with the number 3. Running the tests again shows this as a greenlight already. Normally you'd want a failing test, but JavaScript's arrays already evaluate this way so we are just verifying that our tuples still react to equality checks the same way. I could have made this class more independent and only operable via method calls, but since I try to keep these screencasts short and don't want to add layers of confusion I opted to go this direction instead.

Finally for the equality block, we'll test that the arguments must be in the same order, so I'll switch the order of our tuple's arguments to 1,'a',2, and since we are expecting these to not be equal, when we run the tests we'll see that we are again leveraging JS's native behavior here so it's already passing.

Outside of the equality block we are first testing that our tuple can contain duplicate elements. Alright so let's reassign our subject to a tuple containing the values 1,2,2,2. Then we'll expect our subject's length to equal 4, because if it didn't take duplicates it would be of length 2. Running the tests shows this once again passes, since arrays behave the same way, so we are on to testing immutability.

So since we have 3 elements in our subject, we're going to set its 4th element, which is index 3, to be equal to 5. Then we'll expect that our subject's length should still be 3, and that index 3 should be undefined. Let's run this...and now we can see that our first assertion passed, but our second failed. The reason this failed is because right here on line 3 in the tuple class we set the length property to be the length of the arguments array. This property never gets updated like it would in a real array, so it will lie to us if we ever add new elements. This is starting to present a codesmell for our Tuple class, but let's keep going for now.

To prevent this, once our object is constructed we are going to freeze it, which prevents future modification. And now we could just delete the assertion for length because we know it is fixed, but we'll leave it till our test suite runs entirely. Running the test again we get a greenlight. Now if we had written these specs with strict mode enabled, this line where I try to set the 4th element would throw a TypeError because I'm trying to modify a frozen object. This doesn't matter much for our example, but it's something I want to point out since this may pop up if you run everything with strict mode.

Finally, lets make sure that our Tuple can be destructured or unpacked easily. We'll declare a new function fn, then use the fat arrow syntax again, destructuring our tuple into three separate variables, first,second, and third. Then in the body we'll simply test that each variable is equal to its position in subject. Then we'll call it below, passing in our subject as the argument. When we run it....we get undefined is not a function...which is rather suspicious because it is clearly a function. Let's try removing the destructuring syntax to see if that changes anything. Looks like the whole tuple is now passed in as the first argument, so that seems to indicate our array-like object is not able to be used as an array for destructuring. We could, of course, write our own method for using our tuple arguments with something like a method on the tuple object, we'll call it destructure, and it takes a function as an argument, and the values of our tuple are passed in as arguments. Then inside that function we are free to use our destructured variables.

However, this isn't always what we need. This is the point where we need to make a tradeoff. Would we rather have a named class for our tuple, or would we prefer to simply have a factory function return an array with the attributes we want. I'm going to favor the latter, because I don't really like the API the other way. Plus, I won't be able to show you the alternative unless I favor it, so let's tweak our implementation. Let's go ahead and delete everything, because we can replace this with a one-liner. We'll export a function that takes the same arguments our class took, and simply returns a frozen array cloned from the argument list.

module.exports = ...args => Object.freeze(args.slice(0));

And now that our exported function is no longer a constructor function or class, we can replace our reference at the top with a lowercase name, for convention, and then we'll simply find and replace all references to new Tuple and replace it with tuple, which will make it look a lot like our Python examples from our last lesson on Tuples. Now when rerun the tests everything passes!

So while I'm not entirely sure I'd recommend doing something like this for production use in JavaScript, it was a fun exercise in learning how to read math properties and translating them into specs and code. Hopefully you learned something from this video, and I do encourage you to play around with writing your own data structures like this for learning and fun.