In some projects that I’ve worked on, I sometimes discover code like this that people think is clever:
I just rewrite like this:
process(data) if data.predicate?
Boom! That’s a lot simpler and more readable! I don’t understand why some people like to write weird code like that thinking it is useful. It’s not useful at all. Also, the new numbered block variables are only meant to be used for Ruby scripts that are replicating bash scripts with $1
variables. They are not readable at all in standard business applications since you’d have to go back while reading to remember what the _1
is. It would be way better if it is a variable name like value
, customer
, order
, etc…
Just write idiomatic Ruby code! That’s what is demanded of collaborative Ruby software engineers who want to enable code to be readable and maintainable by others in a team, not just by themselves as individuals.
Here is another example:
cases = {
(1..5) => :low,
(6..10) => :moderate,
(11..) => :high,
}.extend(CaseStruct)
cases[3] # => :low
cases[8] # => :moderate
cases[100] # => :high_brightness:
Just write this:
def status(value)
case value
when 1..5
:low
when 6..10
:moderate
when 11..
:high
end
end
status(3) # => :low
status(8) # => :moderate
status(100) # => :high_brightness
Boom! That’s a lot more Ruby-idiomatic and readable by newcomers to the codebase. It’s just standard Ruby.
There is no need for over-engineering or meta-programming if it only saves a couple lines of code. Usually meta-programming is only useful when it saves tens, hundreds or thousands of lines of code, or provides a language that is closer to reality (not farther as in the examples above). Otherwise, it’s considered over-engineering that makes code look foreign to newcomers to the codebase.
If you really want to use a hash though, you could do something like this:
RANGE_STATUSES = {
(1..5) => :low,
(6..10) => :moderate,
(11..) => :high,
}
def status(value)
RANGE_STATUSES.find {|range, status| range.include?(value)}[1]
end
status(3) # => :low
status(8) # => :moderate
status(100) # => :high_brightness
This code is still basic Ruby. No special meta-programming was involved. If it is preferred to use a hash, consider this approach first before looking into meta-programming.
Now, if you want to generalize the solution to reuse many times (not just once), you can still avoid meta-programing by having users rely on a normal class instead of the weird extend
:
# an incomplete naive implementation that does not handle edge cases
class RangeHash
def initialize(hash)
@hash = hash
end
def [](value)
@hash.find {|range, status| range.include?(value)}[1]
end
end
# This is used as a normal class now instead of extending hash
status = RangeHash.new(
(1..5) => :low,
(6..10) => :moderate,
(11..) => :high,
)
status[3] # => :low
status[8] # => :moderate
status[100] # => :high_brightness
Users can now read the code and understand it more simply given that RangeHash
is a standard class that enables picking values based on Range
keys, so there is no mysterious meta-programming. It’s just standard Ruby.
Everytime I catch over-use of meta-programming in my projects by other developers, I delete it and replace it with simple Ruby idiomatic code while lecturing those developers on how to write Ruby-idiomatic code that enables collaboration with others in a team.
Don’t listen to public blog posts about writing “functional”-style Ruby code without applying your own software engineering reasoning to the problem. Many blog posts on the Internet are written by uneducated people who do programming through monkey-see-monkey-do because they never got a full 4-year Computer Science (or computer related) university degree (they are either self-taught or only went to a short bootcamp), so their knowledge is very weak (they never mastered Object-Oriented Programming), and they tend to repeat things they heard from others that they deem “important” without them truly understanding the words they repeat. You can easily reveal their lack of depth of knowledge by simply asking them a question outside the words they repeat, and those programmers will always break down and fail at answering your question.
Functional programming is only useful for implementing highly mathematical logic, but for most business applications, Object-Oriented Programming results in the most productive and maintainable code that is closest to reality. Ruby is a hybrid OO/FP language. You should take advantage of that and use the right tool for the job instead of trying to force functional style everywhere like what bad unpragmatic programmers do (who are 60% of programmers nowadays sadly).
In conclusion, write Ruby-idiomatic code, don’t re-invent the wheel, think of other developers that might join your project team, use the right software paradigm for the job (like OOP), and avoid meta-programming (e.g. CaseStruct) unless it is absolutely needed, which is very rare.