Scala Coursera Highlights

I recently completed the Functional Programming Principles in Scala course on Coursera. Along the way, I learned some interesting things ranging from small tidbits to major language features. I’m going to use this post as a chance to highlight and review my favorite things that I learned about Scala from the course. This is meant to be more of a quick review than a set of tutorials, so I’ll provide links to actual tutorials and further reading for each section.

Variance

One thing I liked was seeing abstract programming language concepts concretely being used in Scala. An example of this is the idea of variance, namely covariance and contravariance. Covariance and contravariance appear when investigating whether one type is a subtype of another type. This question comes up concretely in several places, such as deciding whether a type is a valid argument parameter, or deciding which types can be held by a data structure.

Argument Parameters

Functions accept subtypes as arguments. For instance, a function requiring an Animal type will accept Cat or Dog subtypes as arguments. An interesting (and tricky) case is when a function takes a function as an argument, e.g.

def fun[T] (animal: Animal, paramFun: Animal => T) : T

What are valid arguments for fun? The animal argument is clear; it’s just any subtype of Animal. But which functions could we pass in for paramFun? This requires determining the subtype of a function.

A natural guess may be that a function f1: A => B is a subtype of a function f2: C => D when A is a subtype of C and B is a subtype of D. However, this is incorrect!

It turns out that we need to ‘reverse’ the argument subtyping; C must be a subtype of A. Specifically, we have:

f1: A => B is a subtype of f2: C => D when:

  • C is a subtype of A
  • B is a subtype of D

For instance, the function

def f1(animal: Animal) : Cat

is a subtype of

def f2(cat: Cat) : Animal

In terms of variances, we describe the above by saying that functions are contravariant in their argument types, and covariant in their return types.

The intuition for this is derived from the Liskov Substitution Principle, which is (paraphrased) the idea that a subtype should be expected to do everything that its supertypes can do. We should be able to use f1 in all of the ways that we can use f2.

To connect it back with this post’s topic, it turns out that defining and enforcing variance rules is a language feature in Scala! Scala provides the - and + type annotations for contravariance and covariance, respectively. Thus we can implement, for instance, a one parameter function class, and bake the variance rules into the type parameters:

class Function1[-V, +W] { ... }

The type tells us that Function1 is contravariant in V and covariant in W, as desired.

Type Bounds

Another type-related feature is type bounding. We can create a parameterized function and specify that its parameterized type is a subtype (or supertype) of another type, e.g:

def foo[T :> String] (t: T)

says that T must be a supertype of String, and

def foo[String <: T <: Object] (t: T)

says that T must be a supertype of String and a subtype of Object.

An Example

As an example of where we would use variance and type bounds, suppose we want to add a prepend function to the List class. Looking at Scala’s documentation, we can see that the List are parameterized by a covariant type A:

sealed abstract class List[+A] ...

Now, “`prepend“` will add an element to the front of the list; a first guess at its type signature might be:

// compile error
def prepend[A] (a: A) : List[A]

We are attempting to pass a covariant type as a function argument; since function arguments are contravariant this will result in a compile error.

To fix this, we can use a type bound to tell the compiler that we’d like to be able to pass in any supertype B of A, and produce a new list of B’s:

// compiles
def prepend[B >: A] (b: B) : List[B]

For Comprehensions

The functions map, flatMap, and filter are used commonly in functional programming. A common pattern is to chain these functions together, which can lead to bloated and unreadable (albeit effective) code. A simple example:

(1 until 10).flatMap(x => 
   (1 until 20).filter(y => y >= 10)
   .map(y => (x,y)))

Scala provides for comprehensions as a way of chaining map, flatMap, and filter operations together. Broadly stated, for comprehensions are syntactic sugar that map ‘arrow’, ‘if’, and ‘yield’ to map, filterWith, and flatMap, leading to more concise and readable code:

for {
  x <- (1 until 10)
  y <- (1 until 20)
  if y >= 10 
} yield (x, y)

Any type that supports map,filter, and flatMap is eligible to be used in a for comprehension, such as List, Future, Map, and many many more. For those familiar with Haskell, Scala’s for comprehensions are analogous to Haskell’s do notation, which eliminates the clutter of explicitly writing binds.

Call by Optional

By default, Scala functions are call by value, meaning that function parameters will be reduced prior to evaluating the function body. However, Scala let’s you override this default by adding => before a parameter’s type:

// x is call by value
// y is call by value
def fun1(x: Int, y: Int)

// x is call by value
// y is call by name
def fun2(x: Int, y: => Int)

The y parameter in fun2 will be call by name, meaning that it will only be evaluated if and when it’s used in the function body.

While this is a relatively minor feature, it speaks to a general theme that I’ve noticed with Scala: the language provides you with many tools, opening up multiple ways to solve a problem. While this flexibility can be misused or confusing, so far I’ve found that it contributes to Scala being a great practical language. Code can be structured using functional ideas, while still incorporating object-oriented ideas and allowing access to the extensive ecosystem of Java libraries.

Here are some links with more details on these areas:

Variance and Type Bounds:

For Comprehensions:

There are also some good general Scala resources that the course pointed to: