{{announcement.body}}
{{announcement.title}}

Scala at Light Speed, Part 3: Functional Programming

DZone 's Guide to

Scala at Light Speed, Part 3: Functional Programming

Check out this post to learn more about functional programming in Scala.

· Java Zone ·
Free Resource

Check out this post to learn more about functional programming in Scala.

This article series is for busy programmers who want to learn Scala fast, in 2 hours or less. The articles are the written version of Rock the JVM's Scala at Light Speed mini-course, which you can find for free on YouTube or on the Rock the JVM website in video form.

This is the third article of the series, which will focus on Scala as a functional programming language. You can watch it in video form here or in the embedded video below.

So far, we've covered:

  • How to get started with Scala
  • The basics: values, expressions, types
  • Object-orientation: classes, instances, singletons, methods, and basic generics

You may also like: Scala at the Speed of Light, Part 1: The Essentials, and Part 2: Object Orientation

What Is a Function?

From the previous part, you remember that Scala has the special apply method that allows instances of classes (or singleton objects) to be invoked like functions:

Scala




x


 
1
  class Person(name: String) {
2
    def apply(age: Int) = println(s"I have aged $age years")
3
  }
4
 
          
5
  val bob = new Person("Bob")
6
  bob.apply(43)
7
  bob(43) // INVOKING bob as a function === bob.apply(43)



Also, let's recap a few points:

  1. Scala runs on top of the JVM.
  2. The JVM was built for Java, which is an OO language.
  3. Scala is a functional programming language, meaning we work with functions like any other values: we pass them around, compose them, return them as results.

To fulfill the functional programming ideal, Scala comes with the following traits (simplified):

Scala




xxxxxxxxxx
1


 
1
trait Function1[T, R] {
2
  def apply(argument: T): R
3
}
4
 
          
5
trait Function2[T1, T2, R] {
6
  def apply(arg1: T1, arg2: T2): R
7
}
8
 
          
9
//... all the way to Function22



Because apply allows us to invoke instances like functions, we can easily write:

Scala




xxxxxxxxxx
1


 
1
  val simpleIncrementer = new Function1[Int, Int] {
2
    override def apply(arg: Int): Int = arg + 1
3
  }
4
 
          
5
  val newValue = simpleIncrementer(23) // same as simpleIncrementer.apply(23)



We've just defined a function!

Scala function values are instances of Function1, Function2, ... Function22, which are nothing but simple traits with an apply method.

Looks Like Functional Programming

In functional programming, we work a lot with function values. Constructing them as an anonymous class every time would be a pain. Scala has syntactic sugar for creating function values:

Scala




xxxxxxxxxx
1


 
1
val doublerFunction = (x: Int) => x * 2
2
/*
3
    same as:
4
    
5
    val doublerFunction = new Function1[Int, Int] {
6
      override def apply(x: Int) = x * 2
7
    }
8
*/



The type of function defined above is Function1[Int, Int], or  Int => Int for short (looks similar to Haskell in that regard).

For those with real typing anxiety, Scala has an even more shorthand notation:

Scala




xxxxxxxxxx
1


 
1
val shortIncrementer: Int => Int = _ + 1 // same as (x: Int) => x + 1
2
val multiplier: (Int, Int) => Int = _ * _ // same as (x: Int, y: Int) => x * y



As long as the compiler knows the function type (as I explicitly declared), you can use underscores for each different argument of the function. This shorthand notation is particularly useful for long method chains with function arguments.

HOFs

Methods or functions taking other functions as arguments or returning functions as results are called higher-order functions (HOFs).

Some classical HOFs we use a lot with collections are map, flatMap, and filter. Let's take them in turn as examples.

A collection's map method will take a function as an argument and will return a new collection containing the applications of the function on every element:

Scala




xxxxxxxxxx
1


 
1
  val aMappedList: List[Int] = List(1,2,3).map(x => x + 1) // or .map(_ + 1), same thing
2
  // List(2,3,4)



A collection's flatMap method takes a function from an element to another collection. The applications of the function on every element will return many small collections, which will be finally concatenated in the returned collection.

Scala




xxxxxxxxxx
1


 
1
  val aFlatMappedList = List(1,2,3).flatMap { x =>
2
    List(x, 2 * x)
3
  } 
4
  // alternative syntax, same as .map(x => List(x, 2 * x))
5
  // returns List(1,2,2,4,3,6) as the concatenation of [1,2], [2,4] and [4,6]



A collection's filter method takes a function from an element to Boolean, and returns a new collection containing just the elements for which the function returns true.

Scala




xxxxxxxxxx
1


 
1
val aFilteredList = List(1,2,3,4,5).filter(_ <= 3)
2
// List(1,2,3)



Combining map, flatMap, and filter is a powerful technique to manipulate collections. An example: all the pairs from combining a number from the list [1,2,3] with a character from the list [a,b,c]:

Scala




xxxxxxxxxx
1


 
1
  val allPairs = List(1,2,3)
2
        .flatMap(number => List('a', 'b', 'c').map(letter => s"$number-$letter"))
3
 
          



So, for every number, we take the list of characters, and for every one, we combine the number and the letter. 

This is the style of thinking we use in Scala and functional programming. Instead of iterating through a collection and manually constructing the values, we are manipulating the existing collections through their higher-order functions. Of course, "manipulating" is an improper term because we work with immutable data structures, so we always return a new collection.

These map/flatMap/filter chains can be very long in practice, and thus become increasingly hard to read. Scala offers another syntax sugar for these large map/flatMap/filter chains in the form of for-comprehensions:

Scala




xxxxxxxxxx
1


 
1
  val alternativePairs = for {
2
    number <- List(1,2,3)
3
    letter <- List('a','b','c')
4
  } yield s"$number-$letter"
5
  // equivalent to the map/flatMap chain above



Misperception to dispel right off the bat: this is not a for loop. It's an expression because it reduces to a value. The value is computed by the map/flatMap/filter chain, which the compiler reconstructs for you when you write a for-comprehension. If your data structure doesn't offer the appropriate  map/flatMap/withFilter  methods, it will not be eligible for for-comprehensions and the compiler will tell you that.

A Brief Collections Overview

By far, the most common data structure is a list. In the standard library, it's implemented as a singly linked list. The fundamental methods are head (first element), tail (remainder of the list), and ::  (prepending an element).

Scala




xxxxxxxxxx
1


 
1
  val aList = List(1,2,3,4,5)
2
  val firstElement = aList.head
3
  val rest = aList.tail
4
  val aPrependedList = 0 :: aList // List(0,1,2,3,4,5)
5
  val anExtendedList = 0 +: aList :+ 6 // List(0,1,2,3,4,5,6)



The supertype for all ordered collections of elements is Seq  (for sequence). The fundamental method is the  apply  method, which accesses an element at a given index in the sequence.

Scala




xxxxxxxxxx
1


 
1
  // sequences
2
  val aSequence: Seq[Int] = Seq(1,2,3) // Seq.apply(1,2,3)
3
  val accessedElement = aSequence(1) // the element at index 1: 2



For large sequential data in memory, Vector is a fast Seq implementation (implemented as a full 32-ary tree). It has the same API as a Seq.

Scala




xxxxxxxxxx
1


 
1
  // vectors: fast Seq implementation
2
  val aVector = Vector(1,2,3,4,5)



Sets are also useful, because they contain no duplicates. Set  is just a trait, so the Set companion's apply method returns some implementation of Set (usually some hash set). The fundamental methods are contains (check if an element is in the set),  +  and  -  (for adding and removing elements).

Scala




xxxxxxxxxx
1


 
1
  // sets = no duplicates
2
  val aSet = Set(1,2,3,4,1,2,3) // Set(1,2,3,4)
3
  val setHas5 = aSet.contains(5) // false
4
  val anAddedSet = aSet + 5 // Set(1,2,3,4,5)
5
  val aRemovedSet = aSet - 3 // Set(1,2,4)



Ranges are useful for generating numbers. They have the same API as a Seq, but don't need to actually contain all the numbers.

Scala
 




xxxxxxxxxx
1


1
  // ranges
2
  val aRange = 1 to 1000
3
  val twoByTwo = aRange.map(x => 2 * x).toList // List(2,4,6,8..., 2000)



Don't ask about the to operator above (for now). Notice instead that we can convert in between various collections with  toList,  toSet,  toSeq  etc.

Finally, multi-type collections. Starting with tuples, which are groups of values under the same value (Python folks should be familiar):

Scala
xxxxxxxxxx
1
 
1
  // tuples = groups of values under the same value
2
  val aTuple = ("Bon Jovi", "Rock", 1982)


And for key-value associations, we have maps with their fundamental methods exemplified below.

Scala
xxxxxxxxxx
1
10
 
1
// maps
2
val aPhonebook: Map[String, Int] = Map(
3
  ("Daniel", 6437812),
4
  "Jane" -> 327285 // same as ("Jane", 327285)
5
)
6
 
          
7
val aNewPhonebook = aPhonebook + ("Mary" -> 5731) // add/replace association
8
val removedDaniel = aPhonebook - "Daniel" // remove key
9
val hasJane = aPhoneBook.contains("Jane")
10
val danielsNumber = aPhonebook("Daniel") // beware this can throw if key not present


In the next part, we will talk about one of Scala's most powerful features: pattern matching.

Further Reading

Clean Code Best Practices in Scala

A Journey With Scala

[DZone Refcard] Getting Started With Scala

Topics:
fp, functional programming, scala, tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}