CSE-4/562 Spring 2021 - Scala Introduction

Scala Introduction

CSE-4/562 Spring 2021

Feb 4, 2021

News

Checkpoint 0 will be posted tonight or tomorrow.

Class actually starts at 12:45 (but I'll be around from 12:30 to answer questions).


My assumptions

(assuming you don't already know Scala)

  • You know Java (or at least some other object oriented language like C++ or C#)
  • You are familiar with the JVM ecosystem (Java, Kotlin, JRruby, Jython, etc...)

This lecture will focus on syntax.

Some Terms

Package
An organizational unit clustering related functionality
Class
A group of related code that applies to entities of a type (Like Java)
Object
A 'singleton' class. (Like Java's static)
Case Class
A class with bonus features (we'll discuss shortly).

Hello World


  package edu.buffalo.myapp
  import java.io.File
  import scala.io._

  object MyApp
  {
    val message: String = "Hello World"
    def main(args: Array[String]): Unit = {
      println(message)
      var stream: Stream = Stream.fromFile(new File("Hello.txt"))
      for(line <- stream.getLines){
        println(line)
      }
    }
  }
    

  package edu.buffalo.myapp
    
Package definitions are exactly like Java.

  import java.io.File
    
Import statements bring classes into the namespace, making them available for use.

  import scala.io._
    
'_' acts like a wildcard in scala.
This is like import scala.io.* in Java.

  import java.io.{ File, FileInputStream }
    
Curly braces group class names together as a shorthand when you import multiple classes from the same package.

  object MyApp { 
    
Objects in scala are "singletons". They act like Java classes with only static methods. E.g., you could call MyApp.main(...)

  class MyClass(name: String, age: Int) { 
    
Classes are defined much like java, except constructor fields are given directly in the class definitions. e.g., you would instantiate this class as new MyClass("Bob", 102).

  class MyClass(name: String, age: Int) 
    extends MyAncestorClass 
    with MyTrait
    with MyOtherTrait { 
    
Inheritence is defined with the extends keyword. Like Java, Scala only allows single inheritance, but you add interfaces and mixins through the with keyword.
Objects can also use the extends and with keywords.

  trait MyTrait { 
    ...
  }
    

Traits are (almost) like what Java calls interfaces.

Unlike Java, a trait can define methods with implementations.


  sealed trait MyTrait { 
    ...
  }
    

Sealed traits can only be extended/with-ed within the same file.


  val message: String = "Hello World"
    

Variables are defined by the val or var keywords. val variables are immutable (like Java's final keyword).

Anywhere a variable is declared, it may be followed by a colon and a type. If omitted, Scala will guess.

Generally prefer to use val wherever you can.


  args: Array[String]
    
Generic types use square brackets ([]). This is like Java's angle brackets (<>)

    def main(args: Array[String]): Unit = {
    

Define functions with the def keyword.

The Colon-Type syntax is used to define the return type. Unit is like Java's void.

The last line of the function will be returned by default, but you can use return to return a value sooner.

Don't forget the =


      for(line <- stream.getLines){
        ...
      }
    
This is scala's iterator syntax (Like Java's for(line : stream.getLines)

      stream.getLines.foreach { line => 
        ...
      }
    
This is another way to write the same thing.
{ line => ... } is a lambda function
with line as a parameter.

  class Foo(bar: String) {
    def apply(baz: String): String = 
      { bar + " " + baz }  
  }
    
The special function apply is used to let a class instance (or object) pretend to be a function.

  val myFoo = new Foo("Abe")
  val result = myFoo("Lincoln")
  println(result)
    
prints Abe Lincoln

Collections

Scala has a robust library of collection types. Collections are usually referenced by their role.

Collections are immutable by default, and already in the namespace (no more import java.util.*).

Mutable collections live in the collections.mutable package if needed.

  • Seq[T]: An ordered sequence of items of type T.
  • IndexedSeq[T]: An ordered sequence of items with O(1) access to individual elements of type T.
  • Set[T]: An unordered collection of unique elements of type T.
  • Map[K,V]: A map from unique keys of type K to values of type V.
  • Iterator[T]: A stateful, usually non-repeatable traversal of some collection of elements of type T
  • Option[T]: A 0 or 1 element collection of type T
For example List[T] and Array[T] are both Seq[T], but only the latter is also an IndexedSeq[T].
You almost never create collections of a specific type. The following all create collections:

      val seq = Seq[Int](1, 2, 3, 4)
      val iseq = IndexedSeq[Int]("Alice", "Bolesław", "Coreline")
      val map = Map(
        "Cookie" -> "Chocolate Chip",
        "Cake" -> "Red Velvet",
        "Confection" -> "Gulab Jamun"
      )
      val opt = if(yes) { Some("A thing") } else { None }
    

Scala uses round brackets to access collection elements (remember apply?).


  println(seq(1))
  println(iseq(2))
  println(map("Cookie"))
    
prints 2, Coreline, and Chocolate Chip

Tuples


  val a                               = (1, "Cookie", "Alice")
  val b: (Int, String, String)        = (2, "Cake", "Bolesław")
  val all: Seq[(Int, String, String)] = Seq(a, b)
    

Scala also has a "Tuple" type (like Python).

The type is also parenthesized. The elements above would have type (Int, String, String)

Access elements of a tuple by a._1, a._2, and so forth.
For example all(1)._2 is "Cake"

Hint: x -> y is shorthand for (x, y). Use this with Map constructors.
  • .toMap: Convert any collection of 2-tuples to a Map.
  • .toSeq: Convert any collection into a sequence.
  • .toIndexedSeq: Convert any collection into an indexed sequence.
  • .toSet: Convert any collection into a set.

So how about those immutable collections...

Why does a collection need to be mutable?

Common Patterns...


  public int add_one(collection: List<Int>) {
    ArrayList<Int> result = new ArrayList<Int>();
    for(element : collection){
      result.append(element + 1)
    }
    return result
  }
    
.map { v => ... }: Get a new collection by transforming every element of the collection using the lambda.

  collection.map { x => x+1 }
    

Common Patterns...


  public int only_big(collection: List<Int>) {
    ArrayList<Int> result = new ArrayList<Int>();
    for(element : collection){
      if(element > 100){
        result.append(element)
      }
    }
    return result
  }
    
.filter { v => ... }: Get a new collection by deleting every element of the collection on which the lambda returns false.

  collection.filter { _ > 100 }
    

  all.filter { x => x._2.equals("Cookie") }
     .map { x => x._3 }
    
Returns Seq("Alice")

Common Patterns...


  public int flatten(collection: List<List<Int>>) {
    ArrayList<Int> result = new ArrayList<Int>();
    for(nested : collection){
      for(element : nested){
        result.append(element)
      }
    }
    return result
  }
    
.flatten: Assuming the target is a collection of collections, get a new collection by concatenating all of the nested collections.

  collection.flatten
    

Common Patterns...


  public int sum(collection: List<Int>) {
    int accum = 0;
    for(element : collection){
      accum += element
    }
    return accum
  }
    
.foldLeft(x) { (accum, v) => ... }: Start with x. Apply the lambda to (x, firstElement) to get a new x. Repeat for every element of the target and return the accumulator.

  collection.foldLeft(0) { (accum, element) => accum + element }
    

  collection.foldLeft(0) { _ + _ }
    

  foo match {
    case "bar" => println("It was bar")
    case "baz" => println("Baz be here")
    case x => println("It was actually "+x+" the whole time!")
  }
    

match is like a switch statement in C/C++, or an elif chain in Python...

... but far more powerful.


  val longFoo = foo match {
                  case x:String => x.toLong
                  case y:Int    => y * 100l
                  case _ => println("giving up!")
                } 
    
You can match based on type.

  val longFoo = foo match {
                  case (x, y) => x.toLong
                  case y:Int  => y * 100l
                  case _ => println("giving up!")
                } 
    
You can match based on tuple nesting.

Case Classes


  case class Foo(a: String, b: Int)
  case class Bar(foo: Foo, baz: String)
    

Case classes give you a bunch of stuff for "free"


  val myFoo = Foo("Abe", 1)
    

For example, you don't need to use "new" to construct one

and accessors are defined for all of the constructor variables.


  val name = bar match { 
                case Foo(name, id) => name
             }
   

But the big perk is that you can use them in match blocks.


  val name = bar match { 
                case Bar(Foo(name, id), baz) => name
             }
   

... even nested.

Scala uses case classes to make implementing union types easy.


  sealed trait MyFooOrBar
  case class Foo(a: String, b: Int) extends MyFooOrBar
  case class Bar(foo: Foo, baz: String) extends MyFooOrBar
   

Scala's compiler will warn you if you have a match block for a sealed trait that doesn't match every possible case.

SBT

Scala relies on the Scala Build Tool for compilation. It's similar to Maven.

  • build.sbt: Project definition
  • src/: Source code
    • main/: Production code
      • scala/: Scala code
        • package/package/class.scala
    • test/: Testing code
      • scala/: Scala code
        • package/package/testClass.scala
One class should have an object with a method defined as: def main(args: Array[String]).

A simple build.sbt


name := "myproject"
version := "0.1"
organization := "edu.buffalo.cse.odin",
    

Testing

Specs2 is my unit testing-framework of choice.


class HelloWorldSpec extends Specification {
  "This is a specification for the 'Hello world' string".txt

  "The 'Hello world' string should" >> {
    "contain 11 characters" >> {
      "Hello world" must haveSize(11)
    }
    "start with 'Hello'" >> {
      "Hello world" must startWith("Hello")
    }
    "end with 'world'" >> {
      "Hello world" must endWith("world")
    }
  }
}

    

Many IDEs provide SBT integration. See the Scala Getting Started Page and Scala Metals Page for more details.

If you prefer a text editor, check out Bloop.