Author Archives: Jason Swett

The “20 Questions” method of debugging

There are three steps to fixing any bug: 1) reproduction, 2) diagnosis and 3) fix. This post will focus on the second step, diagnosis.

There are two ways to approach diagnosing a bug. One is that you can stare at the screen, think really hard, and try to guess what might be going wrong. In my experience this is the most common debugging approach that programmers take. It’s neither very efficient nor very enjoyable.

Another way is that you can perform a series of tests to find where the cause of the bug lies without yet worrying about what the cause of the bug is. Once the location of the bug is found, it’s often so obvious what’s causing the bug that little to no thinking is required. This method of bug diagnosis is much easier and more enjoyable than just thinking really hard, although unfortunately it’s not very common.

20 Questions strategies

You’ve probably played the game 20 Questions, where one player thinks of an object and the other players ask up to 20 yes-or-no questions to try to guess what the object is.

Smart players of 20 Questions try to ask questions that divide the possibilities more or less in half. If what’s known about the object is that it’s an animal, but nothing more, then it’s of course a waste of a guess to guess whether the animal is a turtle, because if the answer is no, then you’ve only eliminated a small fraction of all the kinds of animals that the target object might be because not very many kinds of animals are turtles.

Better to ask something like “Does it live on land?” or “Is it extinct?” which will rule out larger chunks of possibilities.

The binary search algorithm

This “eliminate-half strategy” is the genius of the binary search algorithm, which repeatedly chops a sorted list in half to efficiently find the target item. If you can determine that an item is not present in one half of the list, then it’s necessarily true that the item lies in the other half of the list. If you’re searching a list of 1000 items, you can chop the list down to 500, then 250, then 125, 63, 32, 16, 8, 4, 2, 1. That’s only 10 steps for a list of 1000 items. I find it pretty impressive that a single item can be found in a list of 1000 items in only 10 steps.

The simplicity of the binary search algorithm must be why, a surprisingly large amount of the time, a plastic toy that costs $12.99 is able to correctly determine what object you’re thinking of within 20 guesses. The machine seems miraculously intelligent but its job is probably actually requires more drudgery than actual thinking. All that presumably has to be done is to place the world’s information into nested categories, each “higher” category being about twice as broad as the one below it. For a human to manually perform these categorizations would take a lot of work, but once the categorizations are made, carrying out the work of the binary search steps is mindlessly simple.

If binary search is powerful enough that a cheap toy can use it to guess almost any object you can think of, it’s not hard to imagine how useful binary search can be in pinpointing a bug in a web application.

Playing “20 Questions” with your IT system

Whenever I’m faced with a bug, I play “20 Questions” with my IT system. I ask my system a series of yes-or-no questions. Each answer rules out a chunk of the system as the area which contains the bug. Once my answer has allowed me to rule out an area, I ask a question on the area of the system which hasn’t yet been ruled out. Eventually the area that hasn’t been ruled out becomes so tiny that it’s (much of the time) totally obvious what the root cause is.

Of course, I can’t literally “ask” questions of my system. Instead I devise a series of yes-or-no tests that I can perform. For example, let’s say my web service is not resolving requests. The first question I might ask is “Is the site actually down or is it down for just me?” For that question my test would involve visiting downforeveryoneorjustme.com and typing in the URL for my website. If the site is in fact down for everyone, then my next question might be “Are requests making it to my web server?” For that question my test would involve looking at my web server’s logs to see evidence of HTTP requests being received. And so on.

Unlike a real binary search algorithm, I don’t always do a 50/50 split. Often it’s more economical to do what you might call a “weighted binary search” instead.

Splitting on size vs. splitting on likelihood

The reason a binary search algorithm splits lists in half is because we want each yes-or-no question to eliminate as much search area as possible, but we don’t know whether the answer will be a yes or a no, and so splitting the list in half guarantees that we never eliminate less than half of the list. If for example we chose a 75/25 split instead of a 50/50 split, we risk eliminating only 25% of the possibilities rather than the other 75% of the possibilities. That would be a waste.

The 50/50 strategy makes sense if and only if you don’t know the probability of the target item appearing in one half or the other half (or if the chances truly are 50/50). The strategy makes less sense if have clues about where the target item lies.

For example, I become aware that a certain bug was present after I performed a certain deployment and absent before that, I don’t have to waste time stupidly dividing my entire codebase in half repeatedly in order to find the root cause of the bug. It’s reasonable for me to believe that the code introduced by the deployment is likely to (although of course not certain to) contain the offending code. The change in the deployment might represent just 1% or less of the codebase, meaning that if my guess was wrong that that deployment introduced the bug, then I will only have eliminated 1% of the codebase and my search area will still be the remaining 99%. But because the risk of my guess being wrong is usually so low, and because the cost of performing the yes/no test is usually so small, it’s a win on average to do this kind of 1/99 split rather than a 50/50 split.

Takeaways

The “20 Questions Method of Debugging” is to perform a binary search on your codebase. Divide your codebase roughly in half, devise a test to tell you which half you can rule out as containing the root cause of the bug, and repeat until the remaining search area is so small that the root cause can’t help but reveal itself.

More precisely, the 20 Questions Method of Debugging is a “weighted binary search”. If you have a good reason to believe that the root cause lies in a certain area of your codebase, then you can save steps by immediately testing to see if you can rule out everything that’s not the suspect area rather than doing a dumb 50/50 split. You will probably “lose” sometimes and end up eliminating the smaller fraction rather than the larger fraction, but as long as your guesses are right enough of the time, this strategy will be more efficient on average than always doing a 50/50 split.

The 20 Questions Method of Debugging is a better method of debugging than the “stare at the screen and think really hard” method for two reasons. The first reason is that the 20 Questions Method is more efficient than the “sit and think” method because thinking is expensive but performing a series of yes-or-no tests is cheap. The second reason is my favorite, which is this: the “sit and think” method may never yield the right answer, but the 20 Questions Method is virtually guaranteed to eventually turn up the right answer through power of sheer logic.

How to program in feedback loops

One of the classic mistakes that beginning programmers make is this:

  1. Spend a long time writing code without ever trying to run it
  2. Finally run the code and observe that it doesn’t work
  3. Puzzle over the mass of code that’s been written, trying to imagine what might have gone wrong

This is a painful, slow, wasteful way to program. There’s a better way.

Feedback loops

Computer programs are complicated. Human memory and reasoning are limited and fallible. Even a program consisting of just a few lines of code can defy our expectations for how the program ought to behave.

Knowing how underpowered our brains are in the face of the mentally demanding work of programming, we can benefit from taking some measures to lessen the demand on our brains.

One thing we can do is work in feedback loops.

The idea of a feedback loop is to shift the mindset from “I trust my mind very much, and so I don’t need empirical data from the outside world” to “I trust my mind very little, and so I need to constantly check my assumptions against empirical data from the outside world”.

How to program in feedback loops

The smallest feedback loop can go something like this.

  1. Decide what you want to accomplish
  2. Devise a manual test you can perform to see if #1 is done
  3. Perform the test from step 2
  4. Write a line of code
  5. Repeat test from step 2 until it passes
  6. Repeat from step 1 with a new goal

Let’s go over these steps individually with an example.

Decide what you want to accomplish

Let’s say you’re writing a program that reads a list of inputs from a file and processes those inputs somehow.

You can’t make your first goal “get program working”. That’s too big and vague of a goal. Maybe a good first goal could be “read from the file and print the first line to the screen”.

Devise a manual test you can perform to see if #1 is done

For this step, you could put some code in a file and then run that file on the command line. More precisely, “run the file on the command line” would be the test.

Perform test from #2

In this step, you would perform the test that you devised in the previous step, in this case running the program on the command line.

Significantly, you want to perform this test before you’ve actually attempted to make the test pass. The reason is that you want to avoid false positives. If you write some code and then perform the test and the test passes, you can’t always know whether the test passed because your code works or if the test passed because you devised an invalid test that will pass for reasons other than the code working. The risk of false positives may seem like a remote one but it happens all the time, even to experienced programmers.

Write a line of code

The key word in this sentence is “a”, as in “one”. I personally never trust myself to write more than one or two lines of code at a time, even after 15+ years of programming.

Running a test after each line of code, even if you don’t expect each line to sensibly progress your program toward its goal, helps ensure that you’re not making egregious mistakes. For example, if you add a line that contains a syntax error, it’s good to catch the syntax error in that line before adding more lines.

To continue the example, this step, at least the first iteration of this step, might involve you writing a line that attempts to read from a file (but not print output yet).

Repeat test from #2 until passing

After writing a line of code, perform your test again. Your test might pass. Your test might fail in an unsurprising way. Or something unexpected might happen, like an error.

Once your test results are in, write another line of code and repeat the process until your test passes.

Repeat from step 1

Once your test passes, decide on a new goal and repeat the whole process.

Advanced feedback loops

Once you get comfortable coding in feedback loops, you can do something really exciting, which is to automate the testing step of the feedback loop. Not only can this make your coding faster but it can protect your entire application against regressions (when stuff that used to work stops working).

I consider automated testing a necessary skill for true professional-level programming, but it’s also a relatively “advanced” skill that you can ignore when you’re first getting started with programming.

Bigger feedback loops

In a well-run organization, programmers work in concentric circles of feedback loops. Lines of code are written in feedback loops that last seconds. Automated test suites provide feedback loops that last minutes. Individual tasks are designed for feedback loops that last hours or days. Large projects are broken into sub-projects that last days or weeks.

Takeaways

  • No one’s brain is powerful enough to write a whole program at once without running it.
  • Feedback loops shift your trust from your own fallible mind to hard empirical data.
  • Follow the feedback loop instructions in the post to code faster, more easily and more enjoyably.

How to avoid wasteful refactoring

I’m a big fan of good code. Naturally, I’m also a big fan of refactoring.

Having said that, I certainly think it’s possible to refactor too much. I think the time to stop refactoring is when you’re no longer sure that the work you’re doing is an improvement.

Don’t keep a refactoring if you’re not sure it’s an improvement

If I refactor a piece of code and I’m not sure the refactored version is better than the original version, then I don’t help anything by committing the new version. I should throw away the new version and write it off as a cost of doing business.

I might be tempted to commit the new version because I spent time on it but that’s just sunk cost fallacy talking.

How to tell if a refactoring is an improvement

One way to tell if a refactoring is an improvement is to use heuristics: clear names, short methods, small classes, etc. Heuristics, together with common sense, can of course get you pretty far.

The only way to really, truly, objectively, empirically know if a refactoring is an improvement is to take a time machine to the next time that area of code needs to be changed and A/B test the refactored version against the unrefactored version and see which was easier to work with. Obviously that’s not a performable test.

So we have to do the closest thing we can which is to perform the time-machine test in our imaginations. Sometimes the time-machine test is hard because we’re so familiar with the code that it’s impossible to imagine seeing it for the first time.

When we reach this point, where it’s hard to imagine how easily Maintainer-of-the-Future would be able to understand our code, then it’s usually time to stop refactoring. It’s usually then more efficient wait and reassess the code the next time we have to work with it. By then we will have forgotten the context. Any faults in the code will be more glaringly obvious at that stage. The solutions will be more obvious too.

Refactoring too little

Having said all that, I actually think most developers refactor too little. They might be afraid of being accused of wasting time or gold-plating. But if you know how to tell with reasonable certainty whether a refactoring is an improvement, then you can be pretty sure you’re not wasting time. After all, the whole point of refactoring, and writing good code in general, is to make the code quick and easy to work with.

The surest way to avoid refactoring waste

The surest way to avoid refactoring waste is to keep a policy of doing refactorings before making behavior changes. If you need to make a change to a piece of code but that piece of code is too nasty for you to easily be able to change it, then refactor the code (separately from your behavior change) before changing it. As Kent Beck says, “make the hard change easy (warning: this may be hard) and then make the easy change”.

You can think of this as “lazy loading” your refactorings. You can be sure that your refactorings aren’t a waste because your refactorings are always driven by real needs, not speculation.

Takeaways

  • The time before you change a piece of code is a great time to refactor it. That way you know that your refactoring is driven by a real need.
  • It’s also good to refactor a piece of code after you change it. Just be sure you know how to tell when your work is no longer an improvement, and don’t succumb to the sunk cost fallacy.
  • With those two points in mind, don’t be afraid to refactor aggressively.

Bugfixes don’t make good onboarding tasks

It’s a popular opinion that bugfixes are a good way to get acquainted a codebase. I say they’re not. Here’s why.

Feedback loops

When a new developer starts at an organization, I find it helpful to keep the feedback loop tight. When a new developer can complete a small and easy task, then complete another small and easy task, then complete a slightly bigger and harder task and so on, it helps the developer get comfortable, gain confidence, and know where they stand.

This is why good early tasks have the following characteristics.

  • They’re small
  • They’re easy
  • They’re clearly and crisply defined
  • They don’t require any context that can’t be built into the task instructions
  • They have a clear expected completion time (e.g. “half a day of work”)

Here’s why bugfixes fail every single one of those tests.

Why bugfixes don’t make good onboarding tasks

Bugfixes aren’t always small and easy

Sometimes a bugfix is quick and easy. Often times a bugfix is really hard. Some bugs are the symptoms of underlying problems that are so deep that they can never reasonably be fixed.

Bugfix jobs are often hard to define

In order to fix a bug, the bug needs to 1) be reproduced, 2) be diagnosed and then 3) be fixed. It’s hard to convey to another person exactly what they need to do in order to carry out those steps. It’s going to vary wildly from bug to bug. There’s a lot of room for a person to “wander off the ranch”.

(Note, if the bug has been diagnosed with certainty before the bugfix is assigned to a developer, then the bugfix can be clearly defined and can make a perfectly good onboarding task.)

Bugfixes can require context

Many bugfixes require familiarity with the domain or the application itself in order to be reproduced, diagnosed and fixed successfully.

If a developer is faced with a task that requires him or her to go get some context, that can be a good thing. But often a bugfix requires some narrow piece of context that’s not all that profitable to know. In this case the context-gathering work just adds drag to the developer’s onboarding experience.

Bugfixes almost never have a clear expected completion time

Some bugs take five minutes to fix. Some bugs take weeks to fix. Some bugs require so much work to fix that they’re not even worth fixing.

It’s not great if a new developer is given a task and nobody knows quite how long to expect that developer to take in completing the task.

Takeaway

Unless the root cause has already been diagnosed and the fix can meet the “good onboarding task” criteria above, bugfixes don’t tend to make good onboarding tasks.

How map(&:some_method) works

The map method’s shorthand syntax

One of the most common uses of map in Ruby is to take an object and call some method on the object, like this.

[1, 2, 3].map { |number| number.to_s }

To save us from redundancy, Ruby has a shorthand version which is functionally equivalent to the above.

[1, 2, 3].map(&:to_s)

The shorthand version is nice but its syntax is a little mysterious. In this post I’ll explain why the syntax is what it is.

Passing symbols as blocks

Let’s leave the world of map for a moment and deal with a “regular” method.

I’m going to show you a method which takes a block and then demonstrate four different ways of passing a block to that method.

Side note: if you’re not too familiar with Proc objects yet, I would suggest reading my other posts on how Proc objects work and what the & in front of &block means before continuing.

First way: using a normal block

You’ve of course seen this way before. We call my_method and pass a regular block to it.

def my_method(&block)
  block.call("hello")
end

puts my_method { |value| value.upcase } # outputs "HELLO"

Second way: using Proc.new

If we wanted to, we could instead pass our block using Proc.new. Since my_method takes a block and not a Proc object, we would have to prefix Proc.new with an ampersand to convert the Proc object into a block.

(If you didn’t know, prefixing an expression with & will convert a proc to a block and a block to a proc. See this post for more details on how that works.)

def my_method(&block)
  block.call("hello")
end

puts my_method(&Proc.new { |value| value.upcase })

There would never really be a practical reason to express the syntax this way, but I wanted to show that it’s possible. This “second way” example will also connect the first way and the third way.

Third way: using to_proc

All symbols respond to a method called to_proc which returns a Proc object. If we do :upcase.to_proc, it gives us a Proc object that’s equivalent to what we would have gotten by doing Proc.new { |value| value.upcase }.

def my_method(&block)
  block.call("hello")
end

puts my_method(&:upcase.to_proc)

Fourth way: passing a symbol

I’ll show one final way. When Ruby sees an argument that’s prefixed with an ampersand, it attempts to call to_proc on the argument. So our to_proc on &:upcase.to_proc is actually superfluous. We can just pass &:upcase all by itself.

def my_method(&block)
  block.call("hello")
end

puts my_method(&:upcase)

What ultimately gets passed is the Proc object that results from calling :upcase.to_proc. Actually, more precisely, what gets passed is the block that results from calling &:upcase.to_proc, since the & converts the Proc object to a block.

Passing symbols to map

With the understanding of the above, you now know that this:

[1, 2, 3].map(&:to_s)

Is equivalent to this:

[1, 2, 3].map(&:to_s.to_proc)

Which is equivalent to this:

[1, 2, 3].map(&Proc.new { |number| number.to_s })

Which, finally, is equivalent to this:

[1, 2, 3].map { |number| number.to_s }

So, contrary to the way it may seem, there aren’t two different “versions” of the map method. The shorthand syntax is owing to the way that Ruby passes Proc objects.

Takeaways

  • When an argument is prefixed with &, Ruby attempts to call to_proc on it.
  • All symbols respond to the to_proc method.
  • There aren’t two different versions of the map method. The shorthand syntax is possible due to the two points above.

Understanding Ruby closures

Why you’d want to know about Ruby closures

Ruby blocks are one of the areas of the language that’s simultaneously one of the most fundamental parts of the language but perhaps one of the hardest to understand.

Ruby functions like map and each operate using blocks. You’ll also find heavy use of blocks in popular Ruby libraries including Ruby on Rails itself.

If you start to dig into Ruby blocks, you’ll discover that, in order to understand blocks, you have to understand something else called Proc objects.

And as if that weren’t enough, you’ll then discover that if you want to deeply understand Proc objects, you’ll have to understand closures.

The concept of a closure is one that suffers from 1) an arguably misleading name (more about this soon) and 2) unhelpful, jargony explanations online.

My goal with this post is to provide an explanation of closures in plain language that can be understood by someone without a Computer Science background. And in fact, a Computer Science background is not needed, it’s only the poor explanations of closures that make it seem so.

Let’s dig deeper into what a closure actually is.

What a closure is

A closure is a record which stores a function plus (potentially) some variables.

I’m going to break this definition into parts to reduce the chances that any part of it is misunderstood.

  • A closure is a record
  • which stores a function
  • plus (potentially) some variables

I’m going to discuss each part of this definition individually.

First, a reminder: the whole reason we’re interested in Ruby closures is because of the Ruby concept called a Proc object, which is heavily involved in blocks. A Proc object is a closure. Therefore, all the examples of closures in this post will take the form of Proc objects.

If you’re not yet familiar with Proc objects, I would suggest taking a look at my other post, Understanding Ruby Proc objects, before continuing. It will help you understand the ideas in this post better.

First point: “A closure is a record”

A closure is a value that can be assigned to a variable or some other kind of “record”. The term “record” doesn’t have a special technical meaning here, we just use the word “record” because it’s broader than “variable”. Shortly we’ll see an example of a closure being assigned to something other than a variable.

Here’s a Proc object that’s assigned to a variable.

my_proc = Proc.new { puts "I'm in a closure!" }
my_proc.call

Remember that every Proc object is a closure. When we do Proc.new we’re creating a Proc object and thus a closure.

Here’s another closure example. Here, instead of assigning the closure to a variable, we’re assigning the closure to a key in a hash. The point here is that the thing a closure gets assigned to isn’t always a variable. That’s why we say “record” and not “variable”.

my_stuff = { my_proc: Proc.new { puts "I'm in a closure too!" } }
my_stuff[:my_proc].call

Second point: “which stores a function”

As you may have deduced, or as you may have already known, the code between the braces (puts "I'm in a closure!") is the function we’re talking about when we say “a closure is a record which stores a function”.

my_proc = Proc.new { puts "I'm in a closure!" }
my_proc.call

A closure can be thought of as a function “packed up” into a variable. (Or, more precisely, a variable or some other kind of record.)

Third point: “plus (potentially) some variables”

Here’s a Proc object (which, remember, is a closure) that involves an outside variable.

The variable number_of_exclamation_points gets included in the “environment” of the closure. Each time we call the closure that we’ve named amplifier, the number_of_exclamation_points variable gets incremented and one additional exclamation point gets added to the string that gets outputted.

number_of_exclamation_points = 0

amplifier = Proc.new do
  number_of_exclamation_points += 1
  "louder" + ("!" * number_of_exclamation_points)
end

puts amplifier.call # louder!
puts amplifier.call # louder!!
puts amplifier.call # louder!!!
puts amplifier.call # louder!!!!
puts number_of_exclamation_points # 4 - the original variable was mutated

As a side note, I find the name “closure” to be misleading. The fact that the above closure can mutate number_of_exclamation_points, a variable outside the function’s scope, seems to me like a decidedly un-closed idea. In fact, it seems like there’s a tunnel, an opening, between the closure and the outside scope, through which changes can leak.

I personally started having an easy time understanding closures once I stopped trying to connect the idea of “a closed thing” with the mechanics of how closures actually work.

Takeaways

  • Ruby blocks heavily involve Proc objects.
  • Every Proc object is a closure.
  • A closure is a record which stores a function plus (potentially) some variables.

The two common ways to call a Ruby block

Ruby blocks can be difficult to understand. One of the details which presents an obstacle to fully understanding blocks is the fact that there is more than one way to call a block.

In this post we’ll go over the two most common ways of calling a Ruby block: block.call and yield.

There are also other ways to call a block, e.g. instance_exec. But that’s an “advanced” topic which I’ll leave out of the scope of this post.

Here are the two common ways of calling a Ruby block and why they exist.

The first way: block.call

Below is a method that accepts a block, then calls that block.

def hello(&block)
  block.call
end

hello { puts "hey!" }

If you run this code, you’ll see the output hey!.

You may wonder what the & in front of &block is all about. As I explained in a different post, the & converts the block into a Proc object. The block can’t be called directly using .call. The block has to be converted into a Proc object first and then .call is called on the Proc object.

I encourage you to read my two other posts about Proc objects and the & at the beginning of &block if you’d like to understand these parts more deeply.

The second way: yield

The example below is very similar to the first example, except instead of using block.call we’re using yield.

def hello(&block)
  yield
end

hello { puts "hey!" }

You may wonder: if we already have block.call, why does Ruby provide a second, slightly different way of calling a block?

One reason is that yield gives us a capability that block.call doesn’t have. In the below example, we define a method and then pass a block to it, but we never have to explicitly specify that the method takes a block.

def hello
  yield
end

hello { puts "hey!" }

As you can see, yield gives us the ability to call a block even if our method doesn’t explicitly take a block. (Side note: any Ruby method can be passed a block, even if the method doesn’t explicitly take one.)

The fact that yield exists raises the question: why not just use yield all the time?

The answer is that when you use block.call, you have the ability to pass the block to another method if you so choose, which is something you can’t do with yield.

When we put &block in a method’s signature, we can do more with the block than just call it using block.call. We could also, for example, choose not to call the block but rather pass the block to a different method which then calls the block.

Takeaways

  • There are two common ways to call a Ruby block: block.call and yield.
  • Unlike block.call, yield gives us the ability to call a block even if our method doesn’t explicitly take a block.
  • Unlike using an implicit block and yield, using and explicit block allows us to pass a block to another method.

What the ampersand in front of &block means

Here’s a code sample that I’ve grabbed more or less at random from the Rails codebase.

def form_for(record, options = {}, &block)

The first two arguments, record and options = {}, are straightforward to someone who’s familiar with Ruby. But the third argument, &block, is a little more mysterious. Why the leading ampersand?

This post will be the answer to that question. In order to begin to understand what the leading ampersand is all about, let’s talk about how blocks relate to Proc objects.

Blocks and Proc objects

Let’s talk about blocks and Proc objects a little bit, starting with Proc objects.

Here’s a method which takes an argument. The method doesn’t care of what type the argument is. All the method does is output the argument’s class.

After we define the method, we call the method and pass it a Proc object. (If you’re not too familiar with Proc objects, you may want to check out my other post, Understanding Ruby Proc objects.)

def proc_me(my_proc)
  puts my_proc.class
end

proc_me(Proc.new { puts "hi" })

If you run this code, the output will be:

Proc

Not too surprising. We’re passing a Proc object as an argument to the proc_me method. Naturally, it thinks that my_proc is a Proc object.

Now let’s add another method, block_me, which accepts a block.

def proc_me(my_proc)
  puts my_proc.class
end

def block_me(&my_block)
  puts my_block.class
end

proc_me(Proc.new { puts "hi" })
block_me { puts "hi" }

If we run this code the output will be:

Proc
Proc

Even though we’re passing a Proc object the first time and a block the second time, we see Proc for both lines.

The reason that the result of my_block.class is Proc is because a leading ampersand converts a block to a Proc object.

Before moving on I encourage you to try out the above code in a console. Poke around at the code and change some things to see if it enhances your understanding of what’s happening.

Converting the Proc object to a block before passing the Proc object

Here’s a slightly altered version of the above example. Notice how my_proc has changed to &my_proc. The other change is that Proc.new has changed to &Proc.new.

def proc_me(&my_proc) # an & was added here
  puts my_proc.class
end

def block_me(&my_block)
  puts my_block.class
end

proc_me(&Proc.new { puts "hi" }) # an & was added here
block_me { puts "hi" }

If we run this code the output is the exact same.

Proc
Proc

This is because not only does a leading ampersand convert a block to a Proc object, but a leading ampersand also converts a Proc object to a block.

When we do &Proc.new, the leading ampersand converts the Proc object to a block. Then the leading ampersand in def proc_me(&my_proc) converts the block back to a Proc object.

I again encourage you to run this code example for yourself in order to more clearly understand what’s happening.

The differences between blocks and Proc objects

Ruby has a class called Proc but no class called Block. Because there’s no class called Block, nothing can be an instance of a Block. The material that Ruby blocks are made out of is Proc objects.

What happens when we try this?

my_block = { puts "hi" }

If we try to run this, we get:

$ ruby block.rb
block.rb:1: syntax error, unexpected string literal, expecting `do' or '{' or '('
my_block = { puts "hi" }
block.rb:1: syntax error, unexpected '}', expecting end-of-input
my_block = { puts "hi" }

That’s because the syntax { puts "hi" } doesn’t make any syntactical sense on its own. If we want to say { puts "hi" }, there are only two ways we can do it.

First way: put it inside a Proc object

That would look like this:

Proc.new { puts "hi" }

In this way the { puts "hi" } behavior is “packaged up” into an entity that we can then do whatever we want with. (Again, see my other post on Ruby proc objects for more details.)

Second way: use it to call a method that takes a block

That would look like this:

some_method { puts "hi" }

Why converting a block to a Proc object is necessary

Let’s take another look at our code sample from the beginning of the post.

def form_for(record, options = {}, &block)

In methods that take a block, the syntax is pretty much always &block, never just block. And as we’ve discussed, the leading ampersand converts the block into a Proc object. But why does the block get converted to a Proc object?

Since everything in Ruby is an instance of some object, and since there’s no such thing as a Ruby Block class, there can never be an object that’s a block. In order to be able to have an instance of something that represents the behavior of a block, that thing has to take the form of a Proc object, i.e. an instance of the class Proc, the stuff that Ruby blocks are made out of. That’s why methods that explicitly deal with blocks convert those blocks to Proc objects first.

Takeaways

  • A leading ampersand converts a block to a Proc object and a Proc object to a block.
  • There’s no such thing as a Ruby Block class. Therefore no object can be an instance of a block. The material that Ruby blocks are made out of is Proc objects.
  • The previous points taken together are why Ruby block arguments always appear as e.g. &block. The block can’t be captured in a variable unless it’s first converted to a Proc object.

Understanding Ruby Proc objects

What we’re going to do and why

If you’re a Ruby programmer, you almost certainly use Proc objects all the time, although you might not always be consciously aware of it. Blocks, which are ubiquitous in Ruby, and lambdas, which are used for things like Rails scopes, both involve Proc objects.

In this post we’re going to take a close look at Proc objects. First we’ll do a Proc object “hello world” to see what we’re dealing with. Then we’ll unpack the definition of Proc objects that the official Ruby docs give us. Lastly we’ll see how Proc objects relate to other concepts like blocks and lambdas.

A Proc object “hello world”

Before we talk about what Proc objects are and how they’re used, let’s take a look at a Proc object and mess around with it a little bit, just to see what one looks like.

The official Ruby docs provide a pretty good Proc object “hello world” example:

square = Proc.new { |x| x**2 }

We can see how this Proc object works by opening up an irb console and defining the Proc object there.

> square = Proc.new { |x| x**2 }
 => #<Proc:0x00000001333a8660 (irb):1> 
> square.call(3)
 => 9 
> square.call(4)
 => 16 
> square.call(5)
 => 25

We can kind of intuitively understand how this works. A Proc object behaves somewhat like a method: you define some behavior and then you can use that behavior repeatedly wherever you want.

Now that we have a loose intuitive understanding, let’s get a firmer grasp on what Proc objects are all about.

Understanding Proc objects more deeply

The official Ruby docs’ definition of Proc objects

According to the official Ruby docs on Procs objects, “a Proc object is an encapsulation of a block of code, which can be stored in a local variable, passed to a method or another Proc, and can be called.”

This definition is a bit of a mouthful. When I encounter wordy definitions like this, I like to separate them into chunks to make them easier to understand.

The Ruby Proc object definition, broken into chunks

A Proc object is:

  • an encapsulation of a block of code
  • which can be stored in a local variable
  • or passed to a method or another Proc
  • and can be called

Let’s take these things one-by-one.

A Proc object is an encapsulation of a block of code

What could it mean for something to be an encapsulation of a block of code? In general, when you “encapsulate” something, you metaphorically put it in a capsule. Things that are in capsules are isolated from whatever’s on the outside of the capsule. Encapsulating something also implies that it’s “packaged up”.

So when the docs say that a Proc object is “an encapsulation of a block of code”, they must mean that the code in a Proc object is packaged up and isolated from the code outside it.

A Proc object can be stored in a local variable

For this one let’s look at an example, straight from the docs:

square = Proc.new { |x| x**2 }

As we can see, this piece of code creates a Proc object and stores it in a local variable called square. So this part of the definition, that a Proc object can be stored in a local variable, seems easy enough to understand.

A Proc object can be passed to a method or another Proc

This one’s a two-parter so let’s take each part individually. First let’s focus on “A Proc object can be passed to another method”.

Here’s a method which can accept a Proc object. The method is followed by the definition of two Proc objects: square, which squares whatever number you give it, and double, which doubles whatever number you give it.

def perform_operation_on(number, operation)
  operation.call(number)
end

square = Proc.new { |x| x**2 }
double = Proc.new { |x| x * 2 }

puts perform_operation_on(5, square)
puts perform_operation_on(5, double)

If you were to run this code you would get the following output:

25
10

So that’s what it means to pass a Proc object into a method. Instead of passing data as a method argument like normal, you can pass behavior. Or, to put it another way, you can pass an encapsulation of a block of code. It’s then up to that method to execute that encapsulated block of code whenever and however it sees fit.

If we want to pass a Proc object into another Proc object, the code looks pretty similar to our other example above.

perform_operation_on = Proc.new do |number, operation|
  operation.call(number)
end

square = Proc.new { |x| x**2 }
double = Proc.new { |x| x * 2 }

puts perform_operation_on.call(5, square)
puts perform_operation_on.call(5, double)

The only difference between this example and the one above it is that, in this example, perform_operation_on is defined as a Proc object rather than a method. The ultimate behavior is exactly the same though.

A Proc object can be called

This last part of the definition of a Proc object, “a Proc object can be called”, is perhaps obvious at this point but let’s address it anyway for completeness’ sake.

A Proc object can be called using the #call method. Here’s an example.

square = Proc.new { |x| x**2 }
puts square.call(3)

There are other ways to call a Proc object but they’re not important for understanding Proc objects conceptually.

Closures

In order to fully understand Proc objects, we need to understand something called closures. The concept of a closure is a broader concept that’s not unique to Ruby.

Closures are too nuanced a concept to be included in the scope of this article, unfortunately. If you’d like to understand closures, I’d suggest checking out my other post, Understanding Ruby closures.

But the TL;DR version is that a closure is a record which stores a function plus (potentially) some variables.

Proc objects and blocks

Every block in Ruby is a Proc object, loosely speaking. Here’s a custom method that accepts a block as an argument.

def my_method(&block)
  puts block.class
end

my_method { "hello" }

If you were to run the above code, the output would be:

Proc

That’s because the block we passed when calling my_method is a Proc object.

Below is an example that’s functionally equivalent to the above. The & in front of my_proc converts the Proc object into a block.

def my_method(&block)
  puts block.class
end

my_proc = Proc.new { "hello" }
my_method &my_proc

By the way, if you’re curious about the & at the beginning of &block and &my_proc, I have a whole post about that here.

Proc objects and lambdas

Lambdas are also Proc objects. This can be proven by running the following in an irb console:

> my_lambda = lambda { |x| x**2 }
 => #<Proc:0x00000001241e82a8 (irb):1 (lambda)> 
> my_lambda.class
 => Proc

The difference between lambdas and Proc objects is that the two have certain subtle differences in behavior. For example, in lambdas, return means “exit from this lambda”. In regular Proc objects, return means “exit from embracing method”. I won’t go into detail on the differences between lambdas and Proc objects because it’s outside the scope of what I’m trying to convey in this post. A have a different post that describes the differences between procs and lambdas.

Takeaways

  • A Proc object is an encapsulation of a block of code, which can be stored in a local variable, passed to a method or another Proc, and can be called.
  • A closure is a record which stores a function plus some variables. Proc objects are closures.
  • Blocks are Proc objects.
  • Lambdas are Proc objects too, although a special kind of Proc object with subtly different behavior.

Hangman Challenge refactor (November 2021)

https://youtu.be/xDL5GrYnN_g

This is the first in what I intend to be a series of refactoring readers’ coding submissions.

To make your own Hangman Challenge submission, you can go here and follow the instructions.

“Before” version

What was good about the code

I found the class name Word to be good. It was very obvious to me what idea Word represented.

Most of the code was reasonably easy to understand. Aside from one certain line, there were no places where I was at a total loss as to what was going on.

What could be improved

Before I list what could be improved I want to thank the person who bravely submitted his code for public critique.

One of the first things that drew my attention was the Hangman#guess method. This method was pretty long and contained deep nesting.

The name of the Hangman class also lacks meaning. Word is a good abstraction because it represents a crisp idea: the word that the player is trying to guess. But there’s no such thing as a Hangman, at least not in this context, and so an opportunity for a crisp abstraction is lost.

There are some naming issues. The variables secret, letters, guesses and life aren’t as clear as they could be.

There are a couple places where there’s a lack of obvious meaning. For example, what exactly does it mean when life.zero? is true? It can be inferred that this means the player loses, but it would be better if we were to make this blatantly obvious.

There are a couple YAGNI (you ain’t gonna need it) violations. Both the Word constructor and the Hangman constructor have a superfluous feature which allows you to pass in an arbitrary masked character value and starting life value, respectively. These features aren’t needed in order to meet the requirements of the program. These features were added, presumably, “just in case”.

There are a few cases of needless attr_readers. It’s my view that attr_reader should only be used if an instance variable needs to be surfaced to a class’s public API. Otherwise the public API is being made bigger than necessary, limiting how much of the class can be refactored without risk.

This program contains a type of Ruby syntax I’ve never seen before:

secret.each_char.with_index { |char, i| @letters[i], guessed = char, true if char == letter }

Perhaps there are some cases where this type of syntax works well, but in this particular case I found it confounding.

Lastly, there’s was a bit of duplication, one “magic number”, and what you might call a “magic character”.

Here’s the original code with comments added by me.

#!/usr/bin/ruby
#
# https://github.com/jasonswett/hangman_challenge
#

# frozen_string_literal: true

class Word # good abstraction
  attr_reader :secret # needless attr_reader

  def initialize(secret, char = "_") # YAGNI violation
    @secret = secret # unclear variable name
    @letters = Array.new(secret.length) { char } # unclear variable name
  end

  def guess(letter)
    guessed = false

    # confusing syntax
    secret.each_char.with_index { |char, i| @letters[i], guessed = char, true if char == letter }

    guessed
  end

  def completed? # unclear method name
    secret == mask
  end

  def mask
    @letters.join
  end
end

class Hangman # "Hangman" isn't really an abstraction
  attr_reader :word, :guesses, :life # needless attr_readers

  def initialize(secret, life = 6) # YAGNI violation
    @word = Word.new(secret)
    @guesses = "" # unclear variable name
    @life = life # unclear variable name

    puts "#{word.mask} life left: #{life}"
  end

  def guess(letter) # long method with deep nesting
    if word.guess(letter)
      if word.completed?
        puts "#{word.secret} YOU WIN!"
      else
        if guesses.empty?
          puts "#{word.mask} life left: #{life}" # duplication
        else
          puts "#{word.mask} life left: #{life} incorrect guesses: #{guesses}"
        end
      end
    else
      @life -= 1

      if life.zero? # lack of obvious meaning
        puts "#{word.mask} YOU LOSE!"
      else
        @guesses += letter
        puts "#{word.mask} life left: #{life} incorrect guesses: #{guesses}"
      end
    end
  end
end

# hangman = Hangman.new("apple")
# %w[a b q z e p l].each do |letter|
#   hangman.guess(letter)
# end

# hangman = Hangman.new("quixotic")
# %w[a e i o u l g p r].each do |letter|
#   hangman.guess(letter)
# end

File.readlines(ARGV[0], chomp: true).each do |line|
  next if line.empty?

  if line.length == 1
    @hangman.guess(line)
  else
    @hangman = Hangman.new(line)
  end
end

“After” version

Here’s my version. I’m not saying it’s perfect, just an improvement. I invite you to see if you can identify the specific ways in which the code was improved.

#!/usr/bin/ruby
#
# https://github.com/jasonswett/hangman_challenge
#

# frozen_string_literal: true

class Game
  STARTING_LIFE_AMOUNT = 6

  def initialize(word)
    @word = word
    @remaining_life_amount = STARTING_LIFE_AMOUNT
    puts word_status
  end

  def submit_guess(guessed_letter)
    if @word.guess_correct?(guessed_letter)
      @word.correct_guesses << guessed_letter
    else
      @remaining_life_amount -= 1
      @word.incorrect_guesses << guessed_letter
    end

    if player_has_won?
      puts "#{@word.value} YOU WIN!"
      return
    end

    if player_has_lost?
      puts "#{@word} YOU LOSE!"
      return
    end

    puts complete_status
  end

  def player_has_won?
    @word.value == @word.to_s
  end

  def player_has_lost?
    @remaining_life_amount.zero?
  end

  def word_status
    "#{@word} life left: #{@remaining_life_amount}"
  end

  def complete_status
    [word_status, @word.incorrect_guess_message]
      .compact
      .join(" ")
  end
end

class Word
  attr_accessor :value, :correct_guesses, :incorrect_guesses

  def initialize(value)
    @value = value
    @correct_guesses = []
    @incorrect_guesses = []
  end

  def guess_correct?(guessed_letter)
    @value.include?(guessed_letter)
  end

  def to_s
    @value.split("").map do |letter|
      if @correct_guesses.include?(letter)
        letter
      else
        "_"
      end
    end.join
  end

  def incorrect_guess_message
    if incorrect_guesses.any?
      "incorrect guesses: #{incorrect_guesses.join("")}"
    end
  end
end

File.readlines(ARGV[0], chomp: true).each do |line|
  next if line.empty?

  if line.length > 1
    @game = Game.new(Word.new(line))
  else
    @game.submit_guess(line)
  end
end

Conclusion

Here are the issues with the original code that were fixed (or at least improved) in the refactored version:

  • Long method
  • Deep nesting
  • Unclear variable names
  • Lack of obvious meaning
  • YAGNI violations
  • Magic numbers
  • Needless attr_readers
  • Esoteric syntax
  • Duplication

Again, if you’d like to make your own Hangman Challenge submission, start here.