How to build a Web App

Part 2 - Building a library component

In Part 1 of this series I described how to set up the environment for programming web apps in WordPress, and in the final part I'll show how to create the web app itself. But first I'll show how the main functionality of a web app is often best kept separate from the way it's used.

Take as an example the demo web app Here On The Map, which contains a live Google Map that when you interact with it does things outside the map itself. Google Maps comes as a JavaScript library file. It's a black box; there's no way you can add your own functionality to it so you use it by interfacing to its Application Programming Interface (API).

This is all about the "Separation of Concerns" design pattern. The map component doesn't need to know anything about where it's running and the user interface doesn't care how the map works. In reality they are 2 completely separate programs that happen to be running as a team.

The component

The example I've chosen will adopt this design pattern too. It's an anagram finder, which takes a line of text - typically someone's name - and searches a dictionary for all the combinations of words it can find that will use up all the letters. This is a computing-intensive task that can take a long time to exhaust all the possibilities.

I'm aware there's an anagram module available for Node.js but I was curious to see how hard it would be to write one myself. You are free to use this one for your own purposes.

Here's the complete source code. I doubt if it's the most efficient algorithm but it's quite easy to follow.


const Anagrams = {

  alphabet: [
    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
    'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', `w`, 'x', 'y', 'z'
  ],

  // Get anagrams from the given text
  getAnagrams: function (text, list) {
    const words = [];
    let remaining = text;
    let found;
    while (remaining) {
      found = false;
      const reduced = Anagrams.reduce(list, remaining.length);
      Anagrams.shuffle(reduced);
      const mtext = Anagrams.measure(remaining);
      for (let n = 0; n < reduced.length; n++) {
        const letters = Anagrams.measure(reduced[n]);
        if (Anagrams.contains(mtext, letters)) {
          words.push(reduced[n]);
          remaining = Anagrams.remove(mtext, reduced[n]);
          found = true;
          break;
        }
      }
      if (!found) break;
    }
    return {
      status: found ? `found` : ``,
      words
    };
  },

  // Remove a found word from the remaining text
  remove: function(remaining, found) {
    const mfound = Anagrams.measure(found);
    let result = '';
    for (let n = 0; n < 26; n++) {
      const letter = Anagrams.alphabet[n];
      remaining[letter] -= mfound[letter];
      for (let m = 0; m < remaining[letter]; m++) {
        result += Anagrams.alphabet[n];
      }
    }
    return result;
  },

  // Copies a list, removing words longer than a given length
  reduce: function(list, len) {
    const result = [];
    for (let n = 0; n < list.length; n++) {
      if (list[n].length <= len) {
        result.push(list[n]);
      }
    }
    return result;
  },

  // Shuffle a list
  shuffle: function(list) {
    for (let i = list.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [list[i], list[j]] = [list[j], list[i]];
    }
  },

  // Check if one string contains another, by comparing maps
  contains: function(a, b) {
    for (let n = 0; n < 26; n++) {
      if (a[Anagrams.alphabet[n]] < b[Anagrams.alphabet[n]]) {
        return false;
      }
    }
    return true;
  },

  // Measure a word, returning a map of letter counts
  measure: function (t) {
    const map = {};
    for (let n = 0; n < 26; n++) {
      map[Anagrams.alphabet[n]] = 0;
    }
    const text = t.toLowerCase();
    for (let n = 0; n < text.length; n++) {
      const c = text.charAt(n);
        if (c.toLowerCase() != c.toUpperCase()) {
          map[c]++;
        }
      }
      return map;
    }
  };

The component is constructed as a single object, ideally with a name that won't clash with anything else you might add to your web page. Inside are all the functions it uses.

It starts by defining an array comprising the letters of the alphabet. Then comes the main program, getAnagrams(). This is given the text to process and the dictionary to use, which permits the library to be used for languages other than English if a suitable word list is available.

The function looks down the dictionary for any word it can make using the letters available. When it finds one it adds it to the output list, removes the letters it used from the original text and tries again. Eventually it will either have taken all the letters available or failed to find a word. Either way, it sends back a small object with the words it found and a success/fail flag.

The dictionary is randomised before starting a run, otherwise the same words would come back each time. Words are compared by converting them into sets of 26 elements that hold the number of times each letter of the alphabet occurs in the word.

The remove() function removes a word from the text.

The reduce() function copies the original list, removing all words longer than the input text.

The shuffle() function randomises the list of words.

The contains() function tests if one string of letters contains all the letters of another string. This is not the same as the JavaScript function indexOf, which requires the letters to be in the right order. Here we only care if they are present.

The measure() function converts a word into a map of letter counts.

Who does what

The anagram finder will take a variable length of time to search for a set of words, and the result will be success or failure. The next action will be to try again, hoping that shuffling the dictionary will produce a result. This may have to be done thousands of times before all the possible combinations have been winkled out. The question is how to organise these retries. It is of course possible to add a retry mechanism to the finder itself, but this has problems:

  1. JavaScript has only a single thread, so while the finder is running everything else will be blocked. A single anagram search, using the function above, takes only a fraction of a second, even on a smartphone, so the block won't be noticed, but running it a thousand times is another matter. There's no way to stop the program other than by killing the browser process, which is pretty drastic.
  2. Modern computers are sprinters, not marathon runners. They can go incredibly quickly but only for a short time, after which they rapidly start to overheat. If you ever code a loop and forget to increment the loop counter, after a few seconds the computer's fan starts up, and if you have a temperature monitor you'll see it shooting up to dangerous levels, and again, the only way to stop it is by killing its process. Except you can't because the entire computer is locked up.
    So if you want to call the anagram finder a thousand times you'll need to put in a pause between calls, to let the computer catch its breath and cool off a bit. Pauses in JavaScript are a bit messy as they have to be done with callbacks, and they still don't solve the problem of how to stop the finder from running.

So to keep things clean it's best to let the web app itself handle the retries, the pauses and the user interactions. The anagram finder is lean and clean and doesn't need to worry about all that other stuff.

The library API

From the web app's point of view there's only one useful function exposed by this module; getAnagrams(). The other functions are available but are unlikely to be needed here. The fewer the interfaces, the better in general, as it avoids systems coming to look like a bowl of spaghetti. As you'll see in Part 3, this makes it very simple to incorporate the anagram finder library into the web app.

Some web components - such as Google Maps - are visual in nature, while others, such as the anagram finder, just do data processing of some kind. The important consideration is whether they have a clear set of APIs that allows them to be fully separated from the environment they will be used in. That way they can be easily plugged in and unplugged or replaced by other components such as test harnesses.  When you build a web page, look for parts of it that are really self-contained and build them as separate components. This reduces the size of the code you must manage and in many cases improves reliability because the components are built and tested separately from the applications that will eventually receive them.

Coming up...

In Part 3 of this series I'll show you how to build the user-facing side of the anagram finder web app, which you can see running at http://anagrams.easycoder.software.