Foundations - Scala Fundamentals Part 2

Foundations - Scala Fundamentals Part 2

Introduction

In this lesson, we'll learn about boolean data and the comparison and logical operators, as well as beginning to learn about data injection into strings, and more. Hang in there, because there's a lot more to learn about Scala!

Learning Outcomes

By the end of this lesson, you should be able to:

  • Describe what a comment is
  • Describe what a type is
  • Embed a value/variable/expression in a string
  • Understand what a method is
  • Name the three logical operators
  • Understand what the comparison operators are

Comments

Comments are parts of your program that are never executed or compiled. They have no effect on your program whatsoever, and only exist to communicate with other programmers (and your future self).

A comment is added to your program in two different ways, by putting // in front of some text, or surrounding a block of text with /**/.

Here's an example of the // comment.

scala> //hi everyone

This comment is one line only. It's usually used to add notes to your program.

scala> 4+5 //this will produce 9
val res0: Int = 9 

scala> //see?

The block comment /**/ is used to add larger, multiline notes to your program, or add an inline note to some code.

scala> /*hello
world*/

scala> 4 /*this is the age of my daughter*/ + 5
val res0: Int = 9

Types

Scala is a statically, strongly typed language. Static typing means that every value has a type, and said type doesn't change for the entire program. Strong typing means that Scala is very unlikely to interpret one type of data as another, and will quickly throw a compilation error if you mix up your data types.

But what is a type? A type is a label for data that tells the programming language how to interpret it. As you may or may not be aware, all memory in your computer is a series of 0 and 1 values. These values can be interpreted as:

  • numbers - 0b00000011 = 3
  • letters - 0b01000001 = a and so on... Types tell the program how these 0s and 1s should be interpreted, and what can be done with them.

One way to think about types is like file extensions. .jpg tells your computer that the file in question is a kind of image. .txt tells your computer that the file is a text file. If you change the extension of a file, your computer will try to interpret the data in the file in a different way, sometimes with disastrous results. Types serve a very similar function in Scala, telling the programming language what the data is, how it should be interpreted, and how it can be used.

Int, the whole number datatype

Int is one of the most common types in Scala, and it represents a whole number. Examples of Int data are 1, 7, and -56. 3.14 is not Int data, as it is fractional.

If you use the division operator on two pieces of Int data, you’ll get Int data back. This means that division of Int values cannot produce fractional results. For example:

val a = 1
println(a/2) //returns 0
val b = 4
println(b/2) //returns 2

Because Ints must remain whole at all times, a division operation that yields a fractional result will be rounded down to the nearest whole number. In the above example, 1/2 would return 0.5, but since Int cannot store that data, 0 is the result.

Int is a 32-bit datatype, meaning that it uses thirty-two 1s and 0s to record numeric information. The format it uses means that it can only store numbers of a limited size. In Scala, an Int can not be greater than 2147483647, or less than -2147483648. If you add 1 to 2147483647, you will end up with the result -2147483648, and if you subtract 1 from -2147483648 you will get 2147483647 back.

Double, the fractional number datatype

Doubles are a type that represents fractional data. That is, they can be used to express numbers like 0.5 and 400. Double data is stored differently than Int data, using 64-bits in a different format to describe numbers. Because of the differences in how it stores numbers, Doubles can represent much larger numbers than Ints ever can. The largest number that can be stored in Double format is 1.7976931348623157E308; 1.7976931348623157 times 10 to the power of 308. The downside of the Double format is precision. An Int formatted data can represent every number in its range exactly. On the other hand, the Double format can only approximately represent a good deal of numbers, meaning that after doing math with Doubles for a while, you will get less and less precise results. It is for this reason that if you do not need to express very large numbers or fractional numbers, you should use the Int datatype instead of Double.

String, the textual datatype

As explained in the last part, String is the representation of textual data in Scala. In short, it describes a piece of text and it's a fundamental building block of the Scala language.

There are a number of ways to construct a String in Scala. One of them involves surrounding text with double quotes (").

"Hello, World!"

Another involves the usage of triple quotes (""").

"""This is a triple quoted String. I can use "double quotes" in this string with no problem. 
I can also have newlines in this String very easily."""

Triple quote Strings have advantages. The text you enter into the String will look almost exactly like what will be output by your program. There are downsides to Triple quote Strings though. With regular Strings, you can use short hand text to represent certain abstract concepts. An example is the concept of a newline, which can be put into a normal string with \n.

scala> "hello\nworld"
val res0: String = hello
world

Another example is tabs, configuration-based indentation of text. With a double quote string, you can insert a tab with \t.

scala> "\thello world"
val res0: String = "    hello world"

The size of a tab is controlled by whatever software is representing it. Some things make a tab equal to 4 spaces. Some things make it equal to 2 spaces. VSCode will allow you to configure the size of a tab to whatever you'd like it to be. This concept of tabs is wholly unrepresentable with triple quote Strings.

Boolean, the logical datatype

The Boolean type represents two possible pieces of data: true and false. This datatype is incredibly important to Scala, as it’s used to make logical decisions in a program.

Specifying the type of a Value

In order to specify the type of a value, you follow the name of the value with : TypeName. For example:

val a: Int = 4

As before, we use the val keyword to indicate we’re declaring a value, followed by the name of the value. What changes this time is that we follow the name with : Int, which specifies that the value’s type is Int. After that, we follow with the assignment operator and some data in order to define the value.

Type Inference

As I wrote before. Scala is statically typed. This means that every value that’s declared and defined has an associated type. However, in all previous examples we haven’t specified the types for our values.

These values had types too, but we didn’t need to specify their types because Scala has type inference. This allows Scala to deduce what type the value should be based on the data you define it with. For example:

val a = 4 //a has type Int
val b = true //b has type Boolean

Type inference lets us cut down on some of the boilerplate in our programs. It's immediately obvious when we write val a = 4 that a should be the type Int, since the type of the data defining it, 4, is Int. With type inference, we leave it to the Scala compiler to work out the type based on the data we've given it. That being said you can always specify what type a value is.

val a: Int = 3

Methods

In Scala, methods are program fragments that perform work upon being invoked. These program fragments are bound to types, and can only be invoked on data of the appropriate type. In order to invoke a method, one provides some data, followed by the . operator, followed by the name of the method to invoke. An example of a method is the toString method that exists for all types in Scala:

scala> 5.toString
val res0: String = 5

Here, we invoke the toString method on the data 5. The purpose of the toString method is to convert some data into a String. When invoked, it performs some work in the background to transform the original data into textual data. It is not necessary to know how this is done in order to use the toString method, and that fact is one of the hallmarks of a good method.

Other methods exist in Scala that have similar purposes to toString. For example, toInt is a method that is attached to the String type; any String data can have the toInt method invoked on it to attempt to turn it into an Int:

scala> "5".toInt
val res0: Int = 5

scala> val numberString = "15"
val numberString: String = "15"

scala> numberString.toInt //this works too
val res1: Int = 15

A similar method for turning Strings to Doubles is attached to the String type:

scala> "4.0".toDouble
val res0: Double 4.0

toDouble and toInt is likewise attached to the Int and Double types, allowing one to convert an Int to a Double and vice-versa:

scala> 4.0.toInt
val res0: Int = 4
scala> 4.toDouble
val res1: Double = 4.0

Note that a Double that is not whole when converted to an Int will be truncated:

scala> 4.3.toInt
val res0: Int = 4

What happens when you try to convert a String that is not a textual representation of a whole number into an Int?

scala> "hello".toInt
java.lang.NumberFormatException: For input string: "hello"
  at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
  at java.base/java.lang.Integer.parseInt(Integer.java:668)
  at java.base/java.lang.Integer.parseInt(Integer.java:786)
  at scala.collection.StringOps$.toInt$extension(StringOps.scala:910)
  ... 28 elided

When converting Strings into number types, one must be careful, or your program will crash.

Method parameters

The behavior of the methods toString, toInt, and toDouble only changes based on the values they're invoked from. "5".toInt will always produce 5 and 4.toDouble will always produce 4.0. There are other methods that take input however, and such methods will produce different results based on this additional input. We call the input to a method a parameter, and it is passed to a method via the usage of parentheses.

scala> "hello world".take(3)
val res0: String = hel

scala> "hello world".take(5)
val res1: String = hello

Whether or not a method takes parameters depends on its definition.

Did you know you've been using methods since Part 1? Not all methods require the . operator to be invoked. For example, println is short for System.out.println, but doesn't require the . operator to invoke it.

scala> println("hello, world!")
hello, world!

You've even used methods that don't require the . operator or parentheses to pass inputs to them. The operators discussed in Part 1 are themselves cleverly disguised methods:

4.+(5)

Helpful string methods

There is a great deal of methods that exist on Strings. The following list is a small selection of helpful methods. Try using them and seeing their effect on your Strings:

  • size - Returns the number of letters in the `String
  • reverse - Reverses a String
  • distinct - Outputs each letter in the String
  • nonEmpty - If the String has text, returns true, otherwise `false
  • empty - If the String has no text, returns true, otherwise false
  • toLowerCase - Makes the String lower case
  • toUpperCase - Makes the String upper case
  • last - Gives the last letter of a String

For a more complete listing of the methods available on String, refer to The Java String API and The Scala String extensions.

String Interpolation

String interpolation is used to construct strings with values and calculations instead of using the concatenation operator.

Let’s say you wanted to have a program that output a user’s name and age. With concatenation you would write something like:

val name = “Trixie”

val age = 23

println(“The user’s name is “ + name + “ and they’re “ + age + “.”)

Using concatenation, we have to be careful to leave trailing spaces so that our text is formatted nicely, and we have to open and close the string multiple times. This can be error prone.

String interpolation allows us to more directly insert data into Strings:

val name = “Trixie” 
val age = 26

println(s”The user’s name is $name and they’re $age.”)

The s in front of the double quotes activates string interpretation. Once that’s added, anything with a $ in front of it is interpreted as a value name, and said value’s contents will be injected in place of the name and the $ symbol.

String interpolation works with both double quotes and triple quotes strings.

Comparisons

Comparison is the act of determining something's relationship to something else. There are many comparisons that can be made between types of data, but there is a set of common ones that are used almost universally in programming:

  • < - Less than
  • == - Equal to
  • != - Not equal to
  • > - Greater Than
  • >= Greater than or equal to
  • <= Less than or equal to

If you've ever done any math in school, most of these should be familiar to you. They are all binary operators; they take two pieces of data and return data of the Boolean data type. That is, they will return true or false to you when used.

scala> 4 > 2
val res0: Boolean = true
scala> 4 < 2
val res1: Boolean = false
scala> 5 == 5
val res2: Boolean = true
scala> 2 != 3
val res3: Boolean = true

If you've read through the methods section of this part, you may be wondering if these binary operators are cleverly disguised methods that take one parameter:

scala> 4.<(5)
val res0: Boolean = true

These comparison operators are extremely useful for working with data, and are defined for all of the types we've discussed so far.

scala> 3.0 < 5.0
val res0: Boolean = true

scala> false < true
val res1: Boolean = true

Strings to can be compared with these operators, though their comparison is less intuitive than the previously mentioned types. Strings use lexicographic ordering. When you ask if one String is less than another, the computer will check letter by letter and answer based on the position in the alphabet of the letters being compared. Let's see what this looks like by seeing if "Hola" is less than "Hello".

  1. First we compare the letter H to the letter H.
  2. H is equal to H so we go to the next letter.
  3. Now we compare the letter e to the letter o.
  4. o is greater than e so we stop here
  5. Since Ho is greater than He, "Hola" is not less than "Hello"
  6. The operator returns false

Note that lexicographic ordering considers capitalized letters to be less than uncapitalized letters. Don't let this trip you up!!

And, Or, and Not

For the Boolean type, there exists three very important operators:

  • && - And
  • || - Or
  • ! - Not

These three logical operators allow you to make logical calculations with Boolean data.

And

The and operation is represented by the && operator in Scala. && is a binary operator that takes two Boolean values and returns true if both its inputs are true. Otherwise, it returns false.

scala> true && false
val res0: Boolean = false

scala> true && true
val res1: Boolean = true

scala> false && true
val res2: Boolean = false

scala> false && false
val res3: Boolean = false

The and operation is very useful if you want to know if two or more things are true at the same time.

Or

The or operation is represented by the || operator in Scala. || is a binary operator that takes two Boolean values and returns true if any of its inputs are true. Otherwise, it returns false

scala> true || false
val res0: Boolean = true

scala> true || true
val res1: Boolean = true

scala> false || true
val res2: Boolean = true

scala> false || false
val res3: Boolean = false

The or operation is useful if you want to know if at least one thing is true.

Not

The not operation is represented by the ! operator in Scala. ! is a unary operator that goes in front of the data you want to apply it to. When applied to Boolean data, it inverts it, turning true to false and false to true.

scala> !false
val res0: Boolean = true

scala> !true
val res1: Boolean = false

Logical and comparison operators in use

Logical and comparison operators are frequently used together, and can be used to express complex logic. As an example, if we wanted to test if 4 is less than 3 but greater than 1, we could write:

scala> 1 < 4 && 4 < 3
val res0: Boolean = false

Can you write an expression that tests if a String is equal to "cat" or less than "Dog"?

Practice

To give you a good bit of practice, I've created some scastie.scala-lang.org exercises for you to play with. It's best that you practice programming on your own computer rather than in an online environment, but we'll reach that point soon enough.

Be sure to do the lessons in order presented here. Pressing "run" at the top will run the code. The code will be executed in your browser and output will appear in the Javascript Console. Read all directions, watch the terminal, and read any errors. Don't forget to use println extensively.

Did you find this article valuable?

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