Last week at the Flatiron School, Avi Flombaum talked about the merits of abstraction in code. As Avi mentioned, code can become much more idiomatic when it utilizes abstraction instead of relying on specific details.

The Ruby core comes equipped with a whole toolbox of nicely abstracted methods that can be used to write some great and expressive code. This one of the reasons that programmers love to write code in Ruby.

While doing one of the homework labs over the weekend, I realized how much more expressive code can become when many of the lower-level details are abstracted out. As I’ve become more comfortable programming in Ruby, I’ve been to make my code more abstract and expressive.

A Non-Abstract sort_songs

In the lab, I was tasked to create a sort_songs method that took an array of songs expressed as strings in the format "Artist - Album - Song" and returned the same strings in a nested array that grouped songs based on artist and album. In order to pass this lab, the sort_songs method had to correctly respond to a set of RSpec tests, which sent a preset array of songs to sort_songs and expected the output to be a properly formatted nested array.

On my first crack at this problem I followed the instructions to the letter. I therefore implemented my first sort_songs method to respond exactly to the preset list of songs included in the test.

def sort_songs(songs)
  sorted_array =  [
                    [
                      []
                    ],
                    [
                      [],
                      []
                    ],
                    [
                      []
                    ]
                  ]

  songs.each do |song|
    artist, album, title = song.split(' - ')
    case artist
    when 'Neutral Milk Hotel'
      sorted_array[0][0] << song
    when 'The Magnetic Fields'
      if album == 'Get Lost'
        sorted_array[1][0] << song
      else
        sorted_array[1][1] << song
      end
    when 'Fun'
      sorted_array[2][0] << song
    end
  end

  sorted_array
end

As the instructions to the lab suggested, I initialized an empty array with a nested structure tailored immaculately to the list used in the test. I used the each method to iterate over the songs array, first splitting it into artist, album, and title variables and discarding the ' - ' through parallel variable assignment. Next I used a case statement that funneled the songs into different paths based on the artist and (in just one case!) the album. Then I used << to shovel each song into its comfortable, premade bed in my well-laid-out sorted_array. (Seriously, they look kind of like little beds!)

I didn’t feel too great about writing this case statement, and doubly so for the if/else statement nested in one (and only one!) of the cases. The cases are so specific! When writing this I thought to myself, “What if I added just one more pesky song to the list? This whole program would blow up!”

Finally I returned the sorted array. This also didn’t sit so well with me, as the method implements an “each sandwich,” where the each iterator is the meat and the bread is the empty sorted_array on top and the return of sorted_array at the bottom. Ruby has the map method (and its alias collect) that accomplish this same feat with just one method call.

This code passed the tests, but I wasn’t satisfied. And for good reason: this method is totally useless outside the confines of this one test!

I wanted to make something that could take any song in the "Artist - Album - Song" format and return it in a nested array. But an array is simply not the natural way to conceptualize a music collection. A nested hash, on the other hand, is just right.

Making a Hash

It helped me to first conceptualize this array:

songs = [
  "Neutral Milk Hotel - In An Aeroplane Over the Sea - The King of Carrot Flowers",
  "The Magnetic Fields - Get Lost - You, Me, and the Moon",
  "Fun - Some Nights - Some Nights",
  "The Magnetic Fields - Get Lost - Smoke and Mirrors",
  "The Magnetic Fields - 69 Love Songs - The Book of Love",
  "Fun - Some Nights - We Are Young",
  "The Magnetic Fields - 69 Love Songs - Parades Go By",
  "Fun - Some Nights - Carry On",
  "Neutral Milk Hotel - In An Aeroplane Over the Sea - Holland 1945"
]

as this hash:

songs = {
  "Neutral Milk Hotel"  => { 
    "In An Aeroplane Over the Sea" => [
      "The King of Carrot Flowers", 
      "Holland 1945"
    ]
  },
  "The Magnetic Fields" => { 
    "Get Lost" => [
      "You, Me, and the Moon",
      "Smoke and Mirrors"
    ], 
    "69 Love Songs" => [
      "The Book of Love",
      "Parades Go By"
    ]
  },
  "Fun" => { 
    "Some Nights" => [
      "Some Nights",
      "We Are Young",
      "Carry On"
    ]
  }
}

Using a hash as the data structure in this method seemed to be a more logical choice because it’s organized similarly to my own digital music library. At the top level, there is one large music directory (analogous to the songs hash) that contains all of the music on my computer. Inside the large music directory, there are a number of smaller directories (the top-level keys in the hash) named after different bands. Each band’s directory contains that band’s discography (the top-level values in the songs hash). I may have one or more albums (the keys in the nested hashes) by a given band, and each album has a tracklist (the values in the nested hashes), which can be expressed as a list of songs (or an array of strings).

The hash is also great because it can be very flexibly extended and it lends itself to creating a more abstract version of this program.

An Abstract sort_songs Method

Here is my improved sort_songs method that takes an array of songs and turns it into a hash like the one above:

def sort_songs(songs)
  sorted_music_library = {}

  songs.each do |song|
    artist, album, title = song.split(' - ')
    if sorted_music_library[artist].nil? 
      sorted_music_library[artist] = {album => [title]}
    elsif sorted_music_library[artist][album].nil?
      sorted_music_library[artist].merge!(album => [title])
    else
      sorted_music_library[artist][album] << title
    end
  end

  hash_to_array(sorted_music_library)
end

This method creates a sorted_music_library hash, then iterates over the songs array using the each method. If the artist is not a key in the hash, the hash is updated to include artist as a key with a value of { album => [title] }. If the artist is a key in the hash but the album is not a key in the artist’s nested hash, { album => [title] } is added to the artist’s nested hash using the destructive merge! method. Finally, if the artist and album are already in the sorted_music_library hash, the new song title is shoveled into the album’s array of songs.

Finally, to make the tests pass, I called a separate hash_to_array method at the end of the sort_songs method that utilizes the collect method to drill down into the hash and return a nice, nested array:

def hash_to_array(music_library)
  music_library.collect do |artist, discography|
    discography.collect do |album, tracklist|
      tracklist.collect do |song|
        "#{artist} - #{album} - #{tracklist[tracklist.index(song)]}"
      end
    end
  end
end

I think this code speaks for itself. That’s the power of abstraction: it enables programmers to write more expressive code.

Results

When the songs array is sent to the abstracted sort_songs method, it returns the following array that makes the test pass:

sort_songs(songs)
=> [
      [
        [
          "Neutral Milk Hotel - In An Aeroplane Over the Sea - The King of Carrot Flowers",
          "Neutral Milk Hotel - In An Aeroplane Over the Sea - Holland 1945"
        ]
      ],
      [
        [
          "The Magnetic Fields - Get Lost - You, Me, and the Moon",
          "The Magnetic Fields - Get Lost - Smoke and Mirrors"
        ],
        [
          "The Magnetic Fields - 69 Love Songs - The Book of Love",
          "The Magnetic Fields - 69 Love Songs - Parades Go By"
        ]
      ],
      [
        [
          "Fun - Some Nights - Some Nights",
          "Fun - Some Nights - We Are Young",
          "Fun - Some Nights - Carry On"
        ]
      ]
    ]