Created by John Murray
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.We'll cover some basic constructs and concepts that you should expect to see in everday Scala code:
The for expression is a common pattern for iterating over data in a collection. It is very similar to Java's foreach construct.
ArrrayList<String> list = new ArrayList<String>(3); list.add("a"); list.add("b"); list.add("c"); // ... for (String l : list) { System.out.println(l); }
val list = List("a", "b", "c") for (l <- list) { println(l) }
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.
val list = List("a", "b", "c") list.foreach(l => println(l))
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
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.
val list = List("a", "b", "c") list.foreach(println)
Looks the same as the foreach but is returns a collection of the results of the function f.
We want to take a series of number and find their squares.
2 -> 4
3 -> 9
...
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)); }
val list = List(2, 3, 4) val sqrs = list.map(x => x * x)
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 } }
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.
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?
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; }
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.
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.
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.
class Option[A] { def map[B](f: A => B): Option[B] = { if (isEmpty) None else Some(f(this.get)) } }
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
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.
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.
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 }
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.
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!
class Option[A] { def flatMap[B](f: A => Option[B]): Option[B] = { if (isEmpty) None else f(this.get) } }
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 => } }
Interesting right?
But like map is not just for collections, flatMap is not just for Option types. It too can be applied to collections.
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", ...);
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", ...)
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!
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 => } }
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
Rule #1
for (p <- e) yield e′
expands to
e.map(p => e′)
Rule #2
for (p <- e; p′ <- e′ ...) yield e′′
expands to
e.flatMap { p => for (p′ <- e′ ...) yield e′′ }
Rule #3
for (p <- e) e′
expands to
e.foreach(p => e′)
Rule #4
for (p <- e; p′ <- e′ ...) e′′
expands to
e.foreach { p => for (p′ <- e′ ...) ′ }
The Try is a mechanisms for handling exceptions in Scala in a functional way.
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.
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") }
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!)") }
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!)") }
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.
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.
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.
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) }
Case classes offer a simple way to do POJOs as well as pattern-match against custom classes.
All public fields in case class must be defined in constructor
case class User(firstName: String, lastName: String, age: Int ...)
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
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") # ... }
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"
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)
By John Murray