Skip to main content
Scala: Scala Basics
  1. Posts/

Scala: Scala Basics

·2493 words·12 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

This week, I decided to focus on some Scala basics.

I also wanted something more guided, compared to the previous three weeks, where I was just looking up whatever I was working on.

So, to learn some foundations of Scala, I will be following this course: Scala Essentials | Rock the JVM.

Here are some notes I have taken from this course.

Starting with the first chapter…

Absolute Basics
#

Values and Types
#

Scala is a statically-typed language, which means every value and variable has a type known at compile time.
Values are defined with val (immutable), and variables with var (mutable, but discouraged).
Scala has only objects (no primitives), so you can call methods on any value, even numbers and booleans.

Scala’s “value types”

Data Type Possible Values
Boolean true or false
Byte 8-bit signed two’s complement integer (-2^7 to 2^7-1, inclusive)
-128 to 127
Short 16-bit signed two’s complement integer (-2^15 to 2^15-1, inclusive)
-32,768 to 32,767
Int 32-bit two’s complement integer (-2^31 to 2^31-1, inclusive)
-2,147,483,648 to 2,147,483,647
Long 64-bit two’s complement integer (-2^63 to 2^63-1, inclusive)
(-2^63 to 2^63-1, inclusive)
Float 32-bit IEEE 754 single-precision float
1.40129846432481707e-45 to 3.40282346638528860e+38
Double 64-bit IEEE 754 double-precision float
4.94065645841246544e-324 to 1.79769313486231570e+308
Char 16-bit unsigned Unicode character (0 to 2^16-1, inclusive)
0 to 65,535
String A sequence of Char

Scala has no primitives, only objects, which wrap around Java primitives. This means you can call methods on any value, even numbers and booleans.

Scala Type Hierarchy. In Scala, there are no primitive types… | by Amit Prasad | Medium

A First Look at Types | Scala 3 - Book | Scala Documentation

Scala Type Hierarchy

Example of defining values and types:

// values  
val meaningOfLife: Int = 42 // const int meaningOfLife = 42  
  
// reassigning is not allowed  
// It's constant, not a variable, hence why we call it a "value"  
meaningOfLife = 45 // would not compile  
  
  
// type inference  
val anInteger = 67 // : Int is optional  
  
// common types AnyVal, which are value types, are non-nullable  
val aBoolean: Boolean = false  
val aChar: Char = 'a'  
val anInt: Int = 78 // 4 bytes  
val aShort: Short = 5263 // 2 bytes  
val aLong: Long = 52789572389234L // 8 bytes  
val aFloat: Float = 2.4f // 4 bytes  
val aDouble: Double = 3.14 // 8 bytes  
val aByte: Byte = 127 // 1 byte can hold values from -128 to 127  
val aUnit: Unit = () // no value, no type, just a placeholder (similar to void in Java)  
  
// AnyRef, which are reference types, are nullable  
// Every user-defined type is a subtype of AnyRef  
val aString: String = "Scala"  
// Lists, Maps, Sets, Tuples, etc. are all subtypes of AnyRef  
// Reference types

Expressions
#

Expressions are code fragments that are evaluated to a value. In Scala, almost everything is an expression (including if, code blocks, etc.), not just a statement.
This means you can assign the result of an if or a block to a value.

// expressions are structures that can be evaluated to a value  
val meaningOfLife = 40 + 2  
  
// mathematical expressions: +, -, *, /, bitwise |, &, <<, >>, >>>  
val mathExpression = 2 + 3 * 4  
  
// comparison expressions: <, <=, >, >=, ==, !=  
val equalityTest = 1 == 2  
  
// boolean expressions: !, ||, &&  
val nonEqualityTest = !equalityTest  
  
// instructions vs expressions  
// expressions are evaluated, instructions are executed  
// we think in terms of expressions  
  
// ifs are expressions  
val aCondition = true  
val anIfExpression = if (aCondition) 45 else 99  
  
// code blocks  
val aCodeBlock = {  
  // local values  
  val localValue = 78  
  // expressions...  
  
  // last expression = value of the block  localValue + 54  
}  
  
// everything is an expression  
  
/**  
 * Exercise: *  Without running the code, what do you think these values will print out? */// 1 = true  
val someValue = {  
  2 < 3  
}  
  
// 2 = 42  
val someOtherValue = {  
  if (someValue) 239 else 986  
  42  
}  
  
// 3 = Scala, ()  
val yetAnotherValue: Unit = println("Scala")  
val theUnit: Unit = () // Unit == "void" in other languages

Functions
#

A function in Scala is defined with def. Functions can take parameters and return values.
Recursion is preferred over loops for repetition in functional programming.

// function = reusable piece of code that you can invoke with some arguments and return a result  
def aFunction(a: String, b: Int): String =  
  a + " " + b // ONE expression  
  
// function invocation  
val aFunctionInvocation = aFunction("Scala", 999999999)  
  
def aNoArgFunction(): Int = 45  
def aParamterlessFunction: Int = 45  
  
// functions can be recursive  
def stringConcatenation(str: String, n: Int): String =  
  if (n == 0) ""  
  else if (n == 1) str  
  else str + stringConcatenation(str, n - 1)  
  
/*  
sc("Scala", 3) = "Scala" + sc("Scala", 2) = "Scala" + "ScalaScala" = "ScalaScalaScala"  
sc("Scala", 2) = "Scala" + sc("Scala", 1) = "Scala" + "Scala" = "ScalaScala"  
sc("Scala", 1) = "Scala" 
*/
val scalax3 = stringConcatenation("Scala", 3)  
// when you need loops, use RECURSION.  
  
// "void" functions  
def aVoidFunction(aString: String): Unit =  
  println(aString)  
  
def computeDoubleStringWithSideEffect(aString: String): String = {  
  aVoidFunction(aString) // Unit  
  aString + aString // meaningful value  
} // discouraging side effects  
  
// Nested functions  
def aBigFunction(n: Int): Int = {  
  // small, auxiliary functions inside  
  def aSmallerFunction(a: Int, b: Int): Int = a + b  
  
  aSmallerFunction(n, n + 1)  
}  
  
/**  
 * Exercises 
 * 1. A greeting function (name, age) => "Hi my name is $name and I am $age years old." 
 * 2. Factorial function n => 1 * 2 * 3 * .. * n 
 * 3. Fibonacci function 
 *    fib(1) = 1 
 *    fib(2) = 1 
 *    fib(3) = 1 + 1 
 *    fib(n) = fib(n-1) + fib(n-2) 
 * 
 * 4. Tests if a number is prime 
 */  
// 1  
def greetingForKids(name: String, age: Int): String =  
  "Hi, my name is " + name + " and I am " + age + " years old."  
  
// 2  
/*  
f(5) = 5 
* f(4) = 120  f(4) = 4 
* f(3) = 24  f(3) = 3 
* f(2) = 6  f(2) = 2 
* f(1) = 2  f(1) = 1 
*/
def factorial(n: Int): Int =  
  if (n <= 0) 0  
  else if (n == 1) 1  
  else n * factorial(n - 1)  
  
// 3  
/*  
fib(5) = fib(4) + fib(3) = 5  
fib(4) = fib(3) + fib(2) = 3  
fib(3) = fib(2) + fib(1) = 2  f
fib(1) = 1 
*/
def fibonacci(n: Int): Int =  
  if (n <= 2) 1  
  else fibonacci(n - 1) + fibonacci(n - 2)  
  
// 4  
/*  
isPrime(7) = isPrimeUntil(3) = true  
ipu(3) = 7 % 3 != 0 && ipu(2) = true  
ipu(2) = 7 % 2 != 0 && ipu(1) = true  i
pu(1) = true 
*/
def isPrime(n: Int): Boolean = {  
  def isPrimeUntil(t: Int): Boolean =  
    if (t <= 1) true  
    else n % t != 0 && isPrimeUntil(t - 1)  
  
  isPrimeUntil(n / 2)  
}

Stack and Tail Recursion
#

Recursion is when a function calls itself.
Tail recursion is a special case where the recursive call is the last thing executed, allowing the compiler to optimize and avoid stack overflows.
Use the @tailrec annotation to ensure your recursion is tail-recursive.

// "repetition" = recursion  
// if we give it a very large number, we will get a StackOverflowError  
// because the stack frames will grow too large, as each recursive call adds a new frame to the stack  
def sumUntil(n: Int): Int =  
  if (n <= 0) 0  
  else n + sumUntil(n - 1) // "stack" recursion  
  
// TAIL recursion = the recursive call is the LAST operation in the function  
def sumUntil_v2(n: Int): Int = {  
  /*  
  sut(10, 0) =    
  sut(9, 10) =    
  sut(8, 9 + 10) =    
  sut(7, 8 + 9 + 10) =   
  ...    
  sut(0, 1 + 2 + 3 + .. + 9 + 10)    = 1 + 2 + 3 + .. + 10   
  */  
    
  @tailrec // this annotation tells the compiler to check if the function is tail recursive  
  def sumUntilTailrec(x: Int, accumulator: Int): Int =  
    if (x <= 0) accumulator  
    else sumUntilTailrec(x - 1, accumulator + x) // TAIL recursion = recursive call occurs LAST in its code path  
    // no further stack frames necessary = no more risk of StackOverflowError  
  sumUntilTailrec(n, 0)  
}  
  
// sum of numbers between a and b, inclusive  
// Stack Recursion  
def sumNumbersBetween(a: Int, b: Int): Int =  
  if (a > b) 0  
  else a + sumNumbersBetween(a + 1, b)  
  
// Tail Recursion  
def sumNumbersBetween_v2(a: Int, b: Int): Int = {  
  @tailrec  
  def sumTailrec(currentNumber: Int, accumulator: Int): Int =  
    if (currentNumber > b) accumulator  
    else sumTailrec(currentNumber + 1, currentNumber + accumulator)  
  
  sumTailrec(a, 0)  
}  
  
/**  
* Exercises 
* 1. Concatenate a string n times 
* 2. Fibonacci function, tail recursive 
* 3. Is isPrime function tail recursive or not? 
*/  
// 1  
def concatenate(string: String, n: Int): String = {  
  @tailrec  
  def concatTailrec(remainingTimes: Int, accumulator: String): String =  
    if (remainingTimes <= 0) accumulator  
    else concatTailrec(remainingTimes - 1, string + accumulator)  
  
  concatTailrec(n, "")  
}  
  
// 2  
def fibonacci(n: Int): Int = {  
  // i is the current index in the Fibonacci sequence  
  // last is the last Fibonacci number  
  // previous is the Fibonacci number before last  
  @tailrec  
  def fiboTailrec(i: Int, last: Int, previous: Int): Int =  
    if (i >= n) last  
    else fiboTailrec(i + 1, last + previous, last)  
  
  if (n <= 2) 1  
  else fiboTailrec(2, 1, 1)  
}  
  
// 3 - yes, rephrasing:  
def isPrime(n: Int): Boolean = {  
  @tailrec  
  def isPrimeUntil(t: Int): Boolean =  
    if (t <= 1) true  
      // && is a short-circuit operator, it will not evaluate the second condition if the first is false  
    else if (n % t == 0) false  
    else isPrimeUntil(t - 1)  
  
  isPrimeUntil(n / 2)  
}

Call By Name and Call by Value
#

Scala supports two evaluation strategies for function arguments:

  • Call-by-value (default): argument is evaluated before the function is called.
  • Call-by-name (=>): argument is evaluated each time it is used in the function.
// CBV = call by value = arguments are evaluated before function invocation  
def aFunction(arg: Int): Int = arg + 1  
// aFunction is evaluated BEFORE the function is invoked  
// so it's as if it was called like this: 
// aFunction(90) = 90 + 1 = 91  
val aComputation = aFunction(23 + 67)  
  
// CBN = call by name = arguments are passed LITERALLY, evaluated at every reference  
def aByNameFunction(arg: => Int): Int = arg + 1  
// aByNameFunction is NOT evaluated before the function is invoked  
// so it's as if it was called like this: 
// aByNameFunction(23 + 67) = (23 + 67) + 1 = 91  
val anotherComputation = aByNameFunction(23 + 67)  
  
/*  
CBV major features:  
  - eager evaluation of the argument  
  - argument is evaluated only once, before the function is invoked 
*/
def printTwiceByValue(x: Long): Unit = {  
  println("By value: " + x)  
  println("By value: " + x)  
}  
  
/*  
CBN major features:  
  - delayed evaluation of the argument  
  - argument is evaluated every time it is used 
*/
def printTwiceByName(x: => Long): Unit = {  
  println("By name: " + x)  
  println("By name: " + x)  
}  
  
/*  
Another benefit of CBN is that it delays
the evaluation of the argument until it's used.  
If it's not used, it's not evaluated. 
*/
def infinite(): Int = 1 + infinite()  
def printFirst(x: Int, y: => Int) = println(x)  
  
  
def main(args: Array[String]): Unit = {  
  printTwiceByValue(System.nanoTime()) // prints the same instant twice  
  printTwiceByName(System.nanoTime()) // prints two different instants  
  
  printFirst(42, infinite()) // works  
  // printFirst(infinite(), 42) // crashes

// Example: Logging only if needed
def debug(msg: => String): Unit = if (sys.env.contains("DEBUG")) println(msg)
debug("Expensive computation: " + (1 to 1000000).sum) // Only evaluated if DEBUG is set
}

Default and Named Arguments
#

You can provide default values for function parameters, and use named arguments to make calls more readable or to skip some defaults.

// default arguments are arguments that have a default value  
// if you don't pass them, the default value is used  
@tailrec  
def sumUntilTailrec(x: Int, accumulator: Int = 0): Int =  
  if (x <= 0) accumulator  
  else sumUntilTailrec(x - 1, accumulator + x)  
  
val sumUntil100 = sumUntilTailrec(100) // additional arg passed automatically  
  
// when you use a function most of the time with the same value = default arguments  
def savePicture(dirPath: String, name: String, format: String = "jpg", width: Int = 1920, height: Int = 1080): Unit =  
  println("Saving picture in format " + format + " in path " + dirPath)  
  
  
def main(args: Array[String]): Unit = {  
  // default args are injected  
  savePicture("/users/daniel/photos", "myphoto")  
  // pass explicit different values for default args  
  savePicture("/users/daniel/photos", "myphoto", "png")  
  // pass values after the default argument  
  savePicture("/users/daniel/photos", "myphoto", width = 800, height = 600)  
  // naming arguments allow passing in a different order  
  savePicture("/users/daniel/photos", "myphoto", height = 600, width = 800)  
}

String Operations and Interpolations
#

Strings in Scala are objects with many useful methods.
String interpolation allows embedding variables and expressions directly in strings using s, f, or raw prefixes.

val aString: String = "Hello, I am learning Scala"  
  
// string functions  
val secondChar = aString.charAt(1) // 'e' get the character at index 1  
val firstWord = aString.substring(0, 5) // "Hello" get the substring from index 0 to 5 (exclusive)  
val words = aString.split(" ") // Array("Hello,", "I", "am", "learning", "Scala")  
val startsWithHello = aString.startsWith("Hello") // true  
val allDashes = aString.replace(' ', '-') // "Hello,-I-am-learning-Scala"  
val allUppercase = aString.toUpperCase() // also toLowerCase()  
val nChars = aString.length  
  
// other functions  
val reversed = aString.reverse  
val aBunchOfChars = aString.take(10) // "Hello, I a" take the first 10 characters  
val sortedString = aString.sorted // sorts in ascending order  
// To do descending order, you can use sorted.reverse  
  
// parse to numeric  
val numberAsString = "2"  
val number: Int = numberAsString.toInt  
  
// s-interpolation  
val name = "Alice"  
val age = 12  
val greeting: String = "Hello, I'm " + name + " and I am " + age + "years old."  
val greeting_v2 = s"Hello, I'm $name and I'm $age years old."val greeting_v3 = s"Hello, I'm $name and I will be turning ${age + 1} years old."  
// f-interpolation  
val speed = 1.2f  
val myth = f"$name can eat $speed%2.5f burgers per minute." // "Alice can eat 1.20000 burgers per minute."  
val pi = 3.14159  
val formattedPi = f"Pi rounded to 2 decimals: $pi%1.2f" // "Pi rounded to 2 decimals: 3.14"  
val item = "apple"  
val price = 2.5  
val formattedPrice = f"The price of $item is $$${price}%1.2f" // "The price of apple is $2.50"  
val percent = 0.756  
val formattedPercent = f"Success rate: ${percent * 100}%2.1f%%" // "Success rate: 75.6%"  
val count = 7  
val paddedCount = f"Count: $count%03d" // "Count: 007"  
  
// raw-interpolation  
// raw strings do not interpret escape characters  
val escapes = raw"This is a \n newline"