What I Learned Failing my ParaMorse Project

Turing School of Software Design: Module 1, Week 3

"Fall down seven times, get up eight," is a Japanese saying related to students at the Turing School of Software Design by Heroku developer, Jonan Scheffler, in a lunch and learn session on July 14th at Turing. The idiom seems particularly apropos because I failed the functionality section of this week's project. Turing founder, Jeff Casimir, tasked us with building a program to interpret Morse code, the mid nineteenth century set of signals used to communicate over distances via telegraph. The project is called ParaMorse because the crux of the challenge is to handle parallel streams of code that intertwine to form a single message.

I failed ParaMorse because I only got my code to be able to take in one message in English and output any number of streams of Morse code. This is a necessary step in getting to decoding multiple streams of Morse code, but a far simpler algorithm than the final product. Unlike Morse code, English letters are all single characters. Morse code letters encoded in binary can be one to nineteen characters long.

DICTIONARY =  { "a" => "10111",
  "b" => "111010101",
  "c" => "11101011101",
  "d" => "1110101",
  "e" => "1",
  "f" => "101011101",
  "g" => "111011101",
  "h" => "1010101",
  "i" => "101",
  "j" => "1011101110111",
  "k" => "111010111",
  "l" => "101110101",
  "m" => "1110111",
  "n" => "11101",
  "o" => "11101110111",
  "p" => "10111011101",
  "q" => "1110111010111",
  "r" => "1011101",
  "s" => "10101",
  "t" => "111",
  "u" => "1010111",
  "v" => "101010111",
  "w" => "101110111",
  "x" => "11101010111",
  "y" => "1110101110111",
  "z" => "11101110101",
  " " => "0000000",
  "1" => "10111011101110111",
  "2" => "101011101110111",
  "3" => "1010101110111",
  "4" => "10101010111",
  "5" => "101010101",
  "6" => "11101010101",
  "7" => "1110111010101",
  "8" => "111011101110101",
  "9" => "11101110111011101",
  "0" => "1110111011101110111"
}

Remember, Morse code comes in one digit at a time. To make any progress one has to find answers to a few questions pretty quickly:

  1. How do I detect that I have received a character? How do I know that I have "a" versus a "j"? Keep in mind the first five digits of "j" are the same as the five digits used to encode "a". 
  2. How do I handle spaces? Identifying a string of 7 consecutive 0's, the code for space, is not that hard. But, what if a single stream of code has multiple spaces in a row?
  3. How do I keep the data from each stream organized? If there are two streams of code coming in, how can I know to always take a decoded letter from stream A and then a decoded letter from stream B? What if I have 4 streams of Morse code? What about 8?

The goal of the project was not necessarily to hack together the optimal Morse code cracking algorithm, though that is certainly one area where I fell short. Rather, the goals for the project were to:

  • Build small objects with a single purpose
  • Connect multiple objects to achieve a single overall purpose
  • Build software using an iterative approach
  • Implement a queue data structure

Reflecting on the project there are definitely pieces related to each of the above goals that I will do differently moving forward. In particular, I was too complacent at the early stages of the project. I held close to the early iterations of the project specifications without trying to anticipate the requirements and challenges of subsequent iterations. My mindset was basically, 'I've finished code to translate one Morse code character to English, I'll let future me figure out what to do when we've got to encode multiple characters - to say nothing of multiple streams.' In that regard, I need to be significantly kinder to my future self.

The remainder of this post will be more technical. I will discuss my specific takeaways from each of the aforementioned learning goals. I will also write a description of how I would approach ParaMorse given a fresh start.

Build small objects with a single purpose

Building small, single purpose objects out of Ruby code is definitely my biggest technical takeaway from this project. Part of building software professionally is creating code that other team members can easily dive into, understand and maintain. One of the best ways to accomplish this is to write short methods and singularly purposed classes. For example, in this project we create a Class object called a LetterEncoder whose role is to take one English letter and convert it to Morse code. We have a separate class called a LetterDecoder whose job is to take a character of Morse code and decode it into English. Taking this example further, we have a Queue class, whose job it is to hold the data coming into our program.

require './lib/dictionary'
  class LetterDecoder
    def decode(letter)
      DICTIONARY.key(letter.chomp('0'))
    end
  end

Connect Multiple Objects To Achieve a Single Overall Purpose

Andrew Stillman describes the tools he created that initially got me interested in coding as Legos that could be snapped together and taken apart again to fit a variety of use cases. I'm beginning to realize that at the micro level, code should also behave like a Lego set. A well designed Stream class should be agnostic about whether the data it streams is Morse code or English. An instance of a Stream should work just as well alone as it works in parallel with 2, 3, or 10 other sibling stream instances. 

Build Software Using An Iterative Approach

The role out of ParaMorse was intended to mimic the Agile Design methodology. Instead of building out an understanding of all the system requirements and then producing the system, Agile Design is about building the system iteratively. Each iteration expands upon its predecessor based on feedback from the user. To emulate this dynamic, the specification sheet grew over the course of the project. Iterations one through three were to be completed by Monday before the details of iteration four were fully understood. Iterations four through six took the group through Tuesday. Iteration seven, in which parallel streams were introduced, was left for Wednesday and Thursday. To complicate matters, everyone swapped code after iteration 3. So not only did we not know exactly how the end product was supposed to behave, but we also got to simulate what it is like to take over an existing codebase. 

ImPlement a Queue Data Structure

A Queue data structure is basically a line of data. Queues come in two varieties. A first in, first out (fifo) queue means that first piece of data that enters the queue is the first piece of data accessed by the data structure. This is basically like every kind of line we encounter in the real world. When I line up to pay for groceries, I check out before the person who lines up after me. A last in, first out queue is more like those D-day Allied personnel carriers. The last soldier into the carrier is the first soldier out when the door is lowered on the beach. 

With regards to the ParaMorse project, queues are how to keep the stream of data entering and leaving the system organized. A digit of Morse code comes in from stream A, then gets added to the queue for interpretation. The next digit of code comes from stream B and is added to the end of the queue. This process repeats as data continues to stream in. When the queue contains a discernable character, it pops the digits out and the remaining digits move to the front of the queue. 

Architectural Reimaginings

A ParaMorse program that has multiple streams of binary data as its input and outputs a single text file containing the merged, decoded message should contain the following:

class ParallelDecoder

  def initialize(n)
    # => instantiates n StreamDecoders
  end

  def merge
    # => merges outputs of StreamDecoders
  end

end

class StreamDecoder
  def initialize
    # => creates a instance of Decoder
    # => creates a queue
  end

  def recieve
    # => adds digits to the queue
  end

  def detect_character
    # => returns true if queue contains a character, including a space
  end

  def decode_character
    # => returns decoded character
  end

end

class Queue

  def initialize
    # => instantiates a queue as an empty array
  end

  def push(digit)
    # => pushes data onto end of queue
    # => returns queue
  end

  def pop(n)
    # => removes n character(s) from the end of the queue
    # => returns queue
  end

  def peek(n)
    # => looks at the front n characters of the queue
    # => returns n characters
  end

  def tail(n)
    # => looks the end n characters of the queue
    # => returns n characters
  end

  def count
    # => returns number of elements in the queue
  end

  def flush
    # => clears the queue
  end

end

class FileIo

  def read(file)
    # => returns text of file as a string
  end

  def write(file, text)
    # => writes text to a file
    # => returns number of characters written
  end

end

class Decoder

  def decode
  end

  def encode
  end

end