Declarative Programming in Ruby
I’ve been programming in Ruby for almost two years now, and sometimes I forget how hard it was for me to wrap my head around the Ruby approach to solving problems. Finally, Tyler Mcginnis has given me the vocabulary to describe the paradigm shift I experienced — Imperative vs Declarative code.
With imperative code, you’re telling your program how to do something. And with declarative code, you’re telling your program what you want to do.
-Tyler Mcginnis, React Fundamentals
Coming from a C++ background, I was used to imperative programming; those mental pathways were well-trodden. Although Ruby can be used to write imperative code, its expressiveness makes declarative code possible. The ‘each’ method was a revelation to someone who had been writing ‘for’ and ‘while’ loops for years.
Example: Performing an operation on every element of an array
- Imperative Anti-pattern: Manually keeping track of array index
i = 0while i < array.length do puts array[i]end
- Declarative alternative: #each
array.each do |element| puts elementend
However, Ruby doesn’t stop there. If you still find yourself defaulting to using each for every problem and using local variables to keep track of state, consider whether a simpler solution exists. Here are some anti-patterns to watch for, along with iterators that will make your code more declarative.
Anti-patterns in Ruby Iterators
Creating a new array from an existing array
- Desired return type: Array
- Imperative Anti-pattern: Manually adding elements to new array
squares = []array.each do | element | squares << element ** 2endsquares
- Declarative alternative: #collect
array.collect { | element | element ** 2 }
Returning a subset of an array satisfying criterion
- Desired return type: Array
- Imperative Anti-pattern: Manually adding elements to new array
odds = []array.each do | element | odds << element if element.odd?endodds
- Declarative alternative: #select
array.select { | element | element.odd? }
Returning the first element of an array satisfying a certain criterion
- Desired return type: Object (whatever class the element is)
- Imperative Anti-pattern: Using a local variable to store element
first_positive = 0array.each do | element | first_positive = element if element > 0endfirst_positive
- Declarative alternative: #detect
array.detect { | element | element > 0 }
Determining the number of elements of an array satisfying a certain criterion
- Desired return type: Integer
- Imperative Anti-pattern: Manually incrementing a counter
match_count = 0array.each do | element | match_count += 1 if element % 10 == 0endmatch_count
- Declarative alternative: #count
array.count { | element | element % 10 == 0 }
Determining whether any elements of an array share a certain characteristic
- Desired return type: Boolean
- Imperative Anti-pattern: Manually toggling a boolean flag
is_present = falsearray.each do | element | is_present = true if element < 0endis_present
- Declarative alternative: #any?
array.any? do | element | element < 0end
Determining whether all elements of an array share a certain characteristic
- Desired return type: Boolean
- Imperative Anti-pattern: Manually toggling a boolean flag
all_even = truearray.each do | element | all_even = all_even && element.even?endall_even
array.all? do | element | element.even?end
Conclusion
Once you’ve gotten the hang of declarative programming, imperative programming begins to feel a bit like micro-managing. Learn to let the details go; stop worrying about managing state and put Ruby’s iterators to work for you.