The Ruby Workshop
上QQ阅读APP看书,第一时间看更新

The Ternary Operator

Ternary means composed of three parts. In Ruby (and also in other programming languages), there is a common programming idiom called the ternary operator, which allows a quick if/else conditional to be put on one line by breaking it into three parts:

user_input = 'password'

secret = 'password'

user_has_access = user_input == secret ? true : false

The three parts are the condition (user_input == secret), the statement if true (true), and the statement if false (false). The three parts are separated first by a ? and secondly by a : notation.

While Ruby doesn't always require syntax such as parentheses, the preceding statement may be a bit hard to read unless you are very familiar with how Ruby handles the order of operations. Here is a clearer way of writing the preceding code:

user_has_access = (user_input == secret) ? true : false

The ternary operator is great for quick one-liners. However, if lots of complex logic is starting to creep into any of the three parts, this may be considered a code smell and the code will need refactoring into simpler logic. You can either refactor it into a proper if/else statement or you can refactor the complex logic into separate variables that are then evaluated in the ternary condition.

Consider the following example:

user_input = 'secret'

password = 'secret'

def login_user

  puts "logging in user"

end

def show_access_denied

  puts "Password was incorrect, try again"

end

user_has_access = user_input == secret

user_has_access ? login_user : show_access_denied

By using clearly labeled method names and simplifying the ternary statement, we can clearly see that if the user has access, we will log them in; otherwise, we will ask them to re-input their password.

Exercise 3.03: Speed Determiner

Write a method that determines the speed that a self-driving car should adjust to, based on environmental conditions, as well as the traffic status and distance to a traffic light. The method should return the new speed. Perform the following steps:

  1. Open a new session on IRB.
  2. First, we write the logic in pseudocode:

    Green light: 

          Sunny, all conditions: speed_limit

          Rainy, distance >= 50ft: speed_limit

          Rainy, distance < 50ft: 90% speed_limit

    Yellow light:

          Sunny, distance >= 50ft: 80% speed_limit

          Sunny, distance < 50ft: 50% speed_limit

          Rainy, distance >= 50f: 80% speed_limit

          Rainy, distance < 50f: 25% speed limit

    Red light:

          Sunny, distance >= 50ft: 50% speed limit

          Sunny, distance <= 50ft: 0% speed limit

          Rainy, distance >= 50ft: 25% speed limit

          Rainy, distance <= 50ft: 0% speed limit

  3. Implement the logic in a method. Define the method as drive_decision and also the parameters we consider in the method.

    Note

    We are introducing raise here, which will fatally exit the program if the code is encountered. This is a basic way to make sure that if a parameter is not passed in correctly, we get notified about it.

    def drive_decision(traffic_signal, weather, distance_to_signal, speed_limit)

      if traffic_signal == :green

        if weather == :sunny

          speed_limit

        elsif distance_to_signal >= 50

          speed_limit

        else

          speed_limit * 0.9

        end

      elsif traffic_signal == :yellow

        if weather == :sunny && distance_to_signal >= 50

          speed_limit * 0.8

        elsif weather == :sunny && distance_to_signal < 50

          speed_limit * 0.5

        elsif weather == :rainy && distance_to_signal >= 50

          speed_limit * 0.8

        elsif weather == :rainy && distance_to_signal < 50

          speed_limit * 0.25

        else

          raise "Condition not handled"

        end

      else # red light

        if weather == :sunny && distance_to_signal >= 50

          speed_limit * 0.5

        elsif weather == :rainy && distance_to_signal >= 50

          speed_limit * 0.25

        else

          0 # all other conditions should stop the car

        end

      end

    end

    Note

    raise is a keyword that raises exceptions. Exceptions can be caught and handled – this topic is covered later in the book.

  4. Evaluate the method with different variables:

    drive_decision(:green, :sunny, 100, 50)

    drive_decision(:yellow, :rainy, 50, 25)

  5. Refactor the method using case/when and additional methods. First, we determine the method for drive_decision_when_green:

    def drive_decision_when_green(weather, distance_to_signal, speed_limit)

          case weather

          when :sunny

                speed_limit

          when :rainy

                if distance_to_signal >= 50

                      speed_limit

                else

                      speed_limit * 0.9

                end

          else

                raise "Not handled"

          end

    end

  6. Similarly, we define the method for drive_decision_when_yellow:

    def drive_decision_when_yellow(weather, distance_to_signal, speed_limit)

          case weather

          when :sunny

                if distance_to_signal >= 50

                      speed_limit * 0.8

                else

                      speed_limit * 0.5

                end

          when :rainy

                if distance_to_signal >= 50

                      speed_limit * 0.8

                else

                      speed_limit * 0.25

                end

          else

                raise "Not handled"

          end

    end

  7. We also define the method for drive_decision_when_red:

    def drive_decision_when_red(weather, distance_to_signal, speed_limit)

          if distance_to_signal >= 50

                case weather

                when :sunny

                      speed_limit * 0.5

                when :rainy

                      speed_limit * 0.25

                else

                      raise "Not handled"

                end

          else

                0

          end

    end

  8. Lastly, we define the drive_decision method depending on the three conditions defined before:

    def drive_decision(traffic_signal, weather, distance_to_signal, speed_limit)

          case traffic_signal

          when :green

                drive_decision_when_green(weather, distance_to_signal, speed_limit)

          when :yellow

    drive_decision_when_yellow(weather, distance_to_signal, speed_limit)

          else

                drive_decision_when_red(weather, distance_to_signal, speed_limit)

          end

    end

    Here's the expected output:

Figure 3.20: Output for driving decisions

We can see, in the preceding code, that we are repeating code in the case statement. We can refactor this code by creating a new method that checks for valid traffic, weather states and writing this as a one-liner at the top of a method definition like this:

def drive_decision(traffic_signal, weather, distance_to_signal, speed_limit)

  raise "Unhandled weather condition" unless valid_weather_condition?(weather)

  #...rest of the method implementation…

end

def valid_weather_condition?(weather)

  [:sunny, :rainy].include?(weather.to_sym)

end  

This line of code, which starts off a method implementation, is called a guard clause because it makes a quick decision about whether we should proceed with the method implementation. In other words, it guards the method implementation by making sure only valid parameters are passed in. Guard clauses are a great way to refactor complicated if/else conditionals by checking for these conditions at the top of a method implementation with a guard clause. Then, you can keep the rest of your method implementation clean of additional checks on the data.