en pt

Scala Tutorial Notes

24 Mar 2019 | 22mins

This is the small Scala summary I have made while reading the language tutorial. My goal is to make this post as alive as possible, as I learn more things about the language. Syntax comparisons will be made mainly against ruby and javascript.

Use the following pages to help you through your reading: Scala-lang tutorial and Twitter Scala School.

Terms & types

Primitive Expressions

they represent the simplest elements

The integer 1

   
    1
   

The double 1.572

   
    1.572
   

The boolean value “true”

   
    true
   

The text(String) “Hello”

   
    "Hello"
   

IMPORTANT! Strings on Scala are different from ruby, which accepts both ‘ and “. On Scala always use “” for strings.

Operators

1 + 2 = 3

1 and 2 are operands, ‘+’ is the operator

“Hello, “ ++ “Scala”

“Hello, “ and “Scala” are operands, ‘++’ is the operator

Method Calls

   
    "hello".size
   

“hello” is the target object size is the method

Methods are applied on expressions using the dot notation. Method parameters are passed inside ().

Expressions vs Statements

Expressions: What code is when evaluated. Returns a value.

Statements: represent an action or command to be executed. (What code does)

Method vs Functions

Methods: operate inside an object Function: The rest

Operators are methods

In fact, operators are methods with symbolic values or symbolic identifier.

  
   3 + 2 == 3.+(2)
   1.to(10) == 1 to 10
  

Each expression (or object) has a value and a type.

Definitions & Evaluations

Naming

Naming things gives more readability to your program.

Use ‘Val’ to define constants and ‘Def’ to define methods/functions.

val = value

def = definition

Methods

Can have one or more parameters, separated with ‘,’, and with its respective types defined after a ‘:’. ex:

   
    def square(x: Double) = x * x
    def sumOfSquares(x: Double, y: Double) = square(x) + square(y)
   

If a method is recursive, i.e. call for itself, you need to specify its return type

   
    def factorial(n: Int): Int =
      if (n ==1) 0
      else factorial(n - 1) * n
   

Evaluation

The substitution model is an expression evaluation scheme. Used when the expression can be reduced to a value(not a loop).

Call-by-Value

  1. Reduces arguments first.
  2. Compute the function values, from left to right.

Call-by-Name

  1. Start computing the function values, from left to right.
  2. Reduce arguments when necessary. Can let an argument unreduced if it’s not used on function body.

Functional Loops

Conditional

   
    def abs(x: Double) = if (x >= 0) x else -x
   

Boolean Expressions

   
    true  false      // Constants
    !b               // Negation
    b && b           // Conjunction
    b || b           // Disjunction

    e <= e, e >= e, e < e, e > e, e == e, e != e
   

Lexical Scopes

Nested Functions

Auxiliary functions are defined inside the parent function. Used to isolate a function from the user, just like private functions.

   
    def sqrt(x: Double) = {
      def sqrtIter(guess: Double, x: Double): Double =
        if (isGoodEnough(guess, x)) guess
        else sqrtIter(improve(guess, x), x)

      def improve(guess: Double, x: Double) =
        (guess + x / guess) / 2

      def isGoodEnough(guess: Double, x: Double) =
        abs(square(guess) - x) < 0.001

      sqrtIter(1.0, x)
    }
   

Blocks

Lexical Scoping

Definitions outside blocks are visible inside unless they are shadowed. (Would it be shadowed when redefined, right?)

refactoring sqrt method:

   
    def sqrt(x: Double) = {
      def sqrtIter(guess: Double): Double =
        if (isGoodEnough(guess)) guess
        else sqrtIter(improve(guess))

      def improve(guess: Double) =
        (guess + x / guess) / 2

      def isGoodEnough(guess: Double) =
        abs(square(guess) - x) < 0.001

      sqrtIter(1.0)
    }
   

Semicolons

In most cases semicolons are optional. Used when you have two statements in a single line. When you have 2 long expressions, you can overcome implicit ‘;’ by putting the expressions inside () or by putting the operator ‘+’ on the first line.

   
    someLongExpression +
      someOtherExpression
   

Top-level Definitions

The Object MyExecutableProgram has nested definitions (vals, defs & methods) and is called top-level because is not nested within another definition.

Packages and Imports

Top-level definitions can be organized an imported as packages

   
    // file foo/Baz.scala
    package foo
    object Baz {
      Bar.someMethod
      // Bar is visible because it is in the `foo` package too
    }
   

When some definitions are not visible, you must use fully qualified names to refer them:

   
    // file quux/Quux.scala
    package quux
    object Quux {
      foo.Bar.someMethod
    }
   

Here foo is a package(class?), Bar is an object and someMethod is a method

you can also import an object to not repeat them:

   
    // file quux/Quux.scala
    package quux
    import foo.Bar
    object Quux {
      // Bar refers to the imported `foo.Bar`
      Bar.someMethod
    }
   

Automatic Imports

Any Scala application automatically imports the packages scala, java.lang and scala.Predef.

Writing Executable Programs

All executable applications should have a main method inside an object

   
    object Hello {
      def main(args: Array[String]) = println("hello world!")
    }
   

and can be executed with the following code:

   
    $ scala Hello
   

Tail Recursion

Recursive Function Application

Maximum Common Divisor

   
    def gcd(a: Int, b: Int): Int =
      if (b == 0) a else gcd(b, a % b)
   

Factorial

   
    def factorial(n: Int): Int =
      if (n == 0) 1 else n * factorial(n - 1)
   

MCD oscillates and Factorial extends the number of elements on its expression.

Tail Recursion

Is when the last action of a method is to call itself. In case of the factorial method, it’s not tail recursive because after calling itself with factorial(n-1) it has to multiply to n.

“Both factorial and gcd only call itself but in general, of course, a function could call other functions. So the generalization of tail recursion is that, if the last action of a function consists of calling another function, maybe the same, maybe some other function, the stack frame could be reused for both functions. Such calls are called tail calls.”

What is the stack frame?

It’s possible to require that the following defined function is recursive using the @tailrec annotation. If the function were not tail recursive, it would prompt an error.

Tail recursive factorial:

   
    def factorial(n: Int): Int = {
      @tailrec
      def iter(x: Int, result: Int): Int =
        if (x == 1) result
        else iter(x - 1, result * x)

      iter(n,1)
    }
   

Structuring Information

Aggregating Information with Case Classes

Case class has a syntax close to a JS object

   
    case class Note(
      name: String,
      duration: String,
      octave: Int
    )
   

we can create values(like objects or instances) of this type calling its constructor(Note(…))

   
    val c3 = Note("C", "Quarter", 3)
   

and we can access any property by using the dot notation, just like Ruby or Javascript.

Defining Alternatives with Sealed Traits

A type(here, Symbol) is something that can only be embodied by a fixed set of alternatives. We can express it using the sealed trait definition. For example, a musical symbol can be a note or a rest:

   
    sealed trait Symbol
    case class Note(...) extends Symbol
    case class Rest(...) extends Symbol
   

Pattern Matching

Works as a case function. If the object matches a pattern(LHS) it executes an expression(RHS). The arrow symbol(=>) separates the pattern from the expressions.

Used to distinguish between the different cases of symbols.

   
    def symbolDuration(symbol: Symbol): String =
      symbol match {
        case Note(name, duration, octave) => duration
        case Rest(duration) => duration
      }
   

Here, Note(…) and Rest(…) are constructor patterns and duration is called variable pattern.

Exhaustivity

When not all cases of a Symbol are handled, the compiler informs us.

Equals

When comparing instances of case classes you compare their values. This is different from Ruby classes, where each instance is different from others, even if its properties are equal.

Enumerations

Fix a set of alternatives to a property.

   
    sealed trait NoteName
    case object A extends NoteName
    case object B extends NoteName
    case object C extends NoteName
    …
    case object G extends NoteName
   

Algebraic Data Types

if a part of a program can be formulated in terms of an is relationship, you will express it as a sealed trait: “A symbol is either a note or a rest.”

if it can be formulated in terms of an has relationship, you will express it as a case class:

“A note has a name, a duration, and an octave number.”

Observations

case class != case object

A case class is used to define a class(name, properties, and parent(extends …)) and a case object is used to restrict the values of a property

Higher-Order Functions

On functional languages functions are treated as first-class values, meaning they can be passed as parameters and returned as a result. Those are named higher-order functions.

Motivation

It’s possible to factor out various recursive higher-order functions. Examples:

Sum all integers between a and b ( b > a ):

   
    def sumInts(a: Int, b: Int): Int =
      if (a > b) 0 else a + sumInts(a + 1, b)
   

Sum the cubes of all the integers between a and b

   
    def cube(x: Int): Int = x * x * x

    def sumCubes(a: Int, b: Int): Int =
      if (a > b) 0 else cube(a) + sumCubes(a + 1, b)
   

Sum the factorials of all the integers between a and b

   
    def sumFactorials(a: Int, b: Int): Int =
      if (a > b) 0 else factorial(a) + sumFactorials(a + 1, b)
   

Summing with higher-order functions

The functions above can be summarized passing the respective function (id/cube/factorial) as a parameter to a higher order function (sum).

   
    def sum(f: Int => Int, a: Int, b: Int): Int =
      if (a > b) 0
      else f(a) + sum(f, a + 1, b)
   
   
    def id(x: Int): Int = x
    def sumInts(a: Int, b: Int) = sum(id, a, b)
    def sumCubes(a: Int, b: Int) = sum(cube, a, b)
    def sumFactorials(a: Int, b: Int) = sum(factorial, a, b)
   

Functions Types

A => B

A argument

returns B

Anonymous Functions

Function literal. i.e. don’t need to define a name for it

Anonymous cube function:

   
    (x: int) => x * x * x
   

(…) is the parameter and x * x * x is the body.

The type of the parameter can be omitted. Multiple parameters are separated by commas.

Syntactic Sugar

Are functions defined as below: why they are used? I don’t know…

   
    { def funcName(x1: T1, …, xn: Tn) = e ; funcName }
   

Summation with Anonymous Functions

The respective functions(id/cube) are not defined and passed as parameters anymore. They are passed as literals on the higher order function (sum).

   
    def sumInts(a: Int, b: Int) = sum(x => x, a, b)
    def sumCubes(a: Int, b: Int) = sum(x => x * x * x, a, b)
   

Standard Library

Lists

They seem like an array, but they are not.

Ex:

   
    val fruit: List[String] = List("apples", "oranges", "pears")
    val nums: List[Int] = List(1, 2, 3, 4)
    val diag3: List[List[Int]] = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 1))
    val empty: List[Nothing] = List()
   

Constructors of Lists

All lists are constructed from:

We use the cons operator because its the only way to add an element to a list. There is no .push method.

   
    val nums = 1 :: 2 :: 3 :: 4 :: Nil
   
   
    val nums = Nil.::(4).::(3).::(2).::(1)
   

Manipulating Lists

It’s possible to decompose lists with pattern matching:

   
    nums match {
      // Lists of `Int` that starts with `1` and then `2`
      case 1 :: 2 :: xs => …
      // Lists of length 1
      case x :: Nil => …
      // Same as `x :: Nil`
      case List(x) => …
      // The empty list, same as `Nil`
      case List() =>
      // A list that contains as only element another list that starts with `2`
      case List(2 :: xs) => …
    }
   

Insertion sort method:

   
    val cond: (Int, Int) => Boolean = (x:Int,y:Int)=>x<y
    def insert(x: Int, xs: List[Int]): List[Int] =
      xs match {
        case List() => x :: Nil
        case y :: ys =>
          if (cond(x, y)) x :: y :: ys
          else y :: insert(x, ys)
      }

    insert(2, 1 :: 3 :: Nil) shouldBe (1 :: 2 :: 3 :: Nil)
    insert(1, 2 :: 3 :: Nil) shouldBe (1 :: 2 :: 3 :: Nil)
    insert(3, 1 :: 2 :: Nil) shouldBe (1 :: 2 :: 3 :: Nil)
   

This method is interesting because the condition (cond boolean) to place an element is an anonymous function, and its body is another anonymous function (boolean). So we have to define its parameters x,y and integers and also its body as x<y. The (Int,Int) part do not define it, its only the cond type.

Common Operations on Lists

.map(anonymous func)

.filter(anonymous func)

.flatMap{anonymous func}

Optional Values

Use options when you don’t have a defined return type. Partially defined functions:

   
    def sqrt(x: Double): Option[Double] =
      if (x < 0) None else Some(…)
   

Manipulating Options

Use pattern matching to decompose options:

   
    def foo(x: Double): String =
      sqrt(x) match {
        case None => "no result"
        case Some(y) => y.toString
      }
   

Common Operations on Options

.map(=>)

.filter(=>)

.flatMap(=>)

Error Handling

Try[A], represents a computation that attempted to return an A. Can be Success[A] or Failure;

None x Failure: Failure provide a reason of the failure.

Try[A] also have map, filter ,and flatMap

Either[A,B], represents a value that can either be type A or type B, and can be decomposed in two cases: Left and Right.

   
    def sqrt(x: Double): Either[String, Double] =
      if (x < 0) Left("x must be positive")
      else Right(…)
   

Either[A,B] have map and flatMap, but it transforms the Right case only. “right biased”.

Either[A,B] has a filterOrElse method, it transforms a Right value into Left value if it does not satisfy a given predicate.

Prior to Scala 2.12, Either was unbiased, so you had to specify the side you wanted to transform.

Syntactic Conveniences

String Interpolation

   
    def greet(name: String): String =
      s"Hello, $name!"

    greet("Scala") shouldBe "Hello, Scala!"
   

do not forget to prefix the string literal with “s”!!!

If you want to splice a complex expression:

   
    def greet(name: String): String =
      s"Hello, ${name.toUpperCase}!"

    greet("Scala") shouldBe "Hello, SCALA!"
   

IMPORTANT! Strings on Scala are different from ruby, which accepts both ‘’ and “”. On Scala always use “” for strings.

Tuples

Work as a conjunction of elements, and they can have different types. More generally, a type (T1, …, Tn) is a tuple type of n elements whose ith element has type Ti.

You can retrieve the elements of a tuple using tuple pattern, or you can identify the member by its position (_i)

   
    val is: (Int, String) = (42, "foo")
    is._1 shouldBe 42
    is._2 shouldBe "foo"
   

Functions as Objects

Functions are objects with apply methods.

   
    package scala
    trait Function1[A, B] {
      def apply(x: A): B
    }
   

Expansion of Function Values

   
    {
      class AnonFun extends Function1[Int, Int] {
        def apply(x: Int) = x * x
      }
      new AnonFun
    }
   

Expansion of Function Calls

f(a,b) = f.apply(a, b)

For Expressions

Map

   
    /* xs.map(x => x + 1) */
    for (x <- xs) yield x + 1
   

Filter

   
    /* xs.filter(x => x % 2 == 0) */
    for (x <- xs if x % 2 == 0) yield x
   

flatMap

   
    /* xs.flatMap(x => ys.map(y => (x, y))) */
    for (x <- xs; y <- ys) yield (x, y)
   

Method's Parameters

   
    Range(1, 10, 2)
   

Can be rewritten:

   
    case class Range(start: Int, end: Int, step: Int)
    Range(start = 1, end = 10, step = 2)
   

It’s to set default values when defining the constructor, so the parameter can be omitted.

    
     case class Range(start: Int, end: Int, step: Int = 1)
    
   

with an undefined number of parameters:

    
     def average(x: Int, xs: Int*): Double =
      (x :: xs.to[List]).sum.toDouble / (xs.size + 1)
    
   

Type Aliases

   
    type Result = Either[String, (Int, Int)]
    def divide(dividend: Int, divisor: Int): Result =
      if (divisor == 0) Left("Division by zero")
      else Right((dividend / divisor, dividend % divisor))
    divide(6, 4) shouldBe Right((1, 2))
    divide(2, 0) shouldBe Left("Division by zero")
   

Object-Oriented Programming

Classes

A class defines a new constructor and a new type.

Objects

Objects are the elements of a class type. Use the operator ‘new’ to create an object.

Members of an Object

Just like a property of a Ruby object, you can get the member of an object with the infix operator ‘.’

Methods

A package of functions operating on a data abstraction in the data abstraction itself. i.e. defined inside the class definition.

Data Abstraction

Self Reference

‘this’ represents the object on which the current method is executed.

Preconditions

require is used to enforce a precondition on the caller of a function.

require function:

   
    class Rational(x: Int, y: Int) {
      require(y > 0, "denominator must be positive")
      ...
    }
   

Assertions

assert is used to check the code of the function itself.

   
    val x = sqrt(y)
    assert(x >= 0)
   

Constructors

Operators

As said before, operators are methods with symbolic identifiers. So you can override the operators for some types:

   
    class Rational(x: Int, y: Int) {
      private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
      private val g = gcd(x, y)
      def numer = x / g
      def denom = y / g
      def + (r: Rational) =
        new Rational(
          numer * r.denom + r.numer * denom,
          denom * r.denom
        )
      def - (r: Rational) = ...
      def * (r: Rational) = ...
      ...
    }
   

Precedence Rules

Increasing priority:

   
    (all letters)
    |
    ^
    &
    < >
    = !
    :
    + -
    * / %
    (all other special characters)
   

Abstract Classes

No instances of an abstract class can be created with the operator new.

Class Extensions

subclasses extends superclass:

   
    class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet {
   

NonEmpty is a subclass and IntSet is its superclass. IntSet is a subclass of Object.

Implementation and Overriding

   
    abstract class Base {
      def foo = 1
      def bar: Int
    }

    class Sub extends Base {
      override def foo = 2
      def bar = 3
    }
   

Object Definitions

When you need a single instance of a class (Empty for IntSet), you can create a singleton object named Empty. No other instances of it can be created. This type of object are values.

   
    object Empty extends IntSet {
      def contains(x: Int): Boolean = false
      def incl(x: Int): IntSet = new NonEmpty(x, Empty, Empty)
    }
   

Traits

use when you want to inherit code from more than one superclass. Defined just like an abstract class.

   
    trait Planar {
      def height: Int
      def width: Int
      def surface = height * width
    }
   

OBS: classes, objects, and traits can inherit from one class, but from many traits:

   
    class Square extends Shape with Planar with Movable …
   

Imperative Programming

The substitution model of computation cannot be used. The state of an object depend on its history. Use ‘var’ to create a variable definition. Use ‘=’ to assign a new value to a variable.

State in Objects

Stateful objects Objects with state have some variable members. Ex: a Bank Account

Operational Equivalence

After a sequence of testing, and the results are the same, there is an operational equivalence.

Imperative Loops

while loops:

   
    def power(x: Double, exp: Int): Double = {
      var r = 1.0
      var i = exp
      while (i > 0) { r = r * x; i = i - 1 }
      r
    }
   

for-loops:

    
     for (i <- 1 until 3) { System.out.print(i + " ") }
    

Classes vs Case Classes

Creating instances:

  
   val aliceAccount = new BankAccount
   val c3 = Note("C", "Quarter", 3)
  

The same definition of BankAccount(class) create different objects (the notion of identity), and the same definition of Note(case class) lead to equal values.

Pattern matching does not work in regular classes.

A case class cannot extend another case class.

A case class is a special case of a class, with predefined overriding on some properties(parameters are promoted to members; Equality redefinition; Java hashCode redefinition; toString redefinition; Create a copy of a case class; Constructor that allows the omission of the new keyword; Extractor for pattern matching).

Polymorphic Types

Used in order to write generic code (for values of different types) without compromising static typing richness.

Upper Bounds

A <: B means: A is a subtype of B, and

A >: B means: A is a supertype of B, or B is a subtype of A.

Lower Bounds

A >: Reptile

Lazy Evaluation

Delayed Evaluation

Streams are similar to lists, but their tail is evaluated only in demand.

Defining Streams

   
    val xs = Stream.cons(1, Stream.cons(2, Stream.empty))
   

Stream Ranges

   
    def streamRange(lo: Int, hi: Int): Stream[Int] =
      if (lo >= hi) Stream.empty
      else Stream.cons(lo, streamRange(lo + 1, hi))
   

It returns a single Stream type object with start as a head element. The others are only computed when needed.(tail called on stream)

:: for lists

::# for streams

on Stream cons the second parameter(tl) is evaluated by call-by-name rules(=>)

The other methods are the same as List (filter, map, flatMap)

Lazy Evaluation

if the tail is called many times, it will be recomputed each time. To avoid this we must store the result of the first evaluation of the tail.

   
    lazy val x = expr
   

Lazy vals and streams

Using a lazy value for the tail, Stream.cons can be implemented more efficiently:

   
    def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] {
      def head = hd
      lazy val tail = tl
      …
    }
   

Nice exercise

   
    val builder = new StringBuilder
    val x = { builder += 'x'; 1 }
    lazy val y = { builder += 'y'; 2 }
    def z = { builder += 'z'; 3 }
    z + y + x + z + y + x
    builder.result() shouldBe "xzyz"
   

Type Classes

How to use insertionSort for element types other than Int? ‘<’ operator not defined for arbitrary types…

Parametrize lessThan function (comparison operation)

But there is a class in the standard library that represents orderings:

scala.math.Ordering

We can define insertionSort with ordering, but it is too repetitive.

Use Implicit Parameters

  
   def insertionSort[T](xs: List[T])(implicit ord: Ordering[T]): List[T] = {
     def insert(y: T, ys: List[T]): List[T] =
       … if (ord.lt(y, z)) …

     … insert(y, insertionSort(ys)) …
   }
  

Hey, thanks for reading :D ! Any thoughts on this topic? Comment below! ;)