A Reckless Example of Using Basic Meta-programming Concepts and How it Paid Off

Luis Reyes Bartolome
6 min readJul 15, 2020

Just before we begin, I wanted to give you a fair warning. The example i’m going to use will be a case of using TTY-Prompt where my user input was controlled. Because it was controlled , the returning values were controlled as well. This will save you energy so long as you avoid being careless. This can work if you loose up some of the user input restrictions, but just know that the possibility of something going wrong becomes greater. If you act too reckless, this will bite you in the butt.

Warning screen from the Jackass tv show.
Be reckless, just be smart about it too

As a fairly new student/practitioner of Ruby, there are some things about this language that just seem nice and makes things easier to read. The syntactic sugar sometimes feels a bit sweeter using this language compared to others. But even so, one of the first things they always teach you about Ruby that is easy to forget is that it is meant to be dynamic. It’s easy to forget as you begin to learn the language, but once you make that true realization, you’ll notice a lot of paths will open up for you. The following is an example of recklessly applying this to a user’s input to clean up my code and open up the path of making it even more modular.

At the last week of the Ruby section for my coding boot camp, I was put into a group with some classmates and asked to come up with a CLI application that used 3 models together. The goal was to show our understanding of CRUD & ActiveRecord. While making this, we made the decision that instead of just using puts and gets to spit out information and get the user’s reply, we would use TTY-Prompt (which i totally recommend). To cut to the chase, the gem would allow me to call a prompt, feed it a question and an array/hashes, and have it call a method based on the result. By using TTY-Prompt I was able to mash a bunch of puts & gets into one line with the added bonus that I can feed a hash where the hash[key] would be read to the user as a choice while the hash[value] would be returned to me. It opened the ability to use case on the results and lead it to other methods within the code, ultimately mimicking a real interactive menu.

long code calling TTY-Promptand case being used on a variable. There’re 6 when statements redirecting to another class method
This was the code before replacing the array with hash, but the end result as the same, I had to add a bunch of case/when to direct the user to somewhere else. No bueno. This can be done better…

Our initial mission was to get the code working, but there was something that was bothering me. We had similar code through out our CLI app where it was they kept referring to the same model, but they just called a different method onto it to either access an attribute or to a refer to the same instance. Because we were trying to be a bit nicer and have things easier to read for the future, our app ended up having somewhere around 415 lines!!! Hopping from section to section became more tedious just because of the number of lines in the code vs what was actually happening. The program just looked bloated. Here is where we can use some<basic concepts of >META-PROGRAMMING! to make things neater and a little bit cooler.

Let’s look at an example from my original code. In the program, there was a class method called analytics_page which would execute another Class#method from a different model which ultimately puts something. TTY-Prompt asks a question, I gave an array which returns the inside element once the user chooses it. I would then see what that value was, and execute the respective method.

An example of my analytics_page method. It uses TTY-Prompt, uses a basic array of choices. Then it does a lot of case/whens
After creating running the prompt and getting the user’s input. I made a bunch of case/when to decide which method to execute. It’s a lot of lines with the same format and feel. We can do this better.

Quick aside, TTY-prompt#select will accept a hash as the second argument,and will automatically make the hash’s keys to choices for the user to select, and will return whatever the hash’s values. But the logic can still be applied outside of TTY-Prompt. you just take whatever you receive(gets or received from outside) from the user and search the hash to see it exists and return the value***
Revisiting the code, I felt I could make it a bit more dynamic and cleaner. My solution was to create a hash with an array as the values. Then based on what the input was received, I can look at the first value in the value array, and see if it is genre, artist, or user. I can then call
#send with the second element in the value hash to the respective model and execute whatever was the second value of the array on the respective class.

Revisiting the code, I felt I could make it a bit more dynamic and cleaner. My solution was to create a hash with an array as the values. Then based on what the input was received, I can look at the first value in the value array, and see if it is genre, artist, or user. I can then call #send with the second element in the value hash to the respective model and execute whatever was the second value of the array on the respective class.

I took out the array of choices in TTY-Prompt and put it into a variable outside. I then re-wrote it as a hash. I then checked the first element of the value array, where I purposefully made it refer to a string which would lessen the case checks. The #send method takes out the syntactic sugar of saying something like Class.<whatever you want>. Normally that typing the request out this way is prettier to look at, but by ignoring that and using send, you can make the calls more dynamic and replace it with a string of whatever you want. As long as that method exists, it will execute.

This is already making the code a bit leaner and using less checks, but we can go even further. I still have to do a check to see which class i want to direct it to. If i can get rid of that, i can take out those case/whens and maybe keep it all into one line. This is where another Object method comes in handy, Object.const_get() . Since in my hash, I already have a value that is an array, I can make one of the elements a string of the Class name I want to search for. When I feed that string to Object.const_get and chain send() to it, I can now dynamically call any class and any method I want, including the ones I came up with in the menu_hash.

The same method as mentioned before, just with the case/whens replaced with Object.const_get().send(). It is in fewer lines!
So I was able to remove all the case/whens and execute whatever I wanted based off of the User’s input. I didn’t have to concern myself too much of what the user chose, because I had already limited his options when I created the hash. Also has the bonus effect that if I find myself doing something similar all throughout my code, I can just put the Object.const_get().send() into another method with some parameters, and just references it throughout my code as necessary.
Here is the code from earlier changed to be a bit more dynamic. If the need arose, I can refactor this to take a hash from anywhere else and run the code from here. Or I can just keep this method to just create this hash and a question, and feed thatinto a separate method that takes those 2 parameters and execute whatever I want. i’d lose unnecessary lines in the code and keep stuff dynamic & neat.

Now doing all this based on user input is a reckless, because who knows what we might receive. The potential downside to this is if you loosen up what you pass to these arguments, you open your program up for errors, or to allow someone pass in a code you don’t want. In the examples above, since i was using #send after a TTY:Prompt #select asking the user for input, I was better able to control what was being passed. In a way, the inputs were sanitized. The more “open” the input is, the more restrictive you will want to be on how you use that input. But if you’re feeling extra reckless and brave, you can take user input directly, and chain things like define_method or check the string against public_class_method . But that dive into Metaprogramming will have to come another day.

--

--