No doubt Rails is an amazing system. But like anything complicated it has its quirks. I ran across one of these lately as I was setting up validations on a model for a website I’m building. After spending some time googling to understand what was happening, I found an unsatisfying workaround and an invitation for suggestions to improve Rails by changing this strange behavior. However, to consider any improvement, I believe we need to examine how Ruby treats nil the same as false.
To illustrate my problem, let’s start with a migration:
This is a relatively simple model, holding the name of a customer and a boolean flag showing whether the customer is active. I want to ensure that every record has a value for both fields and will enforce this in the database itself.
After running the migration and firing up the rails console, I then define a class to go with the model.
Here I’ve made both the name and active fields accessible, and added validators as an additional layer of protection to make sure these fields are filled in.
Returning to the console, I create a new record:
As expected, this is a valid record. However, if I make an inactive customer, I get a surprise:
I expected that had I given active a nil value, it would have failed the validation. I did not expect giving active a false value would make it fail validation. In my mind false is a present value. It confirms that information I want for this customer exists. Were the value nil, I would wonder if information were missing. I would not want to assume the value was one way or the other if it were not there.
What then is Rails telling me?
Drawing a Blank
Happily, when an object fails validation, it tells you why:
Here’s the first clue. Rather than the error message describing the value as not present, it says that the value is blank. Rails uses its #blank? method on fields of a record to determine whether they are present. In fact, Rails' #present? method simply returns !#blank?.
For fields such as strings or arrays, this makes sense. If I want to ensure information is in a name field, I don’t want that field to be blank.
Why then does Rails think false is blank?
Let’s look at Rails' source code for this method:
If I call #blank? on an object that has an #empty? method, e.g. a String or an Array, #blank? returns the result of calling #empty? on that object. This is why an empty string is blank. If the object does not respond to #empty?, #blank? returns the result of applying the negation operator to the object.
Now it’s at least clearer why false.blank? returns true. It’s just returning !false. What then does it mean to apply the negation operator to other kinds of objects that don't respond to #empty?, such as numbers?
Negating Objects and Ruby Source
It turns out that in Ruby, ! is not an operator. It’s a method on BasicObject. Like all methods, it has source code:
(For those expecting Ruby syntax, this is the C source code which implements Ruby.) Now we get to ask about RTEST. This is a macro:
The macro uses the bitwise and operator to apply the complement of Qnil to the object being tested. If the result is nonzero, RTEST returns true. Otherwise it returns false. What then is Qnil?
RUBYQnil is defined as an integer. As with all integers, using bitwise _and to apply Qnil to the complement of Qnil produces zero. What else would cause RTEST to return false?
RUBYQfalse is also an integer, and always given the value zero. Using bitwise _and to apply zero to anything produces zero. Here then, within the bowels of Ruby’s source code, we find that false and nil are presumed to be exactly the same.
Aside from rewriting Ruby or Rails, one solution to my problem is not to validate presence in the model and instead validate whether the field has one of the values I want:
This makes sure that active can only have one of two values; nil can’t sneak in. Unlike RTEST, it distinguishes between nil and false as values. While this produces the behavior I want, it still seems clunky.
Can We Do Better?
Even though I understand what’s happening, I don’t like it. There has already been at least one debate about whether to retain this behavior. So far the conclusion seems to be to leave things as they are.
Were I to change something, the first place I would consider is Rails' #blank? method. It seems right to consider an empty string or array as blank. When I use containers like this I expect them to contain something. It also makes sense to consider that when any object (other than nil) is present, then the field is not blank. I usually don’t care about the value of an object. I just want to make sure it’s there.
I want #blank? to distinguish between false and nil. I want it to look like this:
The risk of changing #blank? is that its current behavior is probably so ingrained in how experienced Rails programmers think, that this change would cause even more confusion. Then again, given the number of questions, mine included, that have come up because of this behavior, we might want to consider changing how to handle this.
I understand now that there are places in Ruby's core where nil and false are treated the same. But up here in the real world, this means we have to remember such little tidbits and make our own adjustments when we want to treat these values differently.
Ironically, because in this case Ruby doesn’t treat false and nil differently, we have to treat strings and booleans differently. Does it have to be this way?