In Progress
Unit 1, Lesson 21
In Progress

Bang Bang

Video transcript & code

When writing Ruby code we often take advantage of the "explicit conversion methods" provided by many built-in and standard library classes. For instance, we know that if we need a String object, we can send the #to_s method to pretty much any object and it will convert itself to a String.

42.to_s                         # => "42"
:foo.to_s                       # => "foo"
[1,2,3].to_s                    # => "[1, 2, 3]"

Likewise, both we can get an Array or Integer using the #to_a or #to_i methods, respectively.

(1..3).to_a                     # => [1, 2, 3]
"42".to_i                       # => 42

But what if we need to convert a value to a boolean? As we found in episode 93, Ruby doesn't actually have a boolean data type. Instead, it has TrueClass and FalseClass. Since there is no common Boolean class, there is no corresponding #to_b explicit conversion method.

Normally this isn't an issue, since in Ruby we don't actually need to convert values to true or false very often. In most cases we can just rely on the natural "truthiness" and "falsiness" of Ruby values. Just as a refresher, in Ruby that false and nil are considered "falsy", and all other values are considered "truthy". So for instance we don't need to convert a value to an explicit boolean type before using it in an if statement; we can just rely on its inherent truthiness or falsiness.

values = [true, false, nil, 0, 1, "false", "", 3.14159]

values.each do |value|
  if value
    puts "#{value.inspect} is truthy"
  else
    puts "#{value.inspect} is falsy"
  end
end
# >> true is truthy
# >> false is falsy
# >> nil is falsy
# >> 0 is truthy
# >> 1 is truthy
# >> "false" is truthy
# >> "" is truthy
# >> 3.14159 is truthy

Now, let's say we're writing a program to process orders at a coffee shop. We have a CoffeeOrder model which has attributes for creamer and sugar. For a black coffee, the creamer attribute will have a nil value. For a coffee with creamer, the creamer attribute contains a symbol indicating the type of creamer the customer desires.

CoffeeOrder = Struct.new(:creamer, :sugar) do
  # ...
end

black_coffee = CoffeeOrder.new(nil)
# => #<struct CoffeeOrder creamer=nil, sugar=nil>

skim_coffee = CoffeeOrder.new(:skim) 
# => #<struct CoffeeOrder creamer=:skim, sugar=nil>

For convenience, we also want a predicate method which simply indicates whether some kind of creamer is desired. After thinking about this for a moment, we realize that this method can simply delegate directly to the creamer attribute. Since nil is "falsy", and any symbol value is considered "truthy", the raw value of creamer works just fine as a yes-or-no result as far as Ruby is concerned.

CoffeeOrder = Struct.new(:creamer, :sugar) do
  def creamer?
    creamer
  end
end

black_coffee = CoffeeOrder.new(nil)
black_coffee.creamer?           # => nil

skim_coffee = CoffeeOrder.new(:skim) 
skim_coffee.creamer?            # => :skim

Now let's add a requirement. We also want to be able to serialize the order to JSON. And the JSON we produce needs to conform to a specific format expected by an external order fulfillment program.

We require the 'json' library, and add a #to_json method. The method constructs a Hash of keys and values. The first key we need to provide is add_creamer, which the other system expects to be a JSON boolean value specifiying whether or not the customer wants any creamer at all. We use our #creamer? predicate to for this value. The other key is the type of creamer; we fill this in with the #creamer attribute accessor.

Unfortunately there's a problem with this implementation. A moment ago we said that the #add_creamer field must be a JSON boolean. But when we convert a black CoffeeOrder to JSON, the field has a null value. And when we convert a skim milk order, the field contains the string "skim".

require 'json'
CoffeeOrder = Struct.new(:creamer, :sugar) do
  def creamer?
    creamer
  end

  def to_json
    {
      :add_creamer  => creamer?,
      :creamer_type => creamer,
    }.to_json
  end
end

black_coffee = CoffeeOrder.new(nil)
black_coffee.to_json            
# => "{\"add_creamer\":null,\"creamer_type\":null}"

skim_coffee = CoffeeOrder.new(:skim) 
skim_coffee.to_json             
# => "{\"add_creamer\":\"skim\",\"creamer_type\":\"skim\"}"

While nil and symbols for falsy and truthy are good enough for Ruby, when exporting data to other systems we need to be more careful. In this case, it would be nice if we had a #to_bool or #to_b conversion method on all objects. But no such method exists.

So how can we convert a value to its boolean equivalent? The answer lies in the logical negation operator, aka the exclamation point, aka the "bang" operator. If we apply this operator to any truthy value, we get false back=. If we apply it to any falsy value, we get true back. Once we have a true or a false, all we need to do is to negate the value once more in order to get a boolean counterpart for the original value. And thus, the idiomatic way to convert a value to an explicit true or false is with double negation, or "bang bang"

! :skim                         # => false
! nil                           # => true

!! :skim                        # => true
!! nil                          # => false

We can apply this to our CoffeeOrder #creamer? predicate method by prefixing the call to the #creamer accessor with a bang-bang. Now the generated JSON has the desired boolean flag value in it.

require 'json'
CoffeeOrder = Struct.new(:creamer, :sugar) do
  def creamer?
    !!creamer
  end

  def to_json
    {
      :add_creamer  => creamer?,
      :creamer_type => creamer,
    }.to_json
  end
end

black_coffee = CoffeeOrder.new(nil)
black_coffee.to_json            
# => "{\"add_creamer\":false,\"creamer_type\":null}"

skim_coffee = CoffeeOrder.new(:skim) 
skim_coffee.to_json             
# => "{\"add_creamer\":true,\"creamer_type\":\"skim\"}"

This has been a lot to say about a very small idiom, but it's a fairly common one, and its meaning can be less than obvious when you first come across it. Now you'll know that when you see a "bang bang" in Ruby code, it means that the writer wanted to convert a value to an explicit true or false. And if you ever need to do the same, you'll know how. Happy hacking!

Responses