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.
| 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
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 typesExpressions #
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 languagesFunctions #
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"