Use An Ask, Don’t Tell Policy With Ruby
This article was originally published by Pat Shaughnessy on his personal blog. With his kind permission, we are sharing it here for Codeship readers.
Innisfree is an actual island inLough Gill, County Sligo, Ireland.
The next time you need to develop a new algorithm, ask Ruby for what you want, don’t tell it what to do.
Don’t think of your code as a series of instructions for the computer to follow.
Instead, ask Ruby for what you need: Your code should state the solution to your problem, even if you’re not sure what that solution is yet! Then dive into more and more detail, filling in your solution’s gaps as you do. This can lead to a more expressive, functional solution that you might not find otherwise.
Too often over the years I’ve written code that consists of instructions for the computer to follow. Do this, do that, and then finish by doing this third thing. As I write code I imagine I am the computer, in a way, asking myself: What do I need to do first to solve this problem? When I decide, this becomes the first line of code in my program. Then I continue, writing each line of code as another instruction for the computer to follow.
But what does “Ask, Don’t Tell” mean exactly? And how could Ruby possibly know the answer when I ask it something? An example will help you understand what I mean.
Parsing a Yeats Poem
Last week I needed to parse a text file to obtain the lines of text that appeared after a certain word. My actual task was very boring (separating blog articles from their metadata), so instead let’s work with something more beautiful, The Lake Isle Of Innisfree:
I will arise and go now, and go to Innisfree, And a small cabin build there, of clay and wattles made: Nine bean-rows will I have there, a hive for the honeybee, And live alone in the bee-loud glade.
And I shall have some peace there, for peace comes dropping slow, Dropping from the veils of the morning to where the cricket sings; There midnight's all a glimmer, and noon a purple glow, And evening full of the linnet's wings.
I will arise and go now, for always night and day I hear lake water lapping with low sounds by the shore; While I stand on the roadway, or on the pavements grey, I hear it in the deep heart's core.
My task is to write a Ruby script to return the line that contains a given word, along with the following lines:
Telling Ruby What To Do
When I first wrote this script, I put myself in the computer’s shoes: What do I need to do to find the target word? I started writing instructions for Ruby to follow.
First I need to open the file and read in the poem:
File#readlines saves all the lines of text into an array, which the
parse method will process, returning the result in another array. Later I join the result lines together and print them out.
How do I implement
parse? Again, I imagine that I am the computer, that I am Ruby. How do I find the lines that follow glimmer? Well, obviously I need to loop through the array looking for the target word.
Once I find the word, I’ll start saving the lines into a new array called
result. Since I want to save all the following lines and not just the matching line, I’ll also use a boolean flag to keep track of whether I’ve already seen the target.
What’s wrong with this code? Nothing really. It works just fine, and it’s even somewhat idiomatic Ruby. In the past, I would have probably considered this done and moved on.
However, I can do better than this. I can ask Ruby for what I want, instead of telling Ruby what to do.
Ask Ruby For What You Want
Don’t imagine you are the computer. Don’t think about how to solve a problem by figuring out what Ruby should do and then writing down instructions for it to follow. Instead, start by asking Ruby for the answer.
What should my method return? An array of the lines that appear after the target word. To reflect this, I’ll rename my method from
parse (telling Ruby what to do) to
lines_after (asking Ruby for what I want).
This might seem like an unimportant detail, but naming methods is one of the most difficult and important things a programmer does. Picking a name for a method gives the reader a hint about what the method does, about what your intentions were when you wrote it. Think of writing code the same way you would think of writing an essay or story. You want your readers to understand what you are saying, and to be able to follow along. (You also want them to enjoy reading enough that they consider the code to be their own someday.)
To get started I’ll write the new method to return an empty array.
Notice on the left I changed the label from “Instructions:” to “What do I want?” This reflects my new way of thinking about the problem.
Now, what does “appear after the target word” mean exactly? It means the lines that appear in the array after (and including) the line containing the target. Ah… in other words, the
lines_after method should return a subset or slice of the array. Rewriting the problem in a different way lead me towards a solution I hadn’t thought of before.
Now I can rewrite the “What do I want?” text like this:
I rewrote what I want from Ruby to be more specific: I want a “portion of the array” and I want the portion “including and following the line containing the target.” I haven’t written much code yet, but I’ve taken a big step forward in how I think about the problem.
On the right, I’ve written code to return a subset of the array,
lines[target_index..-1]. But my solution is still incomplete; what should
Thinking about this a bit, it’s easy to see how to find the line containing the target string: I can use detect to find the line that includes the target word.
But I’m still not done. I need the index of the line containing the target, not the line itself. How can I find
target_index? Again, I shouldn’t tell Ruby what to do (maybe create a local variable and loop through the lines checking each one). Instead, I should ask Ruby for what I need. What do I need? I need the index which corresponds to the line containing the target. In other words, I need to find (to detect) the target index, not the target line.
Here’s how to do it:
Here I use Ruby’s
detect method to search a range of index values, not lines. Inside the block I check whether the line corresponding to each index
(lines[i]) contains the target. At the bottom I return the correct slice of the array if I found the target, or an empty array if I didn’t.
Learning From Functional Languages
In my opinion this code is better than what I showed earlier. Why? They both work equally well. What’s the difference? Let’s take a look at them side-by-side.
First of all, I have simpler, more terse code. Less code is better. The
lines_after method contains just four lines of code while the
parse method contains nine. Of course, I could find ways to rewrite
parse to use fewer lines, but any way you look at it
lines_after is simpler than
parse method contains two local variables which are changed, or mutated, by code inside the loop. This makes the method harder to understand. What is the value of
flag? What about
result? To really understand how
parse works you almost need to simulate the loop inside your head, thinking about how the flag and result values change over time.
lines_after method also contains two local variables. However, they aren’t used in the same way -- they aren’t changed as the program runs. The block parameter,
i, while different each time the block is called, doesn’t change inside the block. It’s meaning is clear and unambiguous while that block is running. Similarly, the
target_index variable is set once to an intermediate value, not changed once each time around a loop.
Terse, simple code that doesn’t change values while it is running is the hallmark of functional programming languages like Haskell or Clojure. While these languages allow you to write concurrent code without using locks, their chief benefit is that they encourage (Clojure) or even force you (Haskell) to write simple, terse code. Code that asks the computer for what you need, not code that tells the computer what to do.
But, as we’ve seen, you don’t need to abandon Ruby to write functional code.
Update: Simon Kröger and Josh Cheek both suggested using
drop_while, which gives us an even more readable, functional solution:
I also decided to rename the after method to
lines_after, based on the comments from TenderGlove and John Kary. I agree with them
after would make more sense if I called it as a method on an object containing the lines (e.g.,
lines.after). But as a simple function like in this example
lines_after is more expressive.
Learning From Sandi Metz
In her famous book, Practical Object-Oriented Design in Ruby, Sandi Metz mentions the Ask, Don’t Tell policy also, but using slightly different words. With her brilliant bicycle examples, Sandy shows us in Chapter 4 of POODR why we should be Asking for “What” Instead of Telling “How”. When you send a message to an object, you should ask it for what you want, not tell it what to do or make assumptions about how it works internally. Sandi shows us how this policy -- along with other important design principles -- helps us write classes that are more independent and decoupled one from the other.
The Ask, Don’t Tell policy applies equally well to functional programming and object oriented programming. At a lower level, it helps us write more terse, functional Ruby methods. Stepping back, it can also help us design object oriented applications that are easier to maintain and extend.
Update #2: Apparently I’ve (unknowingly) conflated “Ask, Don’t Tell” with the “Tell, Don’t Ask,” advice Dave Thomas has been giving us for years to make a different but related point about object oriented design. Dave explains here: Telling, Asking, and the Power of Jargon. He also disagrees with my opinion that the
parse_lines example was written in a functional style.
Stay up to date
We'll never share your email address and you can opt out at any time, we promise.