If-expressions

If-expressions

In this unit, we'll learn how to make programs change their behavior based on logic. The if-expression is a fundamental construct in Scala programs. If you have any issues understanding the material in this unit, please ask questions in the discord channel I've set up for teaching Scala, or you can discuss on scala-users.

Please remember to run the provided examples via the Scala console unless told otherwise. If you've forgotten how to launch and/or shut down the Scala console, please refer here.

If expressions

If expressions allow you to modify how your program runs based on whether something is true or false. For example, let's imagine you're programming a car. You want to control the speed of the car based on whether or not it's at a red light. An if expression would let you set the speed based on the what color the light is...

scala> val speedLimit = 60
val speedLimit: Int = 60

scala> def calculateSpeed(light: String) = if light == "red" then 0 else speedLimit
def calculateSpeed(light: String): Int

scala> calculateSpeed("red")
val res0: Int = 0

scala> calculateSpeed("green")
val res1: Int = 60

As you can see, calculateSpeed is giving back 0 if you give it "red", and will give back 60 if you give it something else.

Let’s dissect if expressions a bit more to understand them better…

if <boolean-expression> then <expression> else <expression>

An if-expression starts with the if keyword, then a space, then a boolean expression. A boolean expression is any expression that has a boolean result. true is a boolean expression, as is 4<2. We'll call this boolean expression the "first condition". Following the then keyword, you have an expression of any type, which we'll call the "first path", followed by the else keyword and an expression of any type which we'll call the "default path".

The result of an if-expression is the data from the first path if the first condition is true. If it's not true, the result is the data from the default path.

Let’s experiment with a couple of simple examples:

scala> def simple(firstCondition: Boolean) = if firstCondition then 1 else 0
scala> val a = simple(true)
val a: Int = 1

scala> val b = simple(false)
val b: Int = 0

scala> val c = simple(5 == 5)
val c: Int = 1

scala> val d = simple(5 != 5)
val d: Int = 0

1 is the true path and 0 is the default path. When the condition is equal to true, we get 1, otherwise we get 0.

Another thing to note is that our if-expression can be split up to be multiline, much like other expressions:

scala> def simple(firstCondition: Boolean) =
  if firstCondition then
    1
  else 
    0

This makes the expression much easier to read and comprehend. Finally, you should keep in mind that when I write that if-expression uses expressions for the first condition, first path, and default path, that includes code blocks.

scala> if 
  val a = 4
  a == 4
then 
  println("wow a code block")
  true
else 
  println("I'm a code block too")
  false

wow a code block
val res0: Boolean = true

In fact, whenever I say something needs an expression of any type, you should automatically assume that you can use a code block, because a code block is an expression.

Circling back to our stop light example, the speed was set to 0 if light == “red”, otherwise it’s set to the speed limit. But what do we mean by otherwise?

scala> calculateSpeed("yellow")
val res0: Int = 60

scala> calculateSpeed("Red")
val res0: Int = 0

As you can see, our calculateSpeed method is a bit basic. It only checks if the input is equal to "red", and doesn't do anything special if the light is "yellow". Let's make it a bit more robust.

First, we want to make sure that it will recognize the text "red" no matter how it's capitalized. We don't want it to care about if the input is "Red" or "RED" or even "rEd", it should recognize all of those as "red". To do this, we merely remove capitalization from our input!

scala> def calculateSpeed(light: String) = 
  if light.toLowerCase == "red" then 
    0 
  else 
    speedLimit
def calculateSpeed(light: String): Int

Now our method is a bit more robust. However, we probably want to also handle the situation where the light is yellow. How would we do this?

else if

else if is the answer in this case. It lets us specify a second true path, with a second conditional:

scala> def calculateSpeed(light: String) = 
  if light.toLowercase == "red" then
    0
  else if light.toLowercase == "yellow" then
    speedLimit/2 
  else 
    speedLimit
def calculateSpeed(light: String): Int

You can see that else if behaves a lot like the normal if, even having a then after the conditional. You can include as many else if ... then .... sections as you'd like in your if expression. They will be checked in order, with the if ... then ... section checked first, and the else ... checked last. Since they are checked in order, you should always have your most specific logic at top, and least specific logic at bottom. A common error in if expressions is having logic that's not very specific at the top, and having that path activate instead of a more.

Specificity

What is specificity in logic? Let's imagine I have a String named str. str.isEmpty is less specific than str.startsWith("hel") && str.size == 6. Specificity in logic relates to how much information is in the test. An example of completely non-specific logic is true or false, because these expressions don't test anything at all. str.isEmpty is roughly equivalent to str.size == 0, and so it's more specific because it's testing one piece of data. str.startsWith("hel") && str.size == 6 is even more specific because it tests two pieces of data.

There's no fundamental measure of specificity in a logical expression, it's a subjective measure. That being said, as you program you should get more and more of a feel for varying degrees of specificity when looking at logical expressions.

Types and if-expressions

The type of an if-expression is the common type between its result expressions. Take the following for example:

scala> if true then 0 else 1
val res0: Int = 0

The type of this if expression is Int not because true path was chosen, but because the true path and false path are both Int expressions. What if we change the false path to a String expression?

scala> if true then 0 else "hello"
val res0: Matchable = 0

The type of this if-expression is Matchable. The reason for this is because Int and String are not the same, so the if-expression has the type of their nearest common parent. This chart shows the parent-child relationships between the types we've discussed so far, and some we have not:

image.png

The arrows point from parent to child. As you can see, the first type both String and Int can trace heritage back to is Matchable, so the if-expression becomes this type. The Matchable type is kind of complex to explain, so we'll forgo that for now. However, you should realize that if you if-expression's type is Matchable, then you probably have not defined your if-expression well. If you want to guard against an error like this, you can specify the type of what you're assigning the if-expression to. If you make a mistake in defining the if-expression, this will help you catch it.

scala> val a: Int = if true then
  0
else 
  "hello"

-- [E007] Type Mismatch Error: -------------------------------------------------
4 |  "hello"
  |  ^^^^^^^
  |  Found:    ("hello" : String)
  |  Required: Int

Evaluation of expressions in an if-expression

I've been calling the expressions that are chosen in an if expression based on the boolean expression "paths" for a reason. Code will be executed only based on the path chosen. The other expressions in the if-expression will not be evaluated. Lets see this in practice:

scala> if true then 
  println("hello") 
else 
  println("world")

hello

If all the expressions that made up this if-expression were evaluated, "hello" and "world" would've both been printed. This evaluation of expressions applies to the boolean expressions too:

scala> if true then 
  println("hello") 
else if println("world").hashCode == 1 then 
  println("encore") 
else 
  println("last path")

hello

scala>if false then 
  println("hello") 
else if println("world").hashCode == 0 then 
  println("encore")
else 
  println("last path")

world
last path

In the first line, since the else-if case println("world").hashCode == 0 was not reached, "world" was not printed. If it's reached however, like in the second line, "world" gets printed even if the else-if case is not chosen for execution.

If-expressions without else

So far, all our if-expressions have ended with else <expression>. However, this part of an if-expression can be dropped. If you do so, the type of your if-expression will become Unit and you will not get any data back from it. This has some uses, like if you want to print some text if something is true but do nothing otherwise. You can have else-if cases too if you want, and the same rule applies if you do not have a final else case at the end. However, if you want to get data back from your if-expression, it must always have a final else case.

Practice

Please try to answer the following questions on your own:

  1. Does an if-expression evaluate all the expressions that it's composed of?
  2. Write a method that takes an Int parameter called age. If age is less than 18, the method should print "young", if age is greater than or equal to 18 and less than 50, it should print "adult", if age is greater than or equal to 50, it should print "old".
  3. What is the type of the following if-expression?
    if true then 3.9 else 4.3
    
  4. What is the type of the following if-expression?
    if true then true else 4
    
  5. How many else-if clauses can an if-expression have?
  6. What happens if your if-expression has no else-clause?
  7. Write a function called bmiGroup based on the bmi function from last unit. It should take weight and height as input and output a String based on the bmi for the person. Use the following chart to determine the outputs.
bmigroup
<18.5underweight
18.5-24.9normal
25-29.9overweight
>29.9obese

Answers

Please try to solve the problems in the practice section on your own before referring to this section.

  1. Does an if-expression evaluate all the expressions that it's composed of? No, only the expressions it reaches. This means that if the first conditional is true, then the first path and the first conditional are the only expressions evaluated in the if-expression
  2. Write a method that takes an Int parameter called age. If age is less than 18, the method should print "young", if age is greater than or equal to 18 and less than 50, it should print "adult", if age is greater than or equal to 50, it should print "old".
    def ager(age: Int) = 
     if age < 18 then 
       println("young")
     else if age >= 18 && age < 50 then 
       println("adult")
     else 
       println("old")
    
  3. What is the type of the following if-expression?
    if true then 3.9 else 4.3
    
    Double
  4. What is the type of the following if-expression?
    if true then true else 4
    
    AnyVal. If you answered Matchable, you were on the right track though.
  5. How many else-if clauses can an if-expression have? Potentially unlimited. In theory, the hard limit should be determined by your stack size.
  6. What happens if your if-expression has no else-clause? Its type becomes Unit
  7. Write a function called bmiGroup based on the bmi function from last unit. It should take weight and height as input and output a String based on the bmi for the person. Use the following chart to determine the outputs.
    def bmiGroup(kg: Double, meters: Double) = 
     val bmi = kg / (meters * meters)
     if bmi < 18.5 then
       "underweight"
     else if bmi >= 18.5 && bmi <= 24.9 then
       "normal"
     else if bmi >= 25.0 && bmi <= 29.9 then
       "overweight"
     else 
       "obese"
    

Did you find this article valuable?

Support Mark Hammons' Blog by becoming a sponsor. Any amount is appreciated!