Ruby practice with Exercism
What I learned
November 3, 2020 Ā· Felipe Vogel Ā·- 1. Learn from the work of others, then refactor
- 2. Think before you code
- 3. More concise isnāt always better
- 4. More optimized isnāt always better
- 5. Rubyās collections and blocks!!
- Even more is coming in Exercism v3
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.
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.)