Practical Use Cases of Switch Statement in Scala

1. Catch blocks are Pattern Matches and is same as below code

    try {
        // logic
    } catch {
        case e: RuntimeException => "runtime"
        case e: NullPointerException => "npe"
        case _ => "something else"
    }

    // Catch blocks are Pattern Matches and is same as below code
    /*
    try {
        // logic
    } catch(e) {
    e match {
        case e: RuntimeException => "runtime"
        case e: NullPointerException => "npe"
        case _ => "something else"
        }
    }
     */

2. Generators are also based on pattern matching

val list = List(1, 2, 3, 4)
    val evenOnes = for {
        x <- list if x % 2 == 0 // GENERATOR
    } yield x
    println(evenOnes)

    val tuples = List((1, 2), (2, 3))
    val filterTuples = for {
        (first, second) <- tuples // extractor in this generator
    } yield first * second
    println(filterTuples)

3. Multi value definitons are based on PMs

val tuple = (1, 2, 3)
    val (a, b, c) = tuple
    println(b)

    val head :: tail = list
    println(head)
    println(tail)

4. Partial Functions are based on Pattern Matching

    val mappedList = list.map {
        case v if v % 2 == 0 => s"$v is even"
        case 1 => "One"
        case _ => "Something else "
    }
    println(mappedList)
    // Note: Partial Function is a literal inside {}

    // is same as
    val mappedList2 = list.map(x => x match {
        case v if v % 2 == 0 => s"$v is even"
        case 1 => "One"
        case _ => "Something else "
    })

Switch Cases and Pattern Matching in Scala

In scala switch is enabled via Pattern Matching (PM) and its much more powerful than other languages often referred as “Switch on Steroids”
Some of the properties of switch case are
1. Cases are match in the same order they are written
2. If no cases match it will result in MatchError, make sure all the cases are covered with wildcard
3. Type of Pattern Matching expression is the unified type of all the types from all the cases
4. PM works really well with case classes

Simple switch statement

val random = new Random
    val x = random.nextInt(10)

    val description = x match {
        case 1 => "First element"
        case 2 => "Second element"
        case 3 => "Third element"
        case _ => "Default element" // this is a WILDCARD which matches for all other patterns
    }
    println(description)

Interesting property of Pattern Matching is that it can decompose values especially when used with case classes.

sealed class Animal
case class Dog(breed: String) extends Animal
case class Parrot(greeting: String) extends Animal

val animal: Animal = Dog("terra Nova")
animal match {
    case Dog(breed) => println(s"Matched dog of type $breed breed")
}

Different scenarios/ways we can use switch statement

    // 1. Constants
    val x: Any = "Scala"

    val constants: Any = x match {
        case 1 => "a number"
        case "Scala" => "The scala"
        case true => "The Truth"
        case a02AllThePatterns => "A singleton Object"
     }
    println(constants)

    // 2. Match Anything
    // 2.1 Wildcard
    val matchAnything = x match {
        case _ => "I will match everything"
    }
    println(matchAnything)

    // 2.2 variable
    val matchAVariable = x match {
        case something => s"I have found $something"
    }
    println(matchAVariable)

    // 3 tuples
    val aTuple = (1, 2)
    val matchATuple = aTuple match {
        case (1, 1) => "Found tuple"
        case (something, 2) => s"I have found tuple with $something"
    }
    println(matchATuple)

    // PMs can be nested
    val nestedTuple = (1, (2, 3))
    val matchANestedTuple = nestedTuple match {
        case (_, (2, v)) =>
    }
    println(matchANestedTuple)

    // 4. case classes - constructor pattern
    // PMs can be nested with CCs as well
    val aList: GenericList[Int] = new Cons(1, new Cons(2, Empty))
    val matchAList = aList match {
        case Empty =>
        case Cons(h, t) =>
        case Cons(h, Cons(subHead, subTail)) =>  // nested classes
    }

    // 5 - List Patterns
    val aStandardList = List(1, 2, 3, 4, 5)
    val matchAStandardList = aStandardList match {
        case List(1, _, _, _) => // extractor concept in advanced training
        case List(1, _*) => // List of arbitrary length in advanced training
        case 1 :: List(_) => // infix pattern
        case List(1, 2, 3) :+ 42 => // infix pattern
    }

    // 6. Type specifiers
    val unknown: Any = 2
    val matchAUnknown = unknown match {
        case list: List[Int] => // explicit type specifier
        case _ =>
    }

    // 7. Name Binding
    val MatchANameBinding = aList match {
        case nonEmptyList @ Cons(_, _) => 
            // name binding allows us to use the name later
            // Its like variables but we can name entire pattern here and its very powerful
        case Cons(1, rest @ Cons(2, _)) => 
            // Name Binding is also possible in nested patterns
    }

    // 8. Multi Patterns
    val matchAMultiPattern = aList match {
        case Empty | Cons(0, _) => // Matches multiple patterns with pipe operator
    }

    // 9. if guards
    val matchASpecialElement = aList match {
        case Cons(specialElement, _) if specialElement % 2 == 0 => 
        // if guard on a variable
    }

Options and Exceptions in Scala

Option is solution to a Billion Dollar Mistake introduced by Null Pointer Exceptions. Options deal with unsafe APIs and never do null checks.
As .get is a unsafe operation, we should rely on map/flatMap to read the values from options.
Since we can use map, flatMap on options, we can also use for comprehension.

Scala Implementation Classes

        sealed abstract class Option[+A]
        case class Some[+A](x: A) extends Option[A]
        case object None extends Option[Nothing]

Code Sample

     val config: Map[String, String] = Map(
             "age" -> "36",
             "gender" -> "Male"
         )

         class Person {
             def getPerson = println("person created")
         }
         object Person {
             val random = new Random(System.nanoTime())

             def apply(age: String, gender: String): Option[Person] =
                 if(random.nextBoolean()) Some(new Person)
                 else None
         }

         // by default .get on map returns option
         val age = config.get("age")
         val gender = config.get("gender")
         // NOTE: age.get is unsafe operation and hence we should only rely on map/flatMap to get the value from options
         /*
             if (age != null && gender != null)
                 return Person(age, gender)
             else
                 return null
          */
         val person = age.flatMap(a => gender.flatMap(g => Person(a, g)))
         /*
             if (person != null)
                 return person.getPerson
             else
                 return null
          */
         val personStatus = person.map(c => c.getPerson)
         /*
             Same as
             if (status != null)
                 println(status)
          */
         personStatus.foreach(println)

         // shorthand chained solution
         config.get("age")
           .flatMap(h => config.get("gender")
             .flatMap(p => Person(h, p))
             .map(c => c.connect))
           .foreach(println)

         // for comprehensions: Instead of using chained or multiple map/flatmaps we can use this for readability (much widely used and more readable)
         // If any of the the for statements in None, then entire for-yield will return None
         val forPersonStatus = for {
             age <- config.get("age")
             gender <- config.get("gender")
             Person <- Person(age, gender)
         } yield Person.connect

         forPersonStatus.foreach(println)

Exceptions
In imperative languages like Java, as number of try blocks increases, program becomes barely readable
Scala provides a way to avoid countless try-catch blocks

Scala Implementation Classes

        sealed abstract class Try[+T]
        case class Failure[+T](t: Throwable) extends Try[T]
        case class Success[+T](value: T) extends Try[T]

Code Sample

     val aSuccess = Success(3)
         val aFailure = Failure(new RuntimeException("Not valid"))
         println(aSuccess)
         println(aFailure)

         // unsafe methods
         def unsafeMethod = throw new RuntimeException("Unsafe method invoked")
         // Try with apply method is going to catch the exception and wrap it in Failure object
         val potentialFailure = Try(unsafeMethod)
         println(potentialFailure)
         println(potentialFailure.isSuccess) // tells whether exception is thrown or not

         // orElse
         def backUpMethod(): String = "A valid result"
         val fallbackTry = Try(unsafeMethod).orElse(Try(backUpMethod))
         println(fallbackTry)

         // design the apis in better way
         def betterUnsafeMethod: Try[String] = Failure(new RuntimeException("Better Unsafe method"))
         def betterBackupMethod: Try[String] = Success("Better valid result")
         val betterFallback = betterUnsafeMethod orElse betterBackupMethod
         println(betterFallback)

         // map, flatMap, filter
         println(aSuccess.map(_ * 3))
         println(aSuccess.flatMap(x => Success(x * 10)))
         println(aSuccess.filter(_ > 10)) // this will turn success into failure

         // since map, flatMap, filter are available so will be for-comprehension

Collections, Tuples and Maps

Traverable: It is the root trait of all collections and offers very important methods
maps: maps, flatMap, collect
conversions: toArray, toList, toSeq
size info: isEmpty, size, nonEmpty
tests: exists, forAll
folds: foldLeft, foldRight, reduceLeft, reduceRight
retrieval: head, find, tail
string ops: mkString

By default we use immutable collections in scala

package object scala {
    type List[+A] = immutable.List[A]
}

object Predef {
    type Map[A, +B] = immutable.Map[A, B]
    type Set[A] = immutable.Set[A]
}

Note: By default Seq apply method returns list
val aSeq = Seq(1, 3, 4, 2) // returns List
println(aSeq(2)) // overloaded apply method to get value at this index = prints 3

Tuples
Tuple is an infinite ordered list. It has copy method similar to case classees.

    val aTuple = new Tuple2[Int, String](2, "Hello Scala") // Tuples can be extended to 22 different parameters to be in consistent with functions
    // is same as
    val aTuple1 = Tuple2(2, "Hello Scala")
    val aTuple2 = (2, "Hello Scala")
    val aTuple3 = (2 -> "Hello Scala")

    println(aTuple._1)
    println(aTuple.copy(_2 = "Good Bye")) // copy methods similar to case classes
    println(aTuple.swap) // swaps key value positions

Maps

    val phoneBook = Map(("Jim", 555), ("Niran" -> 999)).withDefaultValue("-1")
    println(phoneBook)
    println(phoneBook.contains("Niran"))
    println(phoneBook("Mary")) // throws error if key is not found

    // add new pairing
    val newPairing = ("Mary" -> 909)
    val newPhonebook = phoneBook + newPairing
    println(newPhonebook)

    // filterKeys
    println(phoneBook.filterKeys(_.startsWith("N")))
    // mapValues
    println(phoneBook.mapValues(number => "01245-" + number))
//    println(phoneBook.mapValues(number => number * 10))

    // conversions to other collections
    println(phoneBook.toList) // list of tuples
    println(List(("Niran" -> 57687)).toMap)

    val names = List("Niran", "Naveen", "Jim", "John")
    println(names.groupBy(_.charAt(0)))

map, flatMap, filter and for comprehension in Scala

.map
When map is applied on some collection of type A, it returns a new collection of the same type with elements of type B.

    val list = List(1,2,3) // this is calling apply method on companion object
    println(list)
    // map
    println(list.map(_ + 1)) // returns List (same type) of type Int
    println(list.map(_ + " is a number")) // returns List (same type) of type String

.filter
Takes a lambda that takes one param and returns boolean. Values are filtered when boolean is true.

  println(list.filter(_ % 2 == 0)) // prints 2

.flatMap
flatten (reduces) the hierarchy by one level each time it is applied

    val toPair = (x: Int) => List(x, x+1)
    println(list.flatMap(toPair))

Example with both map and flatMap

val numbers = List(1, 2, 3, 4)
    val chars = List('a', 'b', 'c', 'd')
    val colors = List("White", "Black")
    val combinations = numbers.flatMap(n => chars.flatMap(c => colors.map(co => c + "" + n + " " + co)))
    println(combinations)

for comprehensions
since the above maps and flatMaps are difficult to read, we use for comprehensions and the compiler will transform the for-comprehension to maps and flatMaps for us

    val forCombinations = for {
        n <- numbers
        c <- chars
        co <- colors
    } yield "" + c + "" + n + " " + co
    println(forCombinations)

Higher Order Functions (HOFs) and Currying In Scala

Higher Order Functions This takes functions as input parameters and return another function as an output aka HOFs
Note: map, flatMap, filter belongs to HOFs as they take function as an arg
Example

val superFunction: (Int, (String, (Int => Boolean)) => Int) => (Int => Int) = null
// Input: (String, (Int => Boolean)) => Int) // function within a function
// Output: (Int => Int) // another function which takes Int and returns Int

Currying Functions
Function that applies a given function ‘n’ times over a given value ‘x’

  // nTimes(F, n, x)
      // nTimes(f, 3, x) = f(f(f(x))) = nTimes(f, 2, f(x))

      def nTimes(f: (Int => Int), n: Int, x: Int): Int = {
          if (n == 0) x
          else nTimes(f, n-1, f(x))
      }

      val adder: Int => Int = _ + 1
      val result = nTimes(adder, 10, 0)
      println(result)

      // better way of writing it is to return a function that can compute the value
      // ntb(f, n) = x => f(f(f(....(x))))
      def nTimesBetter(f: Int => Int, n: Int): Int => Int = {
          if (n == 0) (x: Int) => x
          else (x: Int) => nTimesBetter(f, n-1)(f(x))
      }
      val plus10 = nTimesBetter(adder, 10)
      println(plus10(0))

Examples on how to consume and return curried functions

    def toCurry(f: (Int, Int) => Int): (Int => Int => Int) =
        x => y => f(x, y)   // lambda taking value 'x', returns a lambda taking value 'y' and the result is f(x, y)
    // It receives a function of type (Int, Int) => Int
    // It returns a curried function
    // Note: Looks simple to read but its very hard to think about

    def fromCurry(f: Int => Int => Int): (Int, Int) => Int =
        (x, y) => f(x)(y)

    // TEST
    // def superAdder2: (Int => Int => Int) = toCurry(_ + _) is same as below one
    def superAdder2: (Int => Int => Int) = toCurry((x, y) => x + y)
    def add4 = superAdder2(20)
    println(add4(7))

Scala supports another kind of curried function by specifying functions with MULTIPLE PARAMETER LISTS
Curried Formats are like PARTIAL FUNCTIONS in JS
Note: For functions with multiple parameters, if we want to define smaller functions later (Line:43) we have to define type on RHS, otherwise it will not compile.

def curriedFormatter(stringFormat: String)(value: Double): String = stringFormat.format(value)
val standardFormat: (Double => String) = curriedFormatter("%4.2f")
val preciseFormat: (Double => String) = curriedFormatter("%10.8f")
println(standardFormat(Math.PI))
println(preciseFormat(Math.PI))

Function Library (Function1 till Function22) has two very important functions that are very useful.

  • compose
  • fromThen
    // genericize the above two functions
    def compose[A, B, T](f: A => B, g: T => A): T => B = x => f(g(x))
    def fromThen[A, B, C](f: A => B, g: B => C): A => C = x => g(f(x))

    val add2 = (x: Int) => x + 2
    val times3 = (x: Int) => x * 3
    val composed = compose(add2, times3)
    println(composed(4)) // T=4, A=12, B=14 result is 14
    val ordered = fromThen(add2, times3)
    println(ordered(4)) // A=4, B=6, C=18 result is 18

Functions in Scala

As scala is functional programming we will be able to use functions as first class citizens (passing functions as arguments, returning functions as return type, assigning functions to variables…)
But the problem is that scala sits on top of JVM and JVM is designed for Object Oriented Programming. Scala solves this problem like this

trait MyFunction[A, B] {
  def apply(element: A): B
}
// Java way of implementing above function aka Anonymous Function
val doubler = new MyFunction[Int, Int] {
  // does not need 'override keyword for trait methods'
  def apply(e: Int): Int = e * 2
}
println(doubler(5)) // prints 10

For this reason, to enable developers make use of functional programming concepts, scala provided pre-defined traits/functions (Function1 till Function22) that can take up to 22 generic parameters.
Function1 takes 1 type as input (input param) and return 1 type as output (return type). For example, we can redefine above implementation like this

val scalaDoubler = new Function1[Int, Int] {
  def apply(a: Int) = a * 2
}
println("Scala Doubler: "+scalaDoubler(5))

Apply Method: This method is very special and allows us to call instances of classes or singleton objects like they were functions. And functions are actually instances of classes with apply method.
Anonymous Functions
In scala we can write anonymous function as LAMBDA.
Same Function1 above can be re-written as below different ways

val scalaDoubler1 = (a: Int) => a * 2
or
val scalaDoubler1: (Int => Int) = a => a * 2
or
val scalaDoubler1: (Int => Int) = _ * 2
println(scalaDoubler1(5))

Case Classes in Scala

Case Classes (CCs) are very handy and powerful in scala. These are loaded with lot of important features than normal classes that we may need majority of the times. Some of the features are

1. class parameters are promoted to class fields (no need of explicit val declaration)

    val p1 = new Person("ntalla", 26)
    println(p1.name)

2. Sensible toString is implemented

    println(p1) // gives readable object notation instead of some default hash address

3. equals and hashCode implementations: This reason makes case classes particularly important in collections

    val p2 = new Person("ntalla", 26)
    println(p1 == p2) // this will return true

4. Handy copy methods

    val p2Copy = p2.copy(name = "ntallapa")
    println(p2Copy)

5. CCs have companion objects

    val thePerson = Person 
    // this is the companion object that is created by case class
    // this system created companion object will also have some handy factory methods
    val ntalla = Person("ntalla1", 26) // this is apply method on companion object
    // companion apply method does the same thing as that of the class constructor

6. CCs are serializable
// this feature makes CCs very useful in distributed systems where objects are sent over the network across JVMs

7. CCs have extractor patterns = CCs can be used in PATTERN MATCHING (another very very powerful feature)

Anonymous Classes
Giving implementation without a name. Anonymous implementation can be done for Normal Classes, Abstract Classes and also Traits

  abstract class Animal {
      def eat: Unit
  }

  // giving implementation to abstract class without naming it - Anonymous
  val funnyAnimal: Animal = new Animal {
      override def eat: Unit = println("akdhasks")
  }

  // proof of instance on abstract class
  println(funnyAnimal.getClass)

  // We can also create anonymous class for normal classes 
  // as well along with traits and abstract classes
  class Person(name: String) {
      def sayHi: String = s"$name said hello"
  }

  val p1 = new Person("ntalla") {
      override def sayHi: String = s"${super.sayHi} and welcome"
  }

Generics, variance problems and bounded types

In scala, generics work for both classes and traits. We can have any number of generic type arguments.

class MyList[A] {
  def add(element: A): MyList[A] = ???
}
val listOfIntegers = new MyList[Int]
val listOfStrings = new MyList[String]

// we can have any number of generic type arguments
class MyMap[K, V]

Variance
It defines inheritance relationships of parameterized types. There are 3 types of variance

class Animal
class Cat extends Animal
class Dog extends Animal

Covariance: List[Cat] can extend from List[Animal]

    class CovariantList[+A] // Observe the symbol +
    val animal: Animal = new Cat
    val animalList: CovariantList[Animal] = new CovariantList[Cat]

Invariance: List[Cat] cannot extend from List[Animal]

  class InvarianceList[A]
  val invariantAnimalList: InvarianceList[Animal] = new InvarianceList[Animal]

Contravariance: List[Animal] can extend from List[Cat]

    class ContraVariantList[-A]
    val contraVariantList: ContraVariantList[Cat] = new ContraVariantList[Animal]
    // this may be confusing, look at the below example
    class Trainer[-A]
    val contraVariantList1: Trainer[Cat] = new Trainer[Animal]
    // Now this makes sense as animal trainer can also train cat

Bounded Types
These allow us to use generic classes only for certain types either subtype/supertype of certain types

SubType
    class Cage[A <: Animal](animal: A) // it says class Cage accepts
       // only of type A that are sub type of Animal
    val cage = new Cage(new Dog)
    class Car
    // val newCage = new Cage(new Car) // compiler does not 
       // report error but it gets runtime exception
SuperType
    // this enforces super type
    class Cage1[A >: Animal](animal: A)

Constructors, Inheritance, and Abstraction

Class Parameters vs Class Fields
Parameters passed in the class constructor cannot be accessed on the instance unless they are marked ‘val’ whereas
Fields can be accessed directly on the instance.

class Person(name: String, val age: Int) {
    // instance level functionality
    // name is Class Parameter
    // age is class Field
}
val person = new Person("ntallapa", 23)
println(person.age) // is valid because age is class field and hence can be accesses directly on the object
println(person.name) // Invalid as parameters cannot be directly accessed

Primary Constructor, Secondary/Auxiliary Constructors
Constructor defined inline with the class definition is the primary constructor.
Auxiliary constructors can only invoke primary constructors with some default values.

class Person(name: String, val age: Int) { // Primary Constructor
  def this(name: String) = this(name, 0) // Auxiliary constructor
}

If there is is mismatch in number of constructor args between parent and child class, we should mention it explicitly otherwise the JVM default behavior is to look at same number constructor in the parent class

// constructors
 class Person(name: String, age: Int)
 // class Adult(name: String, age: Int, idCard: String) extends Person
 // the above stmt will not work as there is no 3-arg constructor in Person
 class Adult(name: String, age: Int, idCard: String) extends Person(name, age) // is the correct way

Auxiliary constructors are really not much useful in Scala.

There are 3 Specifiers (conceptually similar to Java)

  • private
  • protected
  • none (public)

Inheritance
Single inheritance can be achieved via extends keyword (extending classes and abstract classes) and multiple inheritance is achieved via “with” keyword on Traits

Abstract Classes and Traits
Unlike in Java, we can have both abstract and non-abstract members in Abstract Class and also in Trait
Differences
traits cannot have constructor parameters
we can only extend one class but inherit multiple traits
traits are a type of a behavior whereas abstract class is a type of thing

What is a sealed class/trait in scala?

A sealed class/trait can only be extended within the same file and not possible to extent outside of the file.

Mawazo

Mostly technology with occasional sprinkling of other random thoughts

amintabar

Amir Amintabar's personal page

101 Books

Reading my way through Time Magazine's 100 Greatest Novels since 1923 (plus Ulysses)

Seek, Plunnge and more...

My words, my world...

ARRM Foundation

Do not wait for leaders; do it alone, person to person - Mother Teresa

Executive Management

An unexamined life is not worth living – Socrates

Diabolical or Smart

Nitwit, Blubber, Oddment, Tweak !!

javaproffesionals

A topnotch WordPress.com site

thehandwritinganalyst

Just another WordPress.com site

coding algorithms

"An approximate answer to the right problem is worth a good deal more than an exact answer to an approximate problem." -- John Tukey