Declarative Thinking with Higher Order Functions and Blocks
Here's an example question we ask as part of our Learn Ruby on Rails and Launch Your First App course:
"Pick out all thed odd numbers from the array [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] and print them out.
Typically we get two types of solutions:
solution 1:
array = [1,2,3,4,5,6,7,8,9,10]
new_array = []
for i in array
new_array << i if i.odd?
end
puts new_array
solution 2:
array = [1,2,3,4,5,6,7,8,9,10]
new_array = array.select {|element| element.odd?}
puts new_array
Experienced Ruby developers will say that solution 2 is more "idiomatic Ruby" - it's shorter, easier to read, and more "intention revealing", meaning that you can easily determine what the code does by its appearance.
The foundamental difference of the two solutions, however, is that solution 1 focuses on the "steps of implementation" or the "how", which is typical for the imperative style of programming; whereas solution 2 follows the declarative style of programming, using a Ruby block to make a declaration / expression of the problem itself - selecting odd numbers.
To understand what declarative programming is, look no further than SQL and CSS.
Here is an SQL statement:
SELECT supplier_id FROM suppliers WHERE supplier_name = "IBM" or supplier_city = "New York"
In this example, the programmer only needs to express the problem to solve (the "what"), instead of the steps that the database engine has to execute to get the result (the "how"). The database engine takes care of the "how".
Similarly, for this CSS statement:
p { color: red }
The programmer only needs to tell the web browser rendering engine that they want the text in paragraphs to be in red color, not having to care about "how" to achieve that.
Declarative programming has the advantage of elevating programmers to think on a higher level that's more natural for humans. We focus on writing a good description of the problem to solve and the computer takes care of the rest. In comparison, imperative programmers often approach a problem imagining themsevles as the computer - "I am going to do this first, then if this condition is true I do x, otherwise I do y.", "I need to use this variable to hold this value, then increment it as I go through this array". Here is the news: we humans are more productive and happier thinking like humans than pretending to be computers.
Ruby, true to its root of putting programmers first, provides a convenient way to allow this type of declarative programming, and that is writing higher order functions with blocks. If you look at methods for Array, you will see quite many methods take a block as a parameter. The methods themselves are typically verbs, such as "select" or "map", and the block is used to provide the conditions or extra behaviors while performing that action, like the example we used before:
array.select {|element| element.odd?}
If you draw the parallel to SQL here, the methods are like SQL's SELECT, UPDATE, INSERT, DELETE commands, pointing out the general "shape" of problem to solve, while the block parameter provides further refinements of conditions or behaviors, just like other parts of SQL such as WHERE or JOIN.
Let's look at a not-as-trivial example on how we can take advantage of this pattern and write better code.
Suppose we have the following models in Rails:
class Course < ActiveRecord::Base
has_many :students
...
end
class Student < ActiveRecord::Base
has_many :assignments
...
end
class Assignment < ActiveRecord::Base
# assignment has a 'grade' property
end
And we want to generate a grade card for a course in the format of a json style hash:
{
'A' => [ "Alice", "Adam", "Anthony" ],
'B' => [ "Bill", "Bob" ],
'C' => [ "Crystal", "Claire", "Chris" ]
}
We could jump into the imperative way of thinking - loop through the students of the course, and for each student we loop through their homework assignments, look at them and put them in the "A", "B" and "C" buckets depending on their grade. But instead, let's think like a declarative programmer:
-
What's the general "shape" of the problem we are trying to solve? Well, we want to extract pieces of information from each student then combine into a course grade card. The shape of "getting pieces of information from a collection's elements to build up one thing", suggests that we have a reduce here. (I like "reduce" better than "inject" because of the MapReduce inference). So now we know we need to write something like this:
students.reduce { .... }For a reduce to work, we now need to supply two pieces of extra behaviors: a) what information we need to extract from each student, and b) how we combine those pieces into the grade card.
-
a) is quite obvious - we need to get a student's grades across all course assignments. The "shape" of "all grades from all course assignments" suggests that we need a "map" here - Again, no imperative looping through assignments! We will add a "uniq" in the end to de-duplicate the grades.
class Student < ActiveRecord::Base .... def grades assignments.map(&:grade).uniq end end -
Now let's think about how we combine those grade arrays from students and student names to form a course grade card in the hash format we want. We know we are building towards a hash, but we do not know how to combine the pieces into the course grade card hash yet. But let's say we use a magic function to do so:
students.reduce({}) {|grade_card, student| magic_function(student.grades_from_assignments, student.name) }And in this magic function, we can whip a hash into accepting an array and a string to return another hash... Or should we? Why should this magic_function be in the Course class? Is hash too primitive as a data structure for this non trivial combination logic?. So let's use a class instead that knows how to do this better. As we write out this class, we find that we only need to pass in a student object and interrogate it to get both the grades and its name.
class GradeCard attr_reader :data def initialize(data={'A' => [], 'B' => [], 'C' => []}) @data = data end def add_student_grades(student) student.grades.each do |grade| @data[grade] << student.name end GradeCard.new(@data) end end -
Now we can finally write out reduce function we started in step 1:
class Course has_many :students def grade_card students.reduce(GradeCard.new) {|grade_card, student| grade_card.add_students_grades(student)} end end
Ruby provides a good set of higher order functions taking blocks as parameters, most notably in the Array and Hash classes, such as each, select, push, pop, delete_if, map, reduce, merge, replace etc for programmers to think and express declaratively. Determine the type of problem you are solving, pick the right higher order function, then work out the details of what you need to supply. This process can help you quickly break down a bigger problem into smaller pieces as we saw above, avoiding "20 line long imperative steps in a method" and leading to cleaner, modular code.
You can also write your own methods this way to take blocks to allow refinments or extra behaviors, and hide the mechanical steps of implementation (we will cover this in a future blog post). Users of your methods will really thank you for allowing them focus on a higher level of abstraction - and that "user" is most likely the future you.