Scala - Intro / – Refresher – For Expressions



Scala - Intro / – Refresher – For Expressions

0 0


scala-intro-refresher-presentation

A reveal.js presentation regarding some simple scala-isms to present to my team at AppNexus

On Github JohnMurray / scala-intro-refresher-presentation

Scala - Intro /

Refresher

Created by John Murray

Meh?

Questions you may have about Scala syntax or from the materials sent out before the class?

I will also take questions during the presentation on any syntax you may not understand, but I'd prefer not to spend too much time on that if possible.

Scala'isms

We'll cover some basic constructs and concepts that you should expect to see in everday Scala code:

  • for Expression
  • foreach
  • map
  • Option[T]
  • flatMap
  • for Comprehension
  • Try[T]
  • Case Classes

For Expressions

For Expressions

The for expression is a common pattern for iterating over data in a collection. It is very similar to Java's foreach construct.

Java Foreach

ArrrayList<String> list = new ArrayList<String>(3);

list.add("a");
list.add("b");
list.add("c");
// ...

for (String l : list) {
    System.out.println(l);
}

Scala For Expression

val list = List("a", "b", "c")
for (l <- list) {
    println(l)
}

Scala Foreach

Scala Foreach

Another construct for iterating over elements that allows you to provide a lambda responsible for performing what you would do otherwise in the body of the for expression.

Scala Foreach

val list = List("a", "b", "c")
list.foreach(l => println(l))

Scala Foreach

class List[A] {
    def foreach[B](f: A => B) {
        var these = this
        while (!these.isEmpty) {
            f(these.head)
            these = these.tail
        }
    }
}
Talk about the parameter f and how it reprsents a function that takes an A and returns a B

Scala Foreach

def println(x: Any) : Unit = Console.println(x)
The function println matches the parameter f in the foreach function. This means that we can use it instead of providing a lambda of our own if we wanted.

Scala Foreach

val list = List("a", "b", "c")
list.foreach(println)

Scala Map

Scala Map

Looks the same as the foreach but is returns a collection of the results of the function f.

Scala Map

We want to take a series of number and find their squares.

2 -> 4

3 -> 9

...

Scala Map - Java Version

ArrayList<Integer> list = new ArrayList<Integer>(3);

list.add(new Integer(2));
list.add(new Integer(3));
list.add(new Integer(4));

ArrayList<Integer> sqrs = new ArrayList<Integer>(3);

for (Integer n : list) {
    sqrs.add(new Integer(n * n));
}

Scala Map - Scala Version

val list = List(2, 3, 4)
val sqrs = list.map(x => x * x)

Scala Map

class List[A] {
    def map[B](f: A => B): List[B] = {
        var list = List[B]()
        var rest = this
        while (rest.length > 0) {
            list = f(rest.head) :: list
            rest = rest.tail
        }

        list.reverse
    }
}

Scala Option

Scala Option

The Option[T] class is used to represent values that may or may not exist. You would typically think of this as the null in Java.

Scala has no null except for interopt with Java.

Scala Option - Some / None

The Option[T] class has two sub-classes, Some[T] and None.

Some[T] represents the precense of a value where as None represents the absence of a value.

Using Option[T] instead of null enforced that you do a check first. How about an example?

Scala Option - Java null

User class:

class User {
    public String name;
    // ...
}

Load user from DB:

public function getUserInfo(int id) {
    if (db.hasUser(id)) {
        return db.getUser(id);
    }
    return null;
}

Scala Option - Java null

What happens when the user is not found?

public function printUser(int id) {
    System.out.println(getUserInfo(id).name);
    // NullReferenceException at '.name'
}

Instead we need to check for the null condition, but this is not enforced by Java at all. We are given the "freedom" to throw unexpected, runtime exceptions.

Scala Option

What would this look like in Scala?

case class User(name: String)

def getUserInfo(id: Int) : Option[User] = {
    if (db.hasUserId(id)) Some(db.getUser(id))
    else None
}

What happens now if the user is not found?

def printUser(id: Int) = getUserInfo(id) match {
    case Some(u) => println(u.name)
    case None    => println("No user found")
}
Because we are using an Option type, we cannot get to the actual value (if it even exists) without first checking and then extracting the value. The is enforced at compile time rather than runtime.

Scala Option - Map

Note that map is a function that applies a transformation to a given item in the form of A => B, meaning from type A to type B.

When applied to a collection, we go from a collection of A to a collection of B. But it doesn‘t always have to be applied to a collection.

Scala Option - Map

class Option[A] {
    def map[B](f: A => B): Option[B] = {
        if (isEmpty) None
        else Some(f(this.get))
    }
}

Scala Option - Map

case class User(lastName: String, firstName: String)

def printUser(id: Int)  = {
    val userName = getUserInfo(id).map(u => u.firstName + " " + u.lastName)
    userName match {
        case Some(n) => println(n)
        case None    =>
    }
}
While this may be a bit trivial right now, we will see how this is more useful later when we get to flatMap

Scala flatMap

Scala flatMap

flatMap is similar to map but is different in its method-signature.

def flatMap[B](f: A => Option[B]): Option[B]
This would be a good spot to do a little bit of white-boarding detailing the differences between map and flatMap. Ideally, start with a map operation and transform it into a flatMap operation.

Scala flatMap

This means that whatever is returned from the inner-function must return an Option type which is returned directly as the result.

Let‘s revise that last example and make it a bit more complex.

Scala flatMap

case class Car(driver: Option[User])
case class User(lastName: String, firstName: String)

def getCar(id: Int) : Option[Car] = {
    if (db.hasCarId(id)) Some(db.getCar(id))
    else None
}

Scala flatMap

def printDriver(id: Int)  = {
    val car : Option[Car] = getCar(id)

    val name : Option[Option[String]] = car.map { c: Car =>
        c.driver.map { d =>                 // Option[String]
            d.firstName + " " + d.lastName
        }
    }

    name match {                            // Option[Option[String]]
        case Some(n) => n match {           // Option[String]
            case Some(nn) => println(nn)    // String
            case None     =>
        }
        case None    =>
    }
}
Stick to using map for the, now more complicated, example.

Scala flatMap

This is no good obviously.

We need to convert an instance of Option[Car] to an instance of Option[String]. How do we do this?

flatMap of course!

Scala flatMap

class Option[A] {
    def flatMap[B](f: A => Option[B]): Option[B] = {
        if (isEmpty) None 
        else f(this.get)
    }
}

Scala flatMap

def printDriver(id: Int)  = {
    val car = getCar(id)

    val name : Option[String] = car.flatMap { c : Car =>
        c.driver.map { d =>                 // Option[String]
            d.firstName + " " + d.lastName
        }
    }

    name match {                            // Option[String]
        case Some(n) => println(n)          // String
        case None    =>
    }
}

Scala flatMap

Interesting right?

But like map is not just for collections, flatMap is not just for Option types. It too can be applied to collections.

Scala flatmap

Java Version

ArrayList<Integer> odd = new ArrayList<Integer>(4);
ArrayList<Integer> evn = new ArrayList<Integer>(4);

odd.add(1); odd.add(3); odd.add(5); odd.add(7);
evn.add(2); evn.add(4); evn.add(6); evn.add(8);

ArrayList<String> output = new ArrayList<String>(4 * 4);

foreach (Integer o : odd) {
    foreach (Integer e : even) {
        output.add(o.toString() + e.toString());
    }
}
# => output: ArrayList("12", "14", "16", "18", "32", "34", "36", ...);

Scala flatmap

val odd = List(1, 3, 5, 7)
val evn = List(2, 4, 6, 8)

odd.flatMap(o => evn.map(e => s"$o$e"))
# => output: List("12", "14", "16", "18", "32", "34", "36", ...)

Scala For Comprehensions

Scala For Comprehensions

While map and flatMap are great, they can be a bit hard to visually understand. Scala offers us a syntax that is a bit easier to reason about with the for comprehension.

On to the examples!

Scala For Comprehensions

Back to the car/driver example, this time using for-comprehensions:

def printDriver(id: Int)  = {
    val car = getCar(id)

    val name : Option[String] = for {
        c <- car                            // flatMap of Car
        d <- c.driver                       // map of User (driver)
    } yield(d.firstName + " " + d.lastName) 

    name match {                            // Option[String]
        case Some(n) => println(n)          // String
        case None    =>
    }
}

Scala For Comprehensions

Since this is really just a syntactic sugar, we should be able to expand this to just simple maps and flatMaps.

And so we shall

Scala For Comprehensions

Rule #1

for (p <- e) yield e′

expands to

e.map(p => e′)

Scala For Comprehensions

Rule #2

for (p <- e; p′ <- e′ ...) yield e′′

expands to

e.flatMap { p => for (p′ <- e′ ...) yield e′′ }

Scala For Comprehensions

Rule #3

for (p <- e) e′

expands to

e.foreach(p => e′)

Scala For Comprehensions

Rule #4

for (p <- e; p′ <- e′ ...) e′′

expands to

e.foreach { p => for (p′ <- e′ ...) ′ }

Scala Try

Scala Try

The Try is a mechanisms for handling exceptions in Scala in a functional way.

Scala Try

Try[T] has two concrete implementations, Success[T] and Failure

You obtain the value using a match and you can transform the values using map or flatMap.

Any operations performed within a map will always result in a Try object being returned. You will never escape the Try‘s safety.

Scala Try

An example

def liveDangerously() = {
    val r = new java.util.Random
    if (r.nextInt < 0) throw new Exception("whoops")
}

Try { liveDangerously } match {
    case Failure(ex) => println(ex.message)
    case _           => println("You made it out alive")
}

Scala Try

An example of map

def liveDangerously(attempt: Int) = {
    println("Attempt number: " attempt.toString)
    val r = new java.util.Random
    if (r.nextInt < 0) 
        throw new Exception("whoops (attempt #" + attempt.toString = ")")
}

val twoAttempts : Try[_] = Try { liveDangerously(1) }.map { r => 
    liveDangerously(2)
}

twoAttempts match {
    case Failure(ex) => println(ex.message)
    case _           => println("You made it out alive (twice!)")
}

Scala Try

An example of flatMap

def liveDangerously(attempt: Int) = {
    println("Attempt number: " attempt.toString)
    val r = new java.util.Random
    if (r.nextInt < 0) 
        throw new Exception("whoops (attempt #" + attempt.toString = ")")
}

val attemptOne : Try[_] = Try { liveDangerously(1) }
val attemptTwo : Try[_] = attemptOne.flatMap { res => 
    Try { liveDangerously(2) }
}

attemptTwo match {
    case Failure(ex) => println(ex.message)
    case _           => println("You made it out alive (twice!)")
}

Scala Try

public class Player {
    public DeadMonster KillMonster() {
        // throws exception if there is no monster found
    }
    public Treasure CollectTreasure(DeadMonster from) {
        // throws exception if you're dead
        // throws exception if monster is faking dead
    }
    public Item BuyRandomItem(Treasure t) {
        // throws exception if not enough gold
    }
}
Let us start more of a concrete-ish example. It is a bit contrived, but should work for our purposes.

Scala Try

This is very unsafe code

Player john = new Player();

DeadMonster monster = john.KillMonster();
Treasure treasure = john.CollectTreasure(monster);
Item item = john.BuyRandomItem(treasure);

But how do we require that you handle error conditions. And, more so, how do we tell you, from the type, that an exception could be thrown.

Scala Try

The Scala Version

class Player {
    def KillMonster(): Try[DeadMonster] = { /* ... */ }
    def CollectTreasure(from: DeadMonster): Try[Treasure] = { /* ... */ }
    def BuyRandomItem(t: Treasure): Try[Item] = { /* ... */ }
}

Now you can see, in the types, that an Exception could be thrown.

Scala Try

More-so, the code requires that you check for failure or success:

val john = new Player

val item = john.KillMonster
           .flatMap(dm => john.CollectTreasure(dm))
           .flatMap(t  => john.BuyRandomItem(t))

item match {
    case Failure(ex) => // handle error
    case Success(i)  => println("Acquired new item: " + i.toString)
}

Scala Case Classes

Scala Case Classes

Case classes offer a simple way to do POJOs as well as pattern-match against custom classes.

Scala Case Classes

All public fields in case class must be defined in constructor

case class User(firstName: String, lastName: String, age: Int ...)

Scala Case Classes

All fields specified in the constructor of a case-class are public and immutable.

val user = new User("John", "Murray", 23)

user.firstName = "Frank"  # => Will throw an exception

Scala Case Classes

All fields specified can be pattern-matched against

val user = new User("John", "Murray", 23)

user match {
    case User(fName, lName, age) => {
        println(s"$fName $lName is $age years old")
    }
    # ...
}

user match {
    case User(_, _, age) => println(s"You're $age")
    # ...
}

Scala Case Classes

In scala, any method called apply can be called on the object just like a function.

For example:

object SayHello {
    def apply() = println("hello there stranger")
}

SayHello()  # => "hello there stranger"

Scala Case Classes

Case classes have a default apply method that is created within the companion object.

So both are equally valid and result in the same action:

val me  = new User("John", "Murray", 23)

val you = User("Bob", "Jane", 37)

Done

go away

By John Murray