Foundations - Scala Fundamentals Part 3
Expressions, Statements, and Code blocks
Table of contents
In this lesson, we'll analyze and discuss some of the intricacies of Scala syntax and its rules. This won't teach you to do much that you can't already do from the last two lessons, but it will help you strengthen your understanding of what you can and cannot do in Scala.
When reading through this lesson, please remember to try to run the example code in the Scala console. If you've forgotten how to start and/or stop the Scala console, please read these instructions.
Learning Outcomes
By the end of this lesson, you should be able to do the following:
- Understand scope
- Understand the difference between expressions and statements
- Understand shadowing
- Understand code blocks
Expressions
Expressions are things that give back data, and they can always be treated like data themselves. There are two forms of expressions, simple expressions and complex expressions. Most things you write in Scala will be expressions, meaning they can be combined and passed around like data.
Simple expressions
You are already well acquainted with simple expressions. You’ve been using them since the start of the course. 5
is a simple expression. ”Hello, World!”
is a simple expression, true
and false
are simple expressions. If you define a value named “a”, then a
is a simple expression. Simple expressions are usually pieces of data, or labels for data (like values).
Complex expressions
Complex expressions are expressions that are formed of one or more other expressions. You’ve been using these as well while programming. 2 + 7
is a complex expression, as is 5 * -(“hello world”.size + 1)
. When viewing these expressions, the scala compiler treats them like a single expression, even though they are made of many.
Multi-line complex expressions
As an expression gets more and more complex, it get larger and larger, and harder and harder to read. For this reason, it can be very useful to spread a complex expression over multiple lines. If you want to spread a complex expression over one or more lines, leave a trailing operator or function call on one line, and the rest of the expression on another.
Try me with Scala-CLI run
This example does not work with the Scala console due to the way it operates. It only works well with a real Scala program, so lets make one to test it.
Write in a file called "multiline.scala" the following text:
val x = 5 +
6 +
7 +
8 +
9
@main def program = println(x)
When you run the program with scala-cli run multiline.scala
, you should get the result 35
back. This isn't necessary to run your programs in Scala, but it can make your code more readable in some cases, so keep it in mind.
The rule of one
When defining a value, you can only put one expression on the right hand side of the assignment operator val a = 5 7
will not work. val a = 5+7
works because it’s a single complex expression formed of simple expressions. However val a = 1+2 2+3
would not. When defining a value, you must always give one expression.
The same applies to parameters of methods, and a lot of other things. In general, you can only have one expression per line of Scala code. Method invocation acts as a complex expression though, which is why 5+7
or 5.+(7)
are considered single expressions.
Practice
Try to do these practice questions to cement your comprehension of expressions.
If you don't know the answer to question four, try putting the code into a scala file and compile it with scala-cli compile yourfilename.scala
.
- Is
3
a simple or complex expression - Can you define a value with an expression?
- Is
"hello, world".size"
a complex expression?
Is this valid Scala code?val a = "hello" .size
- How many expressions can you have per line in Scala?
- Write a complex expression of your choosing.
Statements
I mentioned earlier that most things in Scala give back data. That is, most things in Scala are expressions, simple or complex. Most is not the same as all though; there are some things that don't give back data, and cannot be used in place of data. We call these things statements.
An example of a statement that you're already very familiar with is value definition: val a = 5
is a statement, but a
is an expression afterwards. What's the difference?
scala> val a = 5 + val b = 6
-- Error: ----------------------------------------------------------------------
1 |val a = 5 + val b = 6
| ^^^
| end of statement expected but 'val' found
As you can see, val b = 6
is not treated like data, nor does it give back data. That's why the output of your console is different when you write val a = 6
as opposed to a
:
scala> val a = 6
val a: Int = 6
scala> a
val res0: Int = 6
The res
output in your console means that the line you entered was an expression, and gave back data. The console helpfully puts that data in a value automatically for you (for example, in the above, it puts the expression a
into res0
for you) so that you can use it again later.
How do you tell the difference? Typically statements are definitions and declarations in Scala. You’ve learned one kind of definition, value definition, so far. There are quite a few kinds of definitions in Scala however. Whenever you see me referring to something as defining something, you should make a note that the code in question is almost certainly a statement.
Practice
Try to answer these questions to cement your knowledge of statements.
- Is
println("hello")
a statement? - Is
val imNotAStatement = 3
a statment? - Can you define a value with a statement?
- What do statements typically do in Scala?
The answers can be found at the bottom of the blog post
Blocks of code
A block of code is multiple lines of code grouped together to form a complex expression. Code blocks can be composed of multiple statements or expressions. They can be formed in one of two ways:
A newline with indented code:
scala> val x =
10 + 12
4 + 6
val x: Int = 10
Code surrounded by {}
:
scala> val x = {
10 + 12
4 + 6
}
val x: Int = 10
The first approach is more akin to inferred types. The block is denoted by indentation and putting a newline instead of an expression after the assignment operator. The block is understood to have ended when you enter a non-indented line into the console.
The second is more explicit. The block of code is more readily visible thanks to the {}
. The first option is more and more preferred nowadays, but the second is nice when you need to explicitly indicate that you're giving Scala a block of code.
Now, you'll notice that we have two expressions in our block of code, but x
is assigned the result of the expression on the last line of the block. This is a fundamental property of code blocks; they only return the result of the last expression on the last line of the block.
So what is the use of a block of code? You can put statements inside code blocks too!
scala> val bmi =
val kg = 100.0
val meters = 2.0
kg / (meters*meters)
val bmi: Double = 25.0
This lets you break up complex expressions into simpler ones, assigning names to pieces of work you wanted to do. This can make your code easier to read, both for you and your collaborators.
Try me with Scala-CLI run
Indentation based code blocks are a bit hard to comprehend in the Scala console, because of the console's focus on single lines. It's much easier to see how they work with a normal scala program. Create a file named "indented-blocks.scala" and enter this text into it.
val bmi =
val meters = 2.0
val kg = 100.0
kg / (meters * meters)
val x =
val a = 5
val b =
val c = 10
c + 7
a + b
@main def program =
println(s"bmi is $bmi")
println(s"x is $x")
Be careful of the indentation. It doesn't matter what indentation you use, 1 space, 2 space, 4 space, tab, etc. Just make sure it's consistent or the compiler will get upset.
You should get the following as a result:
bmi is 25.0
x is 22
Did you notice how bmi is defined? There's newlines between the definition of meters, kg, and the expression that calculates the bmi. This is fine, because the block of code is understood to exist until the next non-indented line of code. What about the definition of x? Did you notice that the definition of b
inside is also using a code block? You can have code blocks inside of code blocks (nesting) by just doing the same thing to create a new codeblock (newline instead of an expression, indent the code in the block further to the right).
Scope
scala> val a =
val b = 4
b + 2
val a: Int = 6
In the above code, we define the value b in the process of defining value a, and defining a
as b + 2
. Can we use b
like we would use a
now?
scala> println(b)
-- [E006] Not Found Error: -----------------------------------------------------
1 |println(b)
| ^
| Not found: b
The answer is no, and the reason for this answer is something called scope. When you are using a code block, you are creating a scope. Any definitions made within that scope cannot be used outside of it. A code block nested in another can still access definitions from the outer code block though.
Scopes may seem weird, but they are important for reducing namespace pollution. Scopes make it so that you do not need to come up with unique names for everything in your program.
Shadowing
Shadowing is the act of hiding a previous definition with a new one in an inner scope. Take for example the below code:
scala> val a = 4
val a: Int = 4
scala> val b =
val a = 5
a + 7
val a: Int = 12
The a
inside the code block is not a redefinition of the a
outside the codeblock, but rather a different a
. Since it has the same name, it hides the old a
while you're inside the codeblock.
Unit, the dataless type
When a code block ends with an expression, it forms a complex expression with the type and data that's produced when that ending expression is evaluated. If, however, the code block ends with a statement, the type of the codeblock is the Unit
type, and the data it produces is ()
, the sole inhabitant of the Unit
type. The Unit
type is frequently used to indicate the lack of result data from an expression, and since statements do not give back data, it makes a lot of sense for a code block that ends with a statement to give back ()
.
Practice
Try to answer the following questions about code blocks:
- Is a code block an expression or a statement?
- Define a value named "a" with a code block.
- How do you start a code block when defining a value?
- How many expressions and statements can be in a code block?
- What happens if a code block ends with a statement?
- What happens if a code block ends with an expression?
- What is the
Unit
type? How many pieces of data inhabits the type? - What happens if you define a value inside a code block?
- Can you define a value in a code block with a name that already exists outside of the code block? What about inside of the code block?
- Define a value named "b" with a manually denoted code block.
- Define a value named "c" with a code block that defines a value named "d". "d" should be defined as
5.0
and the result of the code block should bed / 2.0
.
Practice Answers
Before diving into this section, please read the practice problems above and try to solve them yourself. This will help you solidify your knowledge, even if you didn't answer every question correctly. The following answers are here to help you further cement your knowledge and double check your understanding of the material.
Expressions
- Is
3
a simple or complex expression It's a simple expression - Can you define a value with an expression? Yes, a value can be defined with an expression(simple or complex)
- Is
"hello, world".size"
a complex expression? Yes. It's composed of the simple expression"hello, world"
and the invocation of thesize
method.
Is this valid Scala code? Yes. This code does not work in the Scala console, but it's perfectly useable in a standard Scala application. This style is frequently used for complex expressions with many method invocations in a chain.val a = "hello" .size
- How many expressions can you have per line in Scala? You can have one expression per line in Scala. A complex expression composed of many sub-expression is still considered a single expression by the Scala compiler
- Write a complex expression of your choosing.
Remember that a complex expression is just expressions joined together via a method invocation or some other means. The expressions that are the basis of a complex expression can themselves be complex.7 + 5 * 6
Statements
- Is
println("hello")
a statement? No, it's an expression. - Is
val imNotAStatement = 3
a statment? Yes. It's also a liar. - Can you define a value with a statement?
No. A value definition is a statement, but a value cannot be defined with a statement.
val a = 4
is a value definition.a
is defined with4
. Trying to define a value with a statement would look likeval a = val b = 4
. - What do statements typically do in Scala?
Statements are typically used to define things. As of this unit, you know how to define both values and methods. The method definition
def identity(a: Int) = a
is a statement.
Code blocks
- Is a code block an expression or a statement? It's an expression.
- Define a value named "a" with a code block.
val a = val b = 3 b * b
- How do you start a code block when defining a value? You put a newline after the assignment operator
=
and then indent each line you want to be part of the codeblock by a number of spaces that you choose. - How many expressions and statements can be in a code block? As many as you want
- What happens if a code block ends with a statement? The code block has the type
Unit
and gives back()
- What happens if a code block ends with an expression? The code block has the same type as that expression, and gives back the value of evaluating that expression.
- What is the
Unit
type? How many pieces of data inhabits the type? TheUnit
type is a type that indicates that data can't be given back. Only one piece of data inhabits it;()
- What happens if you define a value inside a code block? You can use that value only inside the code block and any code blocks inside said code block.
- Can you define a value in a code block with a name that already exists outside of the code block? What about inside of the code block? You can define a value in a code block with a name that already exists outside of it. This is called shadowing. You cannot define a value with a name that's already been defined inside a code block.
- Define a value named "b" with a manually denoted codeblock.
val b = { println("this value's name is b") 4 }
- Define a value named "c" with a code block that defines a value named "d". "d" should be defined as
5.0
and the result of the code block should bed / 2.0
.val c = val d = 5.0 d / 2.0