Assignment of a code block to a variable using Proc and its application in TTY Table dynamic column expansion

Bashir
6 min readJun 3, 2020

--

Have you ever thought about what we can store in variables? We can store data of primitive types such as integers, strings, and floats, but also non-primitive types such as arrays, hashes as well as custom types (such as our class instances). Adding to that list, we can also assign a piece of code to a variable and we can use that code over and over again. At this point, you may be asking yourself, isn’t that what a method does? Yes, but as we shall see there’s a slight difference between storing code as a variable and calling a method.

While working on a CLI application, I came across a problem with dynamically changing the number of columns a TTY table can have. The table headers and thus the number of columns has to be predefined; this may present a hurdle when trying to display dynamic data with a variety of columns. I was dealing with the following data, which is an array of hashes of hashes.

debt_payments =[{79=>{'amount' => 1000.0, 'min_due' => 100.0, 'due_date' => "May, 2020"}},{79=>{'amount' => 901.64, 'min_due' => 100.0, 'due_date' => "Jun, 2020" }},{79=>{'amount' => 803.12, 'min_due' => 100.0, 'due_date' => "Jul, 2020" }},{79=>{'amount' => 704.44, 'min_due' => 100.0, 'due_date' => "Aug, 2020" }},{79=>{'amount' => 605.59, 'min_due' => 100.0, 'due_date' => "Sep, 2020" }},{79=>{'amount' => 506.58, 'min_due' => 100.0, 'due_date' => "Oct, 2020" }},{79=>{'amount' => 407.41, 'min_due' => 100.0, 'due_date' => "Nov, 2020" }}]

The number 79 above corresponds to a debt id and points to a hash for information about the debt’s principal amount and minimum amount due for a particular month.

We can display the above data in a terminal using the TTY table gem. First, we will define a method that will handle the logic behind displaying the data:

def display_table(debt_payments)
table = TTY::Table.new header: ["Date", "Amount", "Minimum Due"]
debt_payments.each do|debt|
table << [debt[79]['due_date'], debt[79]['amount'], debt[79]['min_due']]
end
puts table.render(:ascii)
end

In the above code, we are passing into the method the debt_payments array, which we defined earlier as an array of hashes. We are initializing a tty table variable and assigning to it the column headers of “Date”, “Amount”, and “Minimum Due.” Then we are iterating through the array, with each iteration, we are shoveling into the table the info for each row. The “puts” statement above renders the table on to the terminal as shown below.

This is a satisfactory function assuming our parameter (debt_payments) has only one debt. What if we have two debts, as depicted in the updated data array below.

debt_payments = [{87=>{'amount' => 2000.0, 'min_due' => 500.0, 'due_date' => "May, 2020" }, 79=>{'amount' => 1000.0, 'min_due' => 100.0, 'due_date' => "May, 2020" }},{87=>{'amount' => 1532.87, 'min_due' => 500.0, 'due_date' => "Jun, 2020" }, 79=>{'amount' => 901.64, 'min_due' => 100.0, 'due_date' => "Jun, 2020" }},{87=>{'amount' => 1058.06, 'min_due' => 500.0, 'due_date' => "Jul, 2020" }, 79=>{'amount' => 803.12, 'min_due' => 100.0, 'due_date' => "Jul, 2020" }},{87=>{'amount' => 575.45, 'min_due' => 500.0, 'due_date' => "Aug, 2020" }, 79=>{'amount' => 704.44, 'min_due' => 100.0, 'due_date' => "Aug, 2020" }},{87=>{'amount' => 84.9, 'min_due' => 84.9, 'due_date' => "Sep, 2020" }, 79=>{'amount' => 605.59, 'min_due' => 515.1, 'due_date' => "Sep, 2020" }},{87=>{'amount' => 1.39, 'min_due' => 1.39, 'due_date' => "Oct, 2020" }, 79=>{'amount' => 91.48, 'min_due' => 91.48, 'due_date' => "Oct, 2020" }
}
]

The above data can be displayed with the info for the debt with id 87 next to the debt with id 79.

This is our modified code to display the table above:

def display_table(debt_payments)
table = TTY::Table.new header: ["Date", "Amount", "Minimum Due", "Date","Amount", "Minimum Due"]

debt_payments.each do|debt|
table << [debt[87]['due_date'], debt[87]['amount'], debt[87]['min_due'],debt[79]['due_date'], debt[79]['amount'], debt[79]['min_due']]
end
puts table.render(:ascii)
end

Because there are currently two debts, we repeat the same code as for the one debt except we have to add to the header an extra “Date”, “Amount”, and “Minimum Due” to correspond to the second debt. Also, we shovel into the table variable the info for each row, which in this case corresponds to the info for both debts. But how can we change this function to handle any number of debts and print the table with any number of columns?

To make the table header grow dynamically is quite simple. First, we count how many debts there are, and for each debt, we can add the corresponding header.

table_header = []debts.each do |debt|
table_header.push("Date", "Amount", "Minimum Due")
end

but how can we grow the line of code below dynamically?

table << [debt[79]['due_date'], debt[79]['amount'], debt[79]                  ["min_due"]]

Well, we can’t really store the above code in an array the same way we store strings. Instead, we can use Proc to store this code in a variable. According to the ruby documentation, “Proc objects are blocks of code that have been bound to a set of local variables. Once bound, the code may be called in different contexts and still access those variables.” In other words, procs are just like methods, with the exception that variables defined within their scope are remembered. This will become clear after we apply proc to our example.

Let’s build our method step by step and incorporate proc to make our table grow dynamically to hold the given data.

def display_table(debt_payments)
ids = debt_payments.first.keys #=> gets ids of each debt
table_header = [] #=> saves the header
table_row = [] #=> saves the procs for each row
ids.each do |id| #=> adds header for each debt
table_header.push("Date", "Amount", "Minimum Due")
code_var = Proc.new {|debt| [debt[id]['due_date'], debt[id]['amount'], debt[id]['min_due']]}

table_row.push(code_var)#=> for each debt, we are saving a proc
#=> each proce will have a unique debt id
end

The above code gives us a table_header array that consists of all the needed headers. This will grow and shrink in response to the number of debts as explained previously. But we also have a table_row_format array that will store our row structure for a particular debt. The proc is created within the same iterator used for the header creation. Proc, when called, is expecting “debt” to be passed in (denoted by the pipe symbol). If you take a look within the proc code, does the “id” variable have to be passed in as a parameter, the same way we pass in the “debt” parameter? No, it’s not necessary because “id” in this case will be equivalent to the value of “id” in the pipe of the “each” enumerable. This is one very important distinction between methods and procs: A proc will remember the variables within its environment, while a method will not.

Let’s move along and see how the proc objects we created above can be used.

*********** Fully Dynamic Function ************def display_table(debt_payments)
ids = debt_payments.first.keys #=> gets ids of each debt
table_header = [] #=> saves the header
table_row = [] #=> saves the procs for each row
ids.each do |id| #=> adds header for each debt
table_header.push("Date", "Amount", "Minimum Due")
code_var = Proc.new {|debt| [debt[id]['due_date'], debt[id]['amount'], debt[id]['min_due']]}

table_row.push(code_var)#=> for each debt, we are saving a proc
#=> each proce will have a unique debt id
table = TTY::Table.new header: table_header #=> initialize headerdebt_payments.each do|debt|
new_row = table_row.map{|code_var| code_var.call(debt)}.flatten
table << new_row
end
puts table.render(:ascii)end

in the above code, we are iterating through each debt, and passing the debt into each proc stored in the table_row. Each proc will give us the details for one debt. For clarification, let’s say we have one debt and one-row data as depicted below:

{79 => {'amount' => 1000.0, 'min_due' => 100.0, 'due_date' => "May 2020"}}

When we create a proc for the above debt in the same way we did previously:

ids.each do |id|    
code_var = Proc.new {|debt| [debt[id]['due_date'], debt[id]['amount'], debt[id]['min_due']]}

All we are doing is storing the following code in the “code_var” variable:

code_var = [debt[79]['due_date'], debt[79]['amount'], debt[79]['min_due']]

Notice how the “id” variable in the proc is assigned the id value from the “ids” iteration. Now, code_var can be called as follow:

debt = {79 => {'amount' => 1000.0, 'min_due' => 100.0, 'due_date' =>     "May 2020"}}code_var.call(debt) # remeber that proc is expecting a debt#this proc call will return the following :
["May 2020", 1000.0, 100.0]

Along with the TTY Table documentation, I hope you now have a complete understanding of how to dynamically increase the number of columns of a table to fit any data given. Also, if you didn’t grasp the concept of proc fully, feel free to read about it in the documentation and learn how it can be applied in other scenarios as well. You may also be interested in reading about lambda which is an alternative to Proc.

--

--