Ruby practice with Exercism

What I learned

November 3, 2020 Ā· Felipe Vogel Ā·

Iā€™ve just reached the end of 36 days of Ruby coding exercises with Exercism. Hereā€™s some of what I learned.

1. Learn from the work of others, then refactor

The great thing about Exercism is that once youā€™ve solved an exercise, you can see other peopleā€™s solutions (ranked by popularity), then rework and resubmit your own.

For example, an exercise I solved in 26 lines of code was solved by someone else in just 9 lines, thanks to their powerful and elegant use of regular expressions. (Iā€™m not a fan compressing a whole program into a monstrous freaky regex, but this isnā€™t like that.)

module RunLengthEncoding
  def self.encode(str)
    str.gsub(/(.)\1+/) { |s| "#{s.length}#{s[0]}" }
  end

  def self.decode(str)
    str.gsub(/\d+./) { |s| s[-1] * s.to_i }
  end
end

Whenever I found a better solution than my own, I copied it and resubmitted it as my new solution, for easier future reference. This was the outcome more often than not, casting away my little creation into the cold and dark as soon as I discovered a less blemished specimen to take its place.

However, I console myself by noting a small victory for my original solutions. I kept a list of exercises where someone elseā€™s solution was not just better but considerably better than mine, and I kept another list of exercises where my solution was (in my opinion) considerably better than anyone elseā€™s. This ā€œmineā€™s betterā€ list has 15 exercises, versus 14 in the other list.

success kid meme

2. Think before you code

So it was great fun to see other peopleā€™s solutions and compare them to mine, with feelings ranging from ā€œmind blownā€ to facepalming at an easy approach that Iā€™d overlooked. Over time it dawned on me that with more careful thought, I could prevent the latter.

Case in point: for generating Pascalā€™s triangle, I painstakingly built an intricate tree-like recursive triangle data structure in 84 lines, only to realize (too late) that it could be done in 8 lines with simple line-by-line iteration. šŸ¤¦ā€ā™‚ļø Lesson: donā€™t bury yourself in a complex solution until youā€™ve spent some time thinking and exploring other possibilities.

3. More concise isnā€™t always better

The cleverest and shortest Exercism solutions are generally the most popular, but I found that they are not always the best. For example, this was the top-ranked solution in one exercise:

module Grep
  def self.grep(pattern, flags, files)
    [].tap do |results|
      files.each do |file|
        File.read(file).lines.each_with_index do |line, index|
          matcher = Regexp.new(
            flags.include?("-x") ? "^#{pattern}$" : pattern,
            (Regexp::IGNORECASE if flags.include?("-i")))
          next unless line.match?(matcher) ^ flags.include?("-v")
          break results << file if flags.include?("-l")
          results << [
            ("#{file}:" unless files.one?),
            ("#{index.succ}:" if flags.include?("-n")),
            line.rstrip].join
        end
      end
    end.join("\n")
  end
end

The module and method name give a general idea of what itā€™s for, but to know anything beyond that requires wading into a quagmire of code. This solution is three times shorter than the runners-up; it is undoubtedly concise. But it is not elegant, nor is it maintanable or extensible. Here is the solution that I picked as the best.

4. More optimized isnā€™t always better

The same holds true for optimized code. Below is another top-ranked solution. It calculates coin change, finding the fewest coins that total a given amount of money (target), using a given list of coin values (coins). (The runner-up solutions are not so concise, but this time theyā€™re equally difficult to understand since their algorithms are only more sprawling.)

def self.generate(coins, target)
  best = Array.new(target + 1)
  best[0] = []

  # Doing larger coins first results in fewer array writes,
  # compared to doing smaller coins first.
  # Both ways give the same answer, though.
  coins.sort.reverse.each { |coin|
    (coin..target).each { |subtarget|
      next unless (best_without = best[subtarget - coin])
      # Lol &.<=
      # But it's necessary to avoid constructing [coin] + best_without when unnecessary.
      next if best[subtarget]&.size &.<= best_without.size + 1
      best[subtarget] = [coin] + best_without
    }
  }

  best[target]&.sort or raise ImpossibleCombinationError.new(target)
end

Even with the comments, thatā€™s not easy to follow. We can take a simpler approach using Rubyā€™s built-in array operations. Hereā€™s my solution:

def self.generate(coins, target)
  largest_to_smallest = coins.reverse
  (1..Float::INFINITY).each do |count|
    raise ImpossibleCombinationError if count > target / coins.min
    largest_to_smallest.repeated_combination(count) do |combo|
      return combo.sort if combo.sum == target
    end
  end
end

My algorithm is not quite as efficient, but there is no perceptible speed difference with the necessarily small input. (There will never be more than a few possible coin values, and the target amount will never be many times more than the largest coin.) So in this case, simplicity wins over efficiency.

5. Rubyā€™s collections and blocks!!

The last snippet above is also a good example of the elegant solutions that arise out of Rubyā€™s rich collections library and its powerful block-based iterators. Before this month of hands-on practice, I didnā€™t understand what was so special about Ruby, and I was frankly annoyed at how Ruby provides so many different ways of doing the same thing. But now, after being impressed over and over by super-elegant Ruby code, Iā€™ve fallen in love. ā¤ļø

Even more is coming in Exercism v3

Definitely give Exercism a try if youā€™re learning Ruby or any of these 51 other languages. And stay tuned, because the good folks who run Exercism are hard at work on a new version that will have even more material and a better mentoring system. (Thatā€™s another amazing thing about Exercism that I havenā€™t mentioned: you can get personalized feedback from mentors, for free! Faith in humanity, restored.)

šŸ‘‰ Newer: Functional programming techniques in Ruby šŸ‘ˆ Older: AutoHotkey - Windows key, home row arrow keys, mouse shortcuts šŸš€ Back to top