Scala 入門 (Essential Scala)

Noel Welsh and Dave Gurnell
(Translated by Takuya Tsuchida)

Version 1.3, April 2017

序文

本書は、はじめて Scala を学ぶプログラマーを対象にしています。Java のようなオブジェクト指向プログラミング言語にある程度精通していることを前提としていますが、関数型プログラミングについての経験は前提としていません。

本書のゴールは最小限の Scala の使い方を説明することです。そのため、Scala コードにおいてイディオムとして使用される重要パターンに焦点をあて、Scala の機能が実現可能にするそれらパターンの文脈で、Scala の機能自体を紹介していきます。Scala の機能を網羅することを目的とはしていませんし、本書はリファレンスマニュアルでもありません。

いくつかの演習を除いて、外部ライブラリには一切依存していません。本書に含まれるすべての問題は、テキストエディターと Scala の REPL、もしくは、Scala IDE for EclipseIntelliJ IDEA のような IDE で完成させることができます。

Scala 入門 (Essential Scala) は、UnderscoreNoel WelshDave Gurnell によって生み出されました。それは、Underscore’s eBook Template とプレーンテキスト、関数型プログラミングへの深い愛情を使用して構築されました。

本書で使用している規則

本書には、たくさんの技術情報とプログラムコードが含まれます。曖昧さを減らし、重要な概念を強調するために、下記のような組版規則を使用します。

組版規則

新しい用語やフレーズは太字で紹介されます。(訳注:原書では斜体ですが、訳書では視認性の高い太字を使用します。)最初に紹介されたあとは、通常の字体で書かれます。

プログラムコード由来の用語やファイル名、ファイルの内容は等幅フォントで記述します。単数形と複数形を区別しないことに注意してください。例えば、StringStirngs と書いたものは java.util.String クラスやその型のオブジェクトを参照します。(訳注:日本語訳ではすべて単数形に統一しています。)

外部リソースへの参照はハイパーリンクとして記述します。API ドキュメントへの参照は、Option のように、ハイパーリンクと等幅フォントの組み合わせを使用して記述します。

ソースコード

ソースコードブロックは下記のように記述されます。文法は必要に応じて適切にハイライトされます。

object MyApp extends App {
  println("Hello world!") // ユーザーへの素晴らしいメッセージを印字する!
}

プログラムコードのいくつかの行は、ページに収めるには広すぎるものがあります。そのような場合には、すべてを1行で書かれるべき長いコードであることを示すために、折れた矢印の継続文字 (continuation character) を使用します。例えば、下記のようなコードを、

println("This code should all be written ↩
  on one line.")

実際は、下記のように書かれなければなりません。

println("This code should all be written on one line.")

コールアウトボックス

本書では、特別な内容を強調するために、3種類のコールアウトボックス (callout box) を使用します。

情報コールアウトは、要約やレシピ、ベストプラクティスを示します。

警告コールアウトは、コーナーケースや根本的な仕組みに関する追加情報を提供します。本書を最初に読むときはこれらの情報を読み飛ばして構いません。のちほど、追加情報が必要なときに戻ってきましょう。

危険コールアウトは、よくある落とし穴や罠を示します。問題を回避するために確実にこれを読んでください。もし、コードを実行するとき問題に見舞われたら、ここに戻ってきましょう。

Thanks

Many thanks to Richard Dallway and Jonathan Ferguson, who took on the herculean task of proof reading our early drafts and helped develop the rendering pipeline that produces the finished book.

Thanks also to Danielle Ashley, who updated all of the code samples to use Tut and increased our code quality 100% overnight!

Thanks also to Amir Aryanpour, Audrey Welsh, Daniel Watford, Jason Scott, Joe Halliwell, Jon Pearce, Konstantine Gadyrka, N. Sriram, Rebecca Grenier, and Raffael Dzikowski, who sent us corrections and suggestions while the book was in early access. Knowing that our work was being used made the long haul of writing worthwhile.

1 はじめに

本書では、Scala コードの短い例を使って学んでいきます。それには2つの推奨される方法があります。(訳注:訳書において Scala IDE を使用する方法は非推奨です。理由は「Scala IDE を準備する」の節に記載しています。)

  1. Scala コンソール (Scala console) を使用する(コマンドラインが好きな人にはよいでしょう)

  2. Scala IDEワークシート (worksheet) を使用する(IDE が好きな人にはよいでしょう)

ここでは、それらを準備するための各工程を一通り説明していきます。

1.1 Scala コンソールを準備する

http://scala-lang.org の手順に従って、コンピューターに Scala を設定します。一度 Scala がインストールされれば、コマンドラインプロンプトで scala と入力することで、対話的なコンソールを実行できるようになるはずです。これは macOS の例です。(訳注:内容を2020年現在のものに修正しています。)

dave@Jade ~> scala
Welcome to Scala 2.13.1 (OpenJDK 64-Bit Server VM, Java 1.8.0_242).
Type in expressions for evaluation. Or try :help.

scala>

scala> プロンプトで個々の式を入力し、Enter キーを押下することで、それらをコンパイルし、実行することができます。

scala> "Hello world!"
res0: String = Hello world!

1.1.1 一行の式を入力する

単純な式を入力してみましょう。

scala> 1 + 2 + 3
res1: Int = 6

Enter キーを押下したとき、コンソールは3つのことを応答します。

次章で見ていくように、Scala のすべての式は を持ちます。型はコンパイル時に決定され、値は式を実行するときに決定されます。ここでは、その両方が報告されています。

識別子 res1 は、式の結果を将来の式の中で参照できるように、コンソールが提供する便利なものです。例えば、下記のように先ほどの結果を2倍することができます。

scala> res1 * 2
res2: Int = 12

有用な値を生成しない式を入力すると、コンソールは応答として何も印字しません。

scala> println("Hello world!")
Hello world!

ここで、"Hello world!" という出力は println という記述に由来しています。入力した式は実際のところ値を返しません。コンソールは上記で見た出力に類するものを提供していないのです。

1.1.2 複数行の式を入力する

簡単に複数行に渡る長い式を分割することができます。式が終わる前に Enter キーを入力すると、コンソールは次の行に継続していることを示す | という文字を印字します。

scala> for(i <- 1 to 3) {
     |   println(i)
     | }
1
2
3

一度に複数行の式を入力したいことがあります。このような場合、:paste コマンドを使用することができます。単純に :paste と入力し、Enter キーを押下し、コードを記述もしくはコピー&ペーストしてください。すべてを入力し終わったら Ctrl+D を押下してください。通常のようにコードのコンパイルと実行がされます。

scala> :paste
// Entering paste mode (ctrl-D to finish)

val x = 1
val y = 2
x + y

// Exiting paste mode, now interpreting.

x: Int = 1
y: Int = 2
res6: Int = 3

ファイルに Scala コードがあるのであれば、ファイルの内容をコンソールの中に貼り付けるために :paste を使用できます。これは、コンソールに式を再入力するよりはるかに便利です。例えば、1 + 2 + 3 を含む example.scala という名前のファイルがあるとき、下記のように :paste を使用できます。

scala> :paste example.scala
Pasting file example.scala...
res0: Int = 6

1.1.3 式の型を印字する

コンソールを使用する上での最後のテクニックです。時々、式を実際に実行せずに、そのを知りたいことがあります。それをするために、:type コマンドを使用することができます。

scala> :type println("Hello world!")
Unit

コンソールは、この式の println という記述を実行しないことに注意してください。コードをコンパイルし、この場合は Unit と呼ばれる何らかの型を印字しているだけです。

Scala の Unit は Java や C の void と同じです。詳細は第2章を読んでください。

1.2 Scala IDE を準備する

Scala IDE は非推奨です(訳者追記)

Scala IDE は、2018年1月に 4.7.1 RC3 がリリースされたのを最後に更新がありません。そのため、訳書においては Scala IDE の利用を非推奨とし、本節を翻訳していません。

なお、Scala の IDE としては IntelliJ IDEA が主流になっています。ワークシートの機能もありますので、IDE を利用したい場合は IntelliJ IDEA をインストールすることで代替できると思います。しかし、訳書においては Scala コンソールでのみ検証しておりますので、Scala コンソールの利用を強く推奨します。

Scala IDE is a plugin that adds Scala language support to Eclipse. A complete version of Scala IDE with Eclipse is also available as a standalone bundle from http://scala-ide.org. This is the easiest way to install the software so we recommend you install the standalone bundle for this course.

Go to http://scala-ide.org now, click the Get the Bundle button, and follow the on-screen instructions to download Scala IDE for your operating system:

Scala IDE: Main website

Once you have downloaded and uncompressed the bundle you should find an application called Eclipse. Launch this. You will be asked to choose a folder for your workspace:

Scala IDE: Choose a workspace location

Accept the default location and you will see an empty main Eclipse window:

Scala IDE: Empty workspace

1.2.1 Creating your First Application

Your Eclipse workspace is two things: a folder containing files and settings, and a main window where you will be doing most of your Scala programming. In your workspace you can find projects for each Scala application you work on.

Let’s create a project for the book exercises now. Select the File menu and choose New > Scala Project:

Scala IDE: Create a new Scala project

Enter a Project name of essential-scala and click Finish. The tree view on the left of your workspace should now contain an empty project:

Scala IDE: Empty project

A project is no good without code to run! Let’s create our first simple Scala application - the obligatory Hello World app. Select the File Menu and choose New > Scala Object:

Scala IDE: Create a Scala object

Name your object HelloWorld and click Finish. A new file called HelloWorld.scala should appear in the tree view on the left of the main window. Eclipse should open the new file in the main editor ready for you to start coding:

Scala IDE: Single Scala file

The content of the file should read as follows:

object HelloWorld {

}

Replace this text with the following minimalist application:

object HelloWorld {
  def main(args: Array[String]): Unit = {
    println("Hello world!")
  }
}

Select the Run Menu and choose Run. This should execute the code in your application, resulting in the words Hello world! appearing in the Console pane at the bottom of the window. Congratulations - you just ran your first Scala application!

Scala IDE: Hello World

Developers with Java experience will notice the resemblance of the code above to the Java hello world app:

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello world!");
  }
}

The resemblance is, of course, no coincidence. These two applications compile to more or less the same bytecode and have exactly the same semantics. We will learn much more about the similarities and differences between Scala and Java as the course continues.

1.2.2 Creating your First Worksheet

Compiling and running code whenever you make a change is a time consuming process that isn’t particularly suitable to a learning environment.

Fortunately, Scala IDE allows us to create special files called Scala Worksheets that are specifically designed for training and experimentation. Every time you save a Worksheet, Eclipse automatically compiles and runs your code and displays the output on the right-hand side of your editor. This provides instant feedback, which is exactly what we need when investigating new concepts!

Create your first Scala Worksheet by selecting the File Menu and choosing New > Scala Worksheet:

Scala IDE: New Scala worksheet

Enter a Worksheet name of FirstSteps and click Finish. A new file called FirstSteps.sc should appear in the tree view on the left of the main window, and should open it in the main editor in the middle:

Scala IDE: Empty Scala worksheet

Note that the object on the left contains a single line of Scala code:

println("Welcome to the Scala worksheet")

for which Eclipse is displaying the corresponding output on the right:

Welcome to the Scala worksheet

Any expression you add to the left of the editor is evaluated and printed on the right. To demonstrate this, change the text in the editor to the following:

object FirstSteps {
  println("Welcome to the Scala worksheet")

  1 + 1

  if(20 > 10) "left" else "right"

  println("The ultimate answer is " + 42)
}

Save your work by selecting the File Menu and choosing Save (or better still by pressing Ctrl+S). Eclipse should automatically evaluate each line of code and print the results on the right of the editor:

object FirstSteps {
  println("Welcome to the Scala worksheet")   //> Welcome to the Scala worksheet

  1 + 1                                       //> res0: Int(2) = 2

  if(20 > 10) "left" else "right"             //> res1: String = left

  println("The ultimate answer is " + 42)     //> The ultimate answer is 42
}

Scala IDE: Completed Scala worksheet

We’ll dive into what all of the text on the right means as we proceed with the course ahead. For now you’re all set to start honing your Scala skills!

2 式・型・値

本章では Scala プログラムの基本的な構成要素である式 (expression)型 (type)値 (value) について見ていきます。それらのコンセプトを理解することが Scala プログラムがどのように動くのかというメンタルモデルを形成するために必要です。

2.1 最初のプログラム

Scala コンソールか Scala ワークシートに "Hello world!" と入力し、コンソールでリターンキーを押下するかワークシートを保存してください。このようなインタラクションが見られるはずです。

"Hello world!"
// res0: String = Hello world!

このプログラムについてはいろいろ言えることがあります。それは特別にリテラル式 (literal expression) もしくは略してリテラルと呼ばれる単一の式で構成されていることです。

Scala は私たちのプログラムを実行(評価)します。Scala コンソールや Scala ワークシートでプログラムを評価するとき、プログラムのとプログラムを評価したという2つの情報を受け取ります。この場合は、型が String で、値が "Hello world!" です。

プログラムが生成した出力値 “Hello world!” はプログラムと同じに見えますが、その2つの間には違いがあります。リテラル式は入力したプログラムテキストである一方、コンソールが表示したものはプログラムを評価した結果です。(リテラル (literal) は、評価されたものがその文字通り (literally) に見えるので名付けられました。)

わずかに複雑なプログラムを見てみましょう。

"Hello world!".toUpperCase
// res1: String = HELLO WORLD!

このプログラムはメソッド呼び出し (method call) を加えることによって最初の例を拡張したものです。Scala における評価は左から右へ進みます。この例では、最初にリテラル "Hello world!" が評価されます。次に、その評価された結果に対しメソッド toUpperCase が呼ばれます。このメソッドは文字列値を大文字に変換したものを新しい文字列として返します。これがコンソールによって表示された最終的な値です。

繰り返しになりますが、このプログラムの型は String で、この場合はプログラムが "HELLO WORLD!" に評価されます。

2.1.1 コンパイル時と実行時

Scala プログラムが通過する明確な2つの段階があります。最初がコンパイル (compile) で、コンパイルが成功していれば、次が実行 (run) または評価です。最初のステージをコンパイル時 (compile-time)、次のステージを実行時 (run-time) と呼びます。

Scala コンソールを使用しているとき、プログラムはコンパイルしてすぐに評価されるので、1つの段階だけがあるような印象を抱かせます。型と値の間における違いを正しく理解するためにも、コンパイル時と実行時がまったく異なることを理解するのは重要です。

コンパイルはプログラムが意味をなしているかを検証する工程です。プログラムが「意味をなす」には2つの観点が必要です。

  1. プログラムは文法的に正確 (syntactically correct) でなければなりません。それはプログラムの部品が言語の文法に従っているということを意味します。“on cat mat sat the”(猫の上敷き物座った)は文法的に正確ではない英文の例です。これは文法的に正確ではない Scala プログラムの例です。
toUpperCase."Hello world!"
// <console>:2: error: identifier expected but string literal found.
// toUpperCase."Hello world!"
//             ^
  1. プログラムは型検証 (type check) されなければなりません。意味をなすプログラムであるということは、プログラムがある制約に従っているということを意味します。“the mat sat on the cat”(敷き物は猫の上に座った)は文法的に正確ですが意味がわからない英文の例です。これは数値を大文字に変換しようとして型検証に失敗するシンプルなプログラムです。
2.toUpperCase
// <console>:13: error: value toUpperCase is not a member of Int
//        2.toUpperCase
//          ^

大文字小文字という概念は数値について意味をなさないので、型システムはこのエラーを捕捉します。

プログラムがコンパイル時の検証を通過した場合、次にプログラムは実行されるかもしれません。実行はコンピューターがプログラムにある指示を実行する工程です。

プログラムのコンパイルが成功したとしても、実行時に失敗する可能性が残っています。整数を0で割ると Scala では実行時エラーが発生します。

2 / 0
// java.lang.ArithmeticException: / by zero
//   ... 342 elided

整数の型 Int はプログラムの型検証を通過すれば割り算できます。しかし、割り算の結果を表現できる Int が存在しないので、実行時にプログラムは失敗します。

2.1.2 式・型・値

それでは、式・型・値とは正確には何でしょうか?

式はファイルやコンソール、ワークシートに入力したプログラムテキストの一部です。それは Scala プログラムの主要な構成要素です。のちほど、定義 (definition)文 (statement) と 名付けられた他の構成要素も見ていきます。式はコンパイル時に存在します。

式の特徴を定義すると、それは値に評価されるということです。値はコンピューターのメモリに保持されます。それは実行時に存在します。例えば、式 2 は、コンピューターのメモリの、特定の場所の、特定のビット列に評価されます。

私たちは値を用いて計算します。値はプログラムが受け渡したり操作したりする実体です。例えば、2つの数値の最小値を計算するために、下記のようなプログラムを書くでしょう。

2.min(3)
// res4: Int = 2

ここに2つの値 23 があり、それらを 2 に評価されるより大きなプログラムに結合しています。

Scala において、すべての値はオブジェクト (object) で、のちほど見ていくように特定の意味を持ちます。

さて次に型のことを考えましょう。型はプログラム上の制約で、どのようにオブジェクトを操作できるかを制限します。すでに2つの型 StringInt を、そして型によって異なる操作を実行できることを見てきました。

この段階で、型についてもっとも重要な点は式は型を持つが値は持たないということです。私たちはコンピューターのメモリの任意の部分を検査できませんし、それを生成したプログラムを知らずして、どのようにそれが解釈されるのかを予言できません。例えば、Scala で Int 型と Float 型はどちらもメモリの32ビットによって表現されます。しかし、与えられた32ビットを IntFloat として解釈すべきというタグやその他の表示はないのです。

実行時エラーを引き起こす式の型を教えてと、Scala コンソールに尋ねることによって、コンパイル時に型が存在することを明らかにできます。

:type 2 / 0
// Int
2 / 0
// java.lang.ArithmeticException: / by zero
//   ... 486 elided

2 / 0 は、それを評価したときには失敗するにも関わらず、Int 型を持つことを見ました。

コンパイル時に存在する型は、値に一貫した解釈を与えるプログラムを書くように制約します。特定の32ビットが、ある時は Int で、またある時は Float であると断言はできません。プログラムの型が検証されたとき、Scala はすべての値が一貫して使用されることを保証するので、値の表現で型の情報を記録する必要がありません。型の情報を取り除くこの処理を型消去 (type erasure)1と呼びます。

必然的に、型に合致する値について、考え得るすべての情報をその型は含みません。そうしないと、型検証はプログラムを実行することと等価になってしまいます。すでに見てきたように、型システムは Int をゼロで割ることを防ぐことはなく、それは実行時エラーを引き起こします。

Scala コード設計の主要部分は、型システムの利用において、どのエラーケースを無視したいのかを決定することです。型システムでたくさんの便利な制約を表現することで、プログラムの信頼性を向上させられることを見ていきます。もしプログラムにおいて十分に重要であると決定すれば、エラーの可能性を表現する型システムを使用した除算演算子を実装することができます。型システムを上手に使用することは本書における重要なテーマのひとつです。

2.1.3 覚えておいてほしいこと

Scala を使用するのであれば、Scala プログラムのメンタルモデルを構築しなければなりません。このモデルにおける3つの基本的な構成要素はです。

式は値に評価されるプログラムの部品です。Scala プログラムの主要な部品になります。

式は型を持ち、プログラムの制約を表現します。コンパイル時にプログラムの型は検証されます。型に一貫性がない場合、コンパイルは失敗し、プログラムを評価(実行)することはできません。

値はコンピューターのメモリに存在し、実行中のプログラムが操作するものです。Scala におけるすべての値はオブジェクトで、その意味はのちほど議論します。

2.1.4 演習

2.1.4.1 型と値

Scala コンソールか Scala ワークシートを使用して、下記の式の型と値を特定してください。

1 + 2

型は Int で値は 3 です。

"3".toInt

型は Int で値は 3 です。

"foo".toInt

型は Int ですが、これは値に評価されません。その代わりに例外が発生し、発生した例外は値ではありません。これをどう理解すればいいでしょうか?式の結果による計算を続けられないということです。例えば、それを印字することはできません。

println("foo")
// foo

println("foo".toInt)
// java.lang.NumberFormatException: For input string: "foo"
//   at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
//   at java.lang.Integer.parseInt(Integer.java:580)
//   at java.lang.Integer.parseInt(Integer.java:615)
//   at scala.collection.immutable.StringLike$class.toInt(StringLike.scala:272)
//   at scala.collection.immutable.StringOps.toInt(StringOps.scala:29)
//   ... 730 elided

を比較してみてください。後者でどんな印字も発生しないことは、println が評価されないということを示しています。

2.2 オブジェクトとの相互作用

前節では Scala プログラムの基本的な構成要素である式・型・値を見てきました。また、すべての値はオブジェクトであることも学びました。本節では、オブジェクトとオブジェクトに対してどのように相互作用するのかについて学んでいきます。

2.2.1 オブジェクト

オブジェクトはデータとそのデータへの操作のグループです。例えば、2 はオブジェクトです。データは整数の2で、データへの操作はなじみのある +- などです。

オブジェクトのデータと操作について特別な専門用語があります。操作はメソッド (method) として知られています。データはフィールド (field) に保持されます。

2.2.2 メソッド呼び出し

メソッド呼び出し (call) によってオブジェクトを作用させます2。メソッド呼び出しの例をいくつかすでに見てきました。例えば、String の大文字版をその toUpperCase メソッドを呼び出すことによって取得できることを見ました。

"hello".toUpperCase
// res0: String = HELLO

一部のメソッドは引数 (parameter) を受け取り、それはメソッドがどのように動作するかを制御します。例えば、take メソッドは String から文字を取り出します。いくつの文字を取り出したいかを指定する引数を take に渡す必要があります。

"abcdef".take(3)
// res1: String = abc

"abcdef".take(2)
// res2: String = ab

メソッド呼び出し文法

メソッド呼び出しのための文法は、

anExpression.methodName(param1, ...)

anExpression.methodName

です。ここで、

  • anExpression はオブジェクトに評価される任意の式
  • methodName はメソッドの名前
  • param1, ... はメソッドの引数に評価される1つ以上の式

とします。

メソッド呼び出しは式なのでオブジェクトに評価されます。これは、より複雑なプログラムをつくるために、メソッド呼び出しを連鎖できるということを意味します。

"hello".toUpperCase.toLowerCase
// res3: String = hello

メソッド呼び出しにおける様々な式はどの順番で評価されるのでしょうか?メソッド引数は、メソッドが呼び出される前に左から右に評価されます。下記の式では、

"Hello world!".take(2 + 3)
// res4: String = Hello

最初に式 "Hello world!" が、次に 2 + 3(これは、最初に 2 を、次に 3 を評価する必要があります。)が、そして最後に "Hello world!".take(5) が評価されます。

2.2.3 演算子

Scala においてすべての値はオブジェクトであるため、IntBoolean のようなプリミティブ型についてもメソッドを呼び出すことができます。これは intboolean がオブジェクトではない Java と対照的です。

123.toShort // これは Scala で `Short` を定義する方法です
// res5: Short = 123

123.toByte // これは `Byte` を定義する方法です
// res6: Byte = 123

しかし、Int がオブジェクトであれば、+- のような基本的な算術演算子は何でしょうか?それらもまたメソッドでしょうか?そうです。Scala のメソッドは英数字の名前と同じようにシンボルの名前を持つことができます。

43 - 3 + 2
// res7: Int = 42

43.-(3).+(2)
// res8: Int = 42

(Scala 2.10 以前においては、43.Double として解釈されることを防ぐために、(43).-(3).+(2) と書く必要があるので注意してください。)

中置演算子記法

Scala において a.b(c) と書かれた任意の式は a b c と書けます。

a b c d e が等価であるのは a.b(c).d(e) で、a.b(c, d, e) ではないことに注意します。

シンボルの名前を持つか、英数字の名前を持つかにかかわらず、1つの引数を受け取るどんなメソッドでも中置演算子記法 (infix operator notation) を使用できます。

"the quick brown fox" split " "
// res: Array[String] = Array(the, quick, brown, fox)

中置記法は、いくつかの文法的略記法のひとつで、冗長なメソッド呼び出しの代わりに、簡潔な演算子式を書くことを可能にします。前置 (prefix)後置 (postfix)右結合 (right-associative)代入演算子 (assignment-style) という記法もありますが、中置記法に比べると一般的ではありません。

中置演算子の結合は何の優先順位規則を支持すべきか?という質問はそれ自身をもたらします。Scala は、数学や論理の直観的知識にならい、メソッド名として使用する識別子から得られる優先順位規則一式を使用します。

2 * 3 + 4 * 5
// res11: Int = 26

(2 * 3) + (4 * 5)
// res12: Int = 26

2 * (3 + 4) * 5
// res13: Int = 70

2.2.4 覚えておいてほしいこと

Scala のすべての値はオブジェクトです。それらのメソッド呼び出しによってオブジェクトを作用させます。Java を背景知識としているのであれば、Int や他の任意のプリミティブ値のメソッドを呼び出せることに注意してください。

メソッド呼び出しのための文法は、

anExpression.methodName(parameter, ...)

anExpression methodName parameter

です。

Scala は非常に少ない演算子を持ち、ほとんどすべてはメソッド呼び出しです。中置演算子記法のような文法規則をコードを簡潔かつ読みやすくするために使用しますが、標準のメソッド記法の方がわかりやすい場合はいつでもそれに戻すことができます。

のちほど見ていくように、式を伴うプログラミングにおける Scala の焦点は、Java で実現するよりさらに短いコードで書くことを可能にすることです。また、値と型を使用する非常に直観的な方法で、コードについての判断を可能にします。

2.2.5 演習

2.2.5.1 演算子スタイル

演算子スタイルに書き直してください。

"foo".take(1)
// res14: String = f
"foo" take 1
// res15: String = f

メソッド呼び出しスタイルに書き直してください。

1 + 2 + 3
// res16: Int = 6
1.+(2).+(3)
// res17: Int = 6

2.2.5.2 置換

下記の2つの式の間にある違いは何ですか?類似点は何ですか?

1 + 2 + 3

6

2つの式は同じ結果型と同じ返却値を持ちます。しかし、それらは異なった方法でその結果に辿り着きます。前者は一連の加算によってその結果を計算する一方、後者はただ単にリテラルです。

どちらの式も副作用を持たないので、ユーザー視点からそれらは交換可能です。1 + 2 + 3 と書けるところはどこでも 6 と書け、どんなプログラムの意味も変えることはありません。逆もまた同様です。これは置換 (substitution)(訳注:数学だと代入という訳語が一般的です。)として知られており、あなたは数式を簡単にする原理として学校で記憶しているかもしれません。

プログラマーとしてはコードがどのように動くのかというメンタルモデルを養う必要があります。評価の置換モデルは目に入る式はどれでもその結果と置換して構わないというとりわけ単純なモデルです。副作用がないことで、式の置換モデルはいつでも機能します3。式の各構成要素である型と値を知っていれば、式全体としての型と値を知っています。関数型プログラミングにおいて副作用を避けようと努力している理由は、それがプログラムを容易に理解できるようにするからです。

2.3 リテラルオブジェクト

すでに Scala の基本型のいくつかを取り上げました。本節では、Scala におけるすべてのリテラル式を扱うことによって、その知識に磨きをかける予定です。リテラル式は「それ自身」が象徴する固定値を表現します。下記はひとつの例です。

42
// res0: Int = 42

この REPL での相互作用は、リテラル 42Int 42 に評価されることを示しています。

リテラルとそれが評価された値を混同してはいけません。リテラル式はプログラムを実行する前のプログラムテキストにおける表現で、値はプログラムを実行した後のコンピューターのメモリにおける表現です。

これまでにプログラミングの経験、とくに Java の経験があれば、Scala におけるリテラルに見覚えがあるはずです。

2.3.1 数値

数値は Java にある同じ型を共有しており、32ビット整数の Int、64ビット浮動小数点数の Double、32ビット浮動小数点数の Float、そして64ビット整数の Long があります。

42
// res1: Int = 42

42.0
// res2: Double = 42.0

42.0f
// res3: Float = 42.0

42L
// res4: Long = 42

Scala は16ビット整数の Short と8ビットの Byte を持ちますが、それらを生成するためのリテラル文法はありません。その代わりに、toShorttoByte と呼ばれるメソッドを使用することで生成できます。

2.3.2 真偽値

真偽値は Java とちょうど同じで、truefalse です。

true
// res5: Boolean = true

false
// res6: Boolean = false

2.3.3 文字値

Chars は16ビット Unicode 値で、シングルクォートで囲まれた単一の文字として書かれます。

'a'
// res7: Char = a

Scala 対 Java の型階層

最初の文字が大文字で書かれていますが、Scala の IntDoubleFloatLongShortByteBooleanChar は、Java の intdoublefloatlongshortbytebooleanchar とちょうど同じものを参照します。

Scala において、それらすべての型はメソッドとフィールドを伴うオブジェクトのように振る舞います。しかしながら、一度コードがコンパイルされたら、Scala の Int は Java の int とちょうど同じです。これが2つの言語間の相互運用を簡単にしてくれるのです。

2.3.4 文字列値

文字列値はちょうど Java の文字列で、同じ方法で書かれます。

"this is a string"
// res8: String = this is a string

"the\nusual\tescape characters apply"
// res9: String =
// the
// usual    escape characters apply

2.3.5 Null 値

Null 値は Java と同じであるにもかかわらず、ほとんど頻繁には使用されません。また、Scala の null は独自の Null 型を持ちます。

null
// res10: Null = null

Scala で Null 値を使用する

null は Java コードで一般的ですが、Scala では非常に悪い習慣であると考えられています。

Java における null の主な用途は、プログラム実行の異なる時点において、値の有無という任意 (optional) 値を実装することです。しかしながら、null 値はコンパイラーによってチェックできないため、NullPointerException という形で実行時エラーが発生する可能性があります。

のちほど、コンパイラーによってチェックされる任意値を定義する手段を Scala が持つことを見ていきましょう。これは null を使用する必要性を取り除き、プログラムをより安全にしてくれます。

2.3.6 Unit 値

Scala で () と書かれる Unit 値は、Java の void に相当します。Unit 値は、println を使用して標準出力に印字するような、何の面白みもない値に評価される式の結果です。コンソールは Unit 値を印字しませんが、式の型を尋ねることで、実際に何らかの式の結果であることがわかります。

()
:type ()
// Unit
println("something")
// something
:type println("something")
// Unit

Unit 値は Scala において重要な概念です。Scala の文法構造の多くは、型と値を持つです。有用な値を得られない式のためのプレイスホルダーが必要で、Unit 値はまさにそれを提供してくれます。

2.3.7 覚えておいてほしいこと

本節では、基本的なデータ型を評価するリテラル式を見てきました。これらの基本型は、等価なものがない Unit を除いて、ほとんど Java と同じです。

すべてのリテラル式はを持ち、に評価されることに注意してください。これは、より複雑な Scala の式にも当てはまります。

次節では、独自のオブジェクトリテラルを定義する方法を学びます。

2.3.8 演習

2.3.8.1 文字通りただのリテラル

下記の Scala リテラルの値と型は何ですか?

42

true

123L

42.0

42Int です。trueBoolean です。123LLong です。42.0Double です。

この演習は、Scala コンソールや Scala ワークシートを使用した体験をするだけです。

2.3.8.2 引用と誤引用

下記のリテラルの違いは何ですか?それぞれの型と値は?

'a'

"a"

1つ目は Char リテラルであり、2つ目は String リテラルです。

2.3.8.3 副作用についての余談

下記の式の違いは何ですか?それぞれの型と値は?

"Hello world!"

println("Hello world!")

リテラル式 "Hello world!"String の値として評価されます。式 println("Hello world!")Unit として評価され、副作用としてコンソールに "Hello world!" を印字します。

これは、値を評価するプログラムと、値を副作用として印字するプログラムとの間の重要な区別です。前者はより大きな式で使用できますが、後者は使用できません。

2.3.8.4 失敗から学ぶ

下記のリテラルの型と値は何ですか?REPL や Scala ワークシートに書いてみて、何が起こるか見てみましょう!

'Hello world!'

エラーメッセージを見ることになるはずです。開発環境のエラーメッセージを読んで慣れることに時間をかけてください。すぐに他にもたくさん見るようになるので!

2.4 オブジェクトリテラル

ここまでは、IntString のような組み込み型のオブジェクトを作成し、それらを式に組み合わせる方法を見てきました。本節では、オブジェクトリテラル (object literals) を使用して独自デザインのオブジェクトを作成する方法を見ていきます。

オブジェクトリテラルを書くとき、式とは別のプログラムの一種である宣言 (declaration) を使います。宣言は値を評価しません。その代わりに名前と値を関連付けます。この名前は他のコードで値を参照するために使用できます。

下記のように空のオブジェクトを宣言できます。

object Test {}

これは値に評価されず式ではありません。むしろ、名前 Test を空のオブジェクト値に結び付けます。

一度 Test という名前に結び付ければ式の中で使用することができ、それは宣言したオブジェクトに評価されます。もっとも単純な式はそれ自身の名前だけで、それ自体が値として評価されます。

Test
// res0: Test.type = Test$@557b7f7d

この式は 123"abc" のようなリテラルを書くことと同じです。オブジェクトの型は Test.type として報告されることに注意してください。これは、これまで見てきた型とは異なり、オブジェクトのためだけに作成された新しい型で、シングルトン型 (singleton type) と呼ばれます。この型における他の値を作成することはできません。

空のオブジェクトはあまり便利ではありません。オブジェクト宣言の本体である中括弧の間には式を入れることができます。しかし、メソッドやフィールド、さらなるオブジェクトを宣言するような宣言を入れるのが一般的です。

オブジェクト宣言文法

オブジェクト宣言のための文法は、

object name {
  declarationOrExpression ...
}

です。ここで、

  • name はオブジェクトの名前
  • declarationOrExpression は宣言か式(オプション)

とします。

それではメソッドとフィールドをどのように宣言するか見ていきましょう。

2.4.1 メソッド

メソッドによってオブジェクトを作用させるので、メソッドを伴うオブジェクトを生成してみましょう。

object Test2 {
  def name: String = "Probably the best object ever"
}

ここでは name と呼ばれるメソッドを生成しました。これをいつもの方法で呼び出すことができます。

Test2.name
// res1: String = Probably the best object ever

下記はより複雑なメソッドを伴うオブジェクトです。

object Test3 {
  def hello(name: String) =
    "Hello " + name
}
Test3.hello("Noel")
// res2: String = Hello Noel

メソッド宣言文法

メソッドを宣言するための文法は、

def name(parameter: type, ...): resultType =
  bodyExpression

def name: resultType =
  bodyExpression

です。ここで、

  • name はメソッドの名前
  • parameter はメソッドに与えられる引数の名前(オプション)
  • type はメソッド引数の型
  • resultType はメソッドの結果型(オプション)
  • bodyExpression はメソッドを呼び出すことで評価される式

とします。メソッド引数はオプションですが、メソッドが引数を持つのであれば、それらの型は与えられなければなりません。結果型はオプションであるにもかかわらず、それを定義することが、機械的に検証されたドキュメントとしての役割を果たすので良い習慣とされます。

引数 (argument) という用語はパラメーター (parameter) と言い換えることができます。(訳注:本書ではどちらの用語も引数という訳語に統一しています。)

返却は暗黙的

メソッドの返却値は本体を評価することによって決定されます。Java でするように return を書く必要はありません。

2.4.2 フィールド

オブジェクトはフィールド (field) と呼ばれる他のオブジェクトを含めることもできます。def によく似た valvar というキーワードを使用して導入します。

object Test4 {
  val name = "Noel"
  def hello(other: String): String =
    name + " says hi to " + other
}
Test4.hello("Dave")
// res3: String = Noel says hi to Dave

フィールド宣言文法

フィールドを宣言するための文法は、

val name: type = valueExpression

var name: type = valueExpression

です。ここで、

  • name はフィールドの名前
  • type はフィールドの型(オプション)
  • valueExpressionname に束縛されるオブジェクトに評価される式

とします。

val を使用することで不変 (immutable) フィールドを定義でき、これは名前に束縛された値の変更ができないことを意味します。var可変 (mutable) フィールドで、束縛された値の変更ができます。

常に var より val を選びましょう。 置換を維持するため、Scala プログラマーは可能な限り不変フィールドを使用することを選びます。アプリケーションコードで時折可変フィールドを生成することは間違いありませんが、本書の大部分では var を使用しないようにしており、普段の Scala プログラミングにおいてもそれに倣いましょう。

2.4.3 メソッド対フィールド

同じ動作をするように見える引数のないメソッドを持つことができるにも関わらず、なぜフィールドが必要なのか不思議に思うかもしれません。その違いはわずかで、フィールドは値に名前を与えますが、一方のメソッドは値を算出する計算に名前を与えます。

その違いを明らかにするオブジェクトがこちらです。

object Test7 {
   val simpleField = {
     println("Evaluating simpleField")
     42
   }
   def noParameterMethod = {
     println("Evaluating noParameterMethod")
     42
   }
}

ここでは、コンソールに何かを印字するために println 式を、式をグループにするためにブロック式({} によって囲まれた式)を使用しています。なお、ブロック式については、次節でより詳しく見ていきます。

オブジェクトを定義したとコンソールに表示されているのに、いずれの println 文も実行されていないことに注意してください。これは遅延読み込み (lazy loading) と呼ばれる Scala と Java の特性によるものです。

オブジェクトやクラス(後述)は、他のコードによって参照されるまで読み込まれません。これが、単純な "Hello world!" アプリを実行するために、Scala が標準ライブラリ全体をメモリに読み込むことを防いでいます。

式の中で Test7 を参照することで、Scala にオブジェクトの本体を強制的に評価させてみましょう。

Test7
// Evaluating simpleField
// res4: Test7.type = Test7$@2149aa05

オブジェクトが最初に読み込まれるとき、Scala はその定義を実行し、各フィールドの値を計算します。その結果、コードの副作用として "Evaluating simpleField" が印字されます。

フィールドにおける本体の式は一度だけ実行されます。 その後、オブジェクトにその最終的な値が格納されます。下記で println 出力が欠けていることからわかるように、その式は二度と評価されることはありません。

Test7.simpleField
// res5: Int = 42

Test7.simpleField
// res6: Int = 42

一方、下記で println 出力が繰り返されていることからわかるように、メソッドの本体はメソッドを呼び出すたびに評価されます。

Test7.noParameterMethod
// Evaluating noParameterMethod
// res7: Int = 42

Test7.noParameterMethod
// Evaluating noParameterMethod
// res8: Int = 42

2.4.4 覚えておいてほしいこと

本節では、独自のオブジェクトを作成し、メソッドとフィールドを与え、式の中で参照しました。

文法としてオブジェクトの宣言、

object name {
  declarationOrExpression ...
}

メソッドの宣言、

def name(parameter: type, ...): resultType = bodyExpression

そしてフィールドの宣言、

val name = valueExpression
var name = valueExpression

を見てきました。これらすべては宣言で、名前と値を束縛します。宣言は式とは異なります。宣言は値を評価しませんし型も持ちません。

また、メソッドとフィールドの違いを見てきました。フィールドはオブジェクトの中に格納された値を参照し、一方のメソッドは値を算出する計算を参照します。

2.4.5 演習

2.4.5.1 キャット・オ・マティック

下記の表は、3匹の猫の名前 (name)・色 (colour)・好きなエサ (food) を示しています。それぞれの猫についてのオブジェクトを定義してください。(経験豊富なプログラマーの方へ:クラスについてはまだ説明していません。)

名前 (Name) 色 (Colour) エサ (Food)

オズワルド (Oswald)

黒 (Black)

ミルク (Milk)

ヘンダーソン (Henderson)

茶トラ (Ginger)

カリカリ (Chips)

クエンティン (Quentin)

トラ (Tabby and white)

カレー (Curry)

これはオブジェクトを定義する文法に慣れるための指の運動にすぎません。下記コードのような解答が得られているはずです。

object Oswald {
  val colour: String = "Black"
  val food: String = "Milk"
}

object Henderson {
  val colour: String = "Ginger"
  val food: String = "Chips"
}

object Quentin {
  val colour: String = "Tabby and white"
  val food: String = "Curry"
}

2.4.5.2 スクウェア・ダンス!

calc と呼ばれるオブジェクトを定義します。それは、square という Double を引数として受け取る、あなたが予想するとおり入力を2乗 (square) するメソッドを伴います。そして、計算の一部として square メソッドの呼び出し を含む cube と呼ばれる、入力を3乗 (cube) するメソッドを追加してください。

これが解答です。cube(x)square(x) を呼び出し、その値を x によってもう一度乗算します。各メソッドの結果型はコンパイラーによって Double として推論されます。

object calc {
  def square(x: Double) = x * x
  def cube(x: Double) = x * square(x)
}

2.4.5.3 精密なスクウェア・ダンス!

前の演習から calc をコピー&ペーストして、Int と同様に Double でも動作するよう一般化された calc2 を作成してください。Java の経験を持っていれば、これはかなり簡単にできるはずです。そうでなければ、下記の解答を読んでください。

Java のように Scala は特に IntDouble の間でうまく一般化できるわけではありません。しかしながら、引数の型ごとに square メソッドと cube メソッドを定義することでオーバーロード (overload) することができます。

object calc2 {
  def square(value: Double) = value * value
  def cube(value: Double) = value * square(value)

  def square(value: Int) = value * value
  def cube(value: Int) = value * square(value)
}

「オーバーロードされた」メソッドとは、異なる引数型で複数回定義したメソッドのことです。オーバーロードされたメソッド型を呼び出すたびに、Scala は引数の型を見ることによって、どの変種が必要かを自動的に判断します。

calc2.square(1.0) // `square` の `Double` 版を呼び出す
// res11: Double = 1.0

calc2.square(1)   // `square` の `Int` 版を呼び出す
// res12: Int = 1

Scala コンパイラーは、低い精度から高い精度が必要な場合に、数値型間の自動変換を挿入できます。例えば、calc.square(2) と書けば、コンパイラーは calc.square の唯一のバージョンが Double を受け取ると判断し、本当のところは calc.square(2.toDouble) という意図であると自動的に推論します。

高い精度から低い精度への逆方向の変換は、丸め誤差につながる可能性があるため、自動的には処理されません。例えば、下記のコードは、xInt であり、その本体の式が Double であるため、コンパイルされません!(実際に試してみてください。)

val x: Int = calc.square(2) // コンパイルエラー
// <console>:13: error: type mismatch;
//  found   : Double
//  required: Int
//        val x: Int = calc.square(2) // コンパイルエラー
//                                ^

これは、DoubletoInt メソッドを手動で使用することで回避できます。

val x: Int = calc.square(2).toInt // toInt は切り捨て
// x: Int = 4

文字列連結の危険性

Java と似た振る舞いを維持するために、Scala もまた必要に応じて任意のオブジェクトを自動的に String に変換します。これは、println("a" + 1) のようなものを簡単に書けるようにするためで、Scala は println("a" + 1.toString) に自動的に書き換えます。

文字列連結と数値加算が同じ + メソッドを共有しているという事実が、予期せぬバグを引き起こすことがあるので注意しましょう!

2.4.5.4 評価の順番

コンソールで入力したとき、下記プログラムの出力は何で、最終的な式の型と値は何になるでしょうか?各フィールドやメソッドの型や依存関係、評価の振る舞いをよく考えてみてください。

object argh {
  def a = {
    println("a")
    1
  }

  val b = {
    println("b")
    a + 2
  }

  def c = {
    println("c")
    a
    b + "c"
  }
}
argh.c + argh.b + argh.a

これが解答です。

argh.c + argh.b + argh.a
// b
// a
// c
// a
// a
// res13: String = 3c31

評価の完全な順番は下記のとおりです。

- プログラムの最後にあるメインの合計を計算するには、
  - `argh` を読み込み、
    - `argh` のすべてのフィールドを計算するので、
      - `b` を計算すると、
        - `"b"` を印字し、
        - `a + 2` を評価するので、
          - `a` を呼び出し、
            - `"a"` を印字し、
            - `1` を返却し、
          - `1 + 2` を返却し、
        - `b` に値 `3` を格納し、
  - `argh.c` を呼び出し、
    - `"c"` を印字し、
    - `a` を評価し、
      - `"a"` を印字し、
      - `1` を返却するのですが、それは破棄され、
    - `b + "c"` を評価し、
      - `b` から値 `3` を取得し、
        - 値 `"c"` を取得し、
        - `+` を評価し、それが実際のところ文字列の連結を参照していると判断し、
          `3` を `"3"` に変換し、
        - 文字列 `"3c"` を返却し、
  - `argh.b` を呼び出し,
    - `b` から値 `3` を取得し、
  - 最初の `+` を評価し、それが実際のところ文字列の連結を参照していると判断し、
    `"3c3"` を生成し、
  - `argh.a` を呼び出し、
    - `"a"` を印字し、
    - `1` を返却し、
  - 最初の `+` を評価し、それが実際のところ文字列の連結を参照していると判断し、
    `"3c31"` を生成する

ふぅ、こんな簡単なコードにしてはたくさんありますね。

2.4.5.5 やぁ、人間

person というオブジェクトを定義します。それは、firstNamelastName というフィールドを含みます。alien という2番目のオブジェクトを定義します。それは、引数として person を受け取り、その firstName を使用して挨拶する greet というメソッドを含みます。

greet メソッドの型は何でしょうか?このメソッドを使用して他のオブジェクトに挨拶することはできますか?

object person {
  val firstName = "Dave"
  val lastName = "Gurnell"
}

object alien {
  def greet(p: person.type) =
    "Greetings, " + p.firstName + " " + p.lastName
}

alien.greet(person)
// res15: String = Greetings, Dave Gurnell

greet の引数 p の型 person.type に注目してください。これは、先ほど言及したシングルトン型 (singleton type) のひとつです。この場合は person オブジェクトに固有の型なので、他のオブジェクトで greet を使用することはできません。これは、Scala のすべての整数で共有されている Int のような型とは大きく異なります。

これでは、Scala でプログラムを書く能力に大きな制限を課してしまいます。組み込み型か独自に作成した単一のオブジェクトで動作するメソッドしか書けないのです。有用なプログラムを構築するために、独自の型を定義し、それぞれの型で多様な値を生成する機能が必要です。クラス (class) を使用してこれを実現できるのですが、それは次節で扱います。

2.4.5.6 メソッドの真価

メソッドは値ですか?式ですか?なぜそうなるのでしょうか?

まず、メソッドと式の等価性を扱いましょう。すでに知っているように、式は値を生成するプログラムの断片です。何かが式であるかどうかの簡単なテストは、それをフィールドに代入できるかどうかを確認することです。

object calculator {
  def square(x: Int) = x * x
}

val someField = calculator.square
// <console>:15: error: missing argument list for method square in object calculator
// Unapplied methods are only converted to functions when a function type is expected.
// You can make this conversion explicit by writing `square _` or `square(_)` instead of `square`.
//        val someField = calculator.square
//                                   ^

このエラーメッセージをまだ完全に理解はできません(「部分適用関数 (partially applied function)」については後ほど学びます。)が、square式ではないことを示しています。しかしながら、square呼び出しは値を生成するのです。

val someField = calculator.square(2)
// someField: Int = 4

引数を持たないメソッドは異なる振る舞いをしているように見えます。しかしながら、これは文法のトリックです。

object clock {
  def time = System.currentTimeMillis
}

val now = clock.time
// now: Long = 1591093416177

clock.time を値として now に割り当てているように見えるにも関わらず、実際にそれは clock.time の呼び出しによって返された値を代入しています。これは、もう一度メソッドを呼び出すことで実証できます。

val aBitLaterThanNow = clock.time
// aBitLaterThanNow: Long = 1591093416354

上で見たように、Scala では「フィールドへの参照」と「引数のないメソッド呼び出し」は同じように見えます。これは、他のコードに影響を与えることなく、フィールドの実装をメソッドに置き換えることができる(逆も然り)ように設計されています。それは、統一アクセス原理 (uniform access principle) と呼ばれるプログラミング言語の機能です。

つまり、要約すると、メソッド呼び出しは式であるメソッド自体は式ではないということです。Scala は関数 (function) と呼ばれる概念を持っており、それはメソッドのように呼び出せるオブジェクトです。オブジェクトが値であることをすでに知っているように、関数は値であり、データとして取り扱うことができます。お気付きのとおり、関数は関数型プログラミング (functional programming) の重要な部分であり、Scala の大きな強みのひとつです。関数と関数型プログラミングについては少しずつ学んでいきましょう。

2.5 メソッドの書き方

前節では、メソッドの文法について見てきました。本書の主な目的のひとつは、文法を超えて、Scala プログラムを構築するための体系的な方法を提供することです。本節はそのような問題を扱う最初の節です。本節では、体系的にメソッドを構築する方法を見ていきます。Scala の経験を積んでいくうちに、この方法のいくつかの段階を省略することができますが、本書の中ではこの方法に従うことを強く推奨します。

アドバイスを具体的にするために、前節の演習を例として使用していきましょう。

calc と呼ばれるオブジェクトを定義します。それは、square という Double を引数として受け取る、あなたが予想するとおり入力を2乗 (square) するメソッドを伴います。そして、計算の一部として square メソッドの呼び出しを含む cube と呼ばれる、入力を3乗 (cube) するメソッドを追加してください。

2.5.1 入力と出力を特定する

最初の段階は、入力となる引数がもしあれば、その型とメソッドの結果型を特定することです。

多くの場合、演習では型を教えてくれるので、説明文からそのまま読み取ることができます。上の例で、入力型は Double になっています。結果型もまた Double になることが推論できます。

2.5.2 テストケースを準備する

型だけでは物語のすべてを語れません。Double から Double への関数はたくさんありますが、2乗を実行しているものは少数です。そのため、メソッドの期待される振る舞いを例証するいくつかのテストケースを準備しましょう。

外部依存を避けたいので、本書ではテストライブラリを使用しないことにします。Scala が提供する assert 関数を使用することで手軽なテストライブラリを実装できます。square の例については、下記のようなテストケースが考えられます。

assert(square(2.0) == 4.0)
assert(square(3.0) == 9.0)
assert(square(-2.0) == 4.0)

2.5.3 宣言を書く

型とテストケースが準備できたので、メソッド宣言を書くことができます。その本体はまだ書けないので、その場所に Scala の便利な機能である ??? を使用しておきます。

def square(in: Double): Double =
  ???

この段階は、前の段階で収集した情報から機械的に与えられるべきです。

2.5.4 コードを実行する

コードを実行し、それがコンパイルされていること(つまり、タイプミスをしていないこと)と、また、テストが失敗すること(つまり、何らかのテストが実行されていること)を確認してください。なお、メソッド宣言の後にテストを配置する必要があるかもしれません。

2.5.5 本体を書く

メソッドの本体を書く準備が整いました。本書を通じて、いくつかのテクニックを修得していきます。現時点では、2つのテクニックを見ていきましょう。

2.5.5.1 結果型を考える

最初のテクニックは、結果型を見ることです。この場合は Double です。どうやって Double の値を生成するのでしょうか?リテラルを書くこともできますが、この場合は明らかに正しくありません。Double を生成する他の方法は、何らかのオブジェクトのメソッドを呼び出すことで、それは次のテクニックがもたらします。

2.5.5.2 入力型を考える

次のテクニックは、メソッドの入力引数の型を見ることです。この場合は Double です。Double を生成する必要があることは確立していますが、入力から Double を生成するためにはどのようなメソッドを呼び出せばいいのでしょうか?そのようなメソッドはたくさんありますが、ここで呼び出すべき正しいメソッドとして * を選択するためにはドメイン知識を活用しなければなりません。

完全なメソッドは下記のように書くことができます。

def square(in: Double): Double =
  in * in

2.5.6 コードを再実行する

最後に、コードを再実行して、テストがすべて通過することを確認します。

これはとても単純な例でしたが、今のプロセスを実践することで、今後遭遇するであろうより複雑な例にうまく対応できるようになります。

メソッドを書く方法

体系的にメソッドを書くための6段階の方法があります。

  1. メソッドの入力型と出力型を特定する。
  2. 入力例を与えられたメソッドの期待される出力について、いくつかのテストケースを書く。これらのケースを書き出すために assert 関数を使用することができる。
  3. 下記のようにメソッドの本体に ??? を使用してメソッドの宣言を書く。
def name(parameter: type, ...): resultType =
 ???
  1. コードを実行し、テストケースが実際に失敗することを確認する。
  2. メソッドの本体を書く。現在のところ、ここで2つのテクニックを適用することができる。
  • 結果型とそのインスタンスをどのように生成できるかを考える
  • 入力型とそれを結果型に変化させるために呼び出せるメソッドを考える
  1. コードを再実行し、テストケースが通過することを確認する。

2.6 複合式

これで Scala の基本的な入門編はほぼ終了しました。本節では、より複雑なプログラムで必要になるであろう、条件式 (conditional)ブロック (block) という2種類の特別な式について見ていきます。

2.6.1 条件式

条件式は、ある条件にもとづいて評価する式を選択することを可能にします。例えば、2つの数値のうちどちらが小さいかにもとづいて文字列を選択することができます。

if(1 < 2) "Yes" else "No"
// res0: String = Yes

条件式は式である

Scala における if の記述は Java と同じ構文を持ちます。重要な違いのひとつは、Scala の条件式は式であるということで、それは型と返却値を持ちます。

選択されない式は評価されません。これは、副作用を伴う式を使用すると明らかです。

if(1 < 2) println("Yes") else println("No")
// Yes

No がコンソールに出力されないので、println("No") という式は評価されていないことがわかります。

条件式文法

条件式のための文法は、

if(condition)
  trueExpression
else
  falseExpression

です。ここで、

  • conditionBoolean 型の式
  • trueExpressionconditiontrue に評価されるときに評価される式
  • falseExpressionconditionfalse に評価されるときに評価される式

とします。

2.6.2 ブロック

ブロックは、計算をまとめて並べることができる式のことです。それは、セミコロンもしくは改行によって区切られた部分式を含む中括弧のペアとして書かれます。

{ 1; 2; 3 }
// <console>:13: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
//        { 1; 2; 3 }
//          ^
// <console>:13: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
//        { 1; 2; 3 }
//             ^
// error: No warnings can be incurred under -Xfatal-warnings.

ご覧のように、このコードを実行すると、コンソールはいくつかの警告を発生させ、Int3 を返します。

ブロックは、中括弧によって囲まれた一連の式や宣言のことです。ブロックもまた式であり、各部分式を順番に実行し、最後の式の値を返します。

12 の値を捨ててしまうなら、なぜそれらを実行するのでしょうか?これはいい質問で、上記で Scala コンパイラーが警告を発生させた理由にもなります。

ブロックを使用するひとつの理由は、最終的な値を計算する前に副作用を生成するコードを使用したいからです。

{
  println("This is a side-effect")
  println("This is a side-effect as well")
  3
}
// This is a side-effect
// This is a side-effect as well
// res3: Int = 3

また、下記のように中間結果に名前をつけたいときにブロックを使用することもできます。

def name: String = {
  val title = "Professor"
  val name = "Funkenstein"
  title + " " + name
}
name
// res4: String = Professor Funkenstein

ブロック式文法

ブロック式の文法は、

{
   declarationOrExpression ...
   expression
}

です。ここで、

  • declarationOrExpression は宣言か式(オプション)
  • expression はブロック式の型と値を決定する式

とします。

2.6.3 覚えておいてほしいこと

条件式は Boolean の条件にもとづいて評価する式を選択することを可能にします。その文法は下記のとおりです。

if(condition)
  trueExpression
else
  falseExpression

式である条件式は、型を持ち、オブジェクトに評価されます。

ブロックは、式や宣言を順番に並べることを可能にします。それは、副作用を伴う式を順番に並べたり、計算における中間結果に名前をつけたりしたいときによく使用されます。その文法は下記のとおりです。

{
   declarationOrExpression ...
   expression
}

ブロックの型と値は、ブロックにおける最後の式になります。

2.6.4 演習

2.6.4.1 模範的なライバル関係

下記の条件式における型と値は何ですか?

if(1 > 2) "alien" else "predator"

それは、値 "predator" を伴う String です。明らかにプレデターは最高です。

if(1 > 2) "alien" else "predator"
// res6: String = predator

型は then 式と else 式における型の上限境界によって決まります。この場合、両式は String なので、その結果もまた String になります。

値は実行時に決まります。21 より大きいので、条件式は else 式の値を評価します。

2.6.4.2 あまり知られていないライバル関係

この条件式はどうでしょうか?

if(1 > 2) "alien" else 2001

それは値 2001 を伴う Any です。

if(1 > 2) "alien" else 2001
// res8: Any = 2001

これは前の演習と似ていますが、その違いは結果型です。先ほど、型は真偽両式の上限境界 (upper bound) であることを見ました。"alien"2001 は全く異なる型なので、最も近い共通の祖先は、すべての Scala 型における最高位の基底型 Any になります。

これは、型がプログラムを実行する前のコンパイル時に決まるという重要な観測結果です。コンパイラーはプログラムを実行する前に 12 のどちらが大きいかを知らないので、条件式の結果型から最良の推量をするしかないのです。前の演習では String に至るまでの道のりを得ることができましたが、このプログラムでは Any が限りなく近いものになっています。

Any については以降の節で詳しく説明します。なお、それは IntBoolean のような値型を包含するので、Java プログラマーは Object と混同してはいけません。

2.6.4.3 else のない if

この条件式はどうでしょうか?

if(false) "hello"

結果の型と値はそれぞれ Any() です。

if(false) "hello"
// res10: Any = ()

すべてのコードが等しくあるべきですが、else 式のない条件式の半数のみ値に評価されます。Scala は、else への分岐が評価されるべき場合に Unit 値を返すことでこの問題を回避しています。これらの式はたいてい副作用がある場合にのみ使用します。

2.7 まとめ

本章では、Scala の基礎をとても簡単に紹介しました。

たくさんのオブジェクトについてリテラルで書く方法や、既存のオブジェクトから新しいオブジェクトを生成するためにメソッド呼び出しや複合式を使用する方法を見てきました。

また、独自のオブジェクトを宣言し、メソッドやフィールドを構築しました。

次章では、新しい種類の宣言であるクラスが、どのようにオブジェクトを生成するためのテンプレートを提供するのかを見ていきます。クラスは、コードを再利用したり、共通の型で類似のオブジェクトを統一したりすることを可能にします。

3 オブジェクトとクラス

前章では、オブジェクトを作成し、メソッド呼び出しを通じてオブジェクトを作用させる方法を見ました。本章では、クラス (class) を使用してオブジェクトを抽象化する方法を見ていきます。クラスはオブジェクトを構築するためのテンプレートです。クラスが与えられれば、同じ型を持ち、共通のプロパティを持つ多くのオブジェクトをつくることができます。

3.1 クラス

クラスは、似たようなメソッドやフィールドを持つオブジェクトを生成するためのテンプレートです。Scala では、クラスもまた型を定義し、クラスから生成されたすべてのオブジェクトは同じ型を共有します。これによって、前章の演習「やぁ、人間」が持つ問題を克服することができます。

3.1.1 クラスを定義する

これは、シンプルな Person クラスの宣言です。

class Person {
  val firstName = "Noel"
  val lastName = "Welsh"
  def name = firstName + " " + lastName
}

オブジェクト宣言のように、クラス宣言は名前(この場合 Person)を束縛し、また式ではありません。しかしながら、オブジェクト名と違って、式の中でクラス名を使用することはできません。クラスは値ではなく、クラスは異なる名前空間 (namespace) に住んでいます。

Person
// <console>:13: error: not found: value Person
//        Person
//        ^

new 演算子を使用して、新しい Person オブジェクトを生成できます。オブジェクトは値なので、通常の方法でそれらのメソッドやフィールドにアクセスできます。

val noel = new Person
// noel: Person = Person@15bfe8fd

noel.firstName
// res1: String = Noel

オブジェクトの型が Person であることに注意してください。印字された値には、@xxxxxxxx という形式のコードが含まれており、そのオブジェクトを特定する一意の識別子です。new を呼び出すたびに、同じ型の別オブジェクトが生成されます。

noel
// res2: Person = Person@15bfe8fd

val newNoel = new Person
// newNoel: Person = Person@60cea738

val anotherNewNoel = new Person
// anotherNewNoel: Person = Person@22fe5689

これは、引数として任意の Person を受け取るメソッドを書けることを意味します。

object alien {
  def greet(p: Person) =
    "Greetings, " + p.firstName + " " + p.lastName
}
alien.greet(noel)
// res3: String = Greetings, Noel Welsh

alien.greet(newNoel)
// res4: String = Greetings, Noel Welsh

Java ヒント

Scala クラスはすべて java.lang.Object の派生クラスであり、ほとんどの場合、Scala と同様に Java からも使用できます。Person におけるデフォルト印字の振る舞いは、java.lang.Object に定義されている toString メソッドに由来しています。

3.1.2 コンストラクター

現状、Person クラスは全然役に立ちません。新しいオブジェクトを好きなだけ生成することができますが、すべて同じ firstNamelastName を持つためです。それでは、それぞれの人に異なる名前を与えるにはどうすればいいのでしょうか?

その解決策は、コンストラクター (constructor) を導入することで、それは新しいオブジェクトを生成するときに引数を渡すことを可能にします。

class Person(first: String, last: String) {
  val firstName = first
  val lastName = last
  def name = firstName + " " + lastName
}
val dave = new Person("Dave", "Gurnell")
// dave: Person = Person@258c10ba

dave.name
// res5: String = Dave Gurnell

コンストラクター引数 firstlast はクラスの本体でのみ使用できます。オブジェクトの外側からデータにアクセスするには、valdef を使用してフィールドやメソッドを宣言しなければなりません。

コンストラクター引数とフィールドはしばしば冗長です。幸運なことに、Scala は両方を一度に宣言する便利な略記法を提供してくれています。コンストラクター引数の前に val キーワードをつけることで、Scala はそれらのフィールドを自動的に定義してくれるようになります。

class Person(val firstName: String, val lastName: String) {
  def name = firstName + " " + lastName
}
new Person("Dave", "Gurnell").firstName
// res6: String = Dave

val フィールドは不変 (immutable) で、一度初期化された後にそれらの値を変更することはできません。Scala は可変 (mutable) フィールドを定義するために var キーワードも提供しています。

Scala プログラマーは、不変かつ副作用のないコードを書くことを好む傾向があるので、置換モデルを使用してそれらを導出することができます。本書では、もっぱら不変な val フィールドに焦点を当てていきます。

クラス宣言文法

クラスを宣言するための文法は、

class Name(parameter: type, ...) {
  declarationOrExpression ...
}

class Name(val parameter: type, ...) {
  declarationOrExpression ...
}

です。ここで、

  • Name はクラスの名前
  • parameter はコンストラクター引数に与えられた名前(オプション)
  • type はコンストラクター引数の型
  • declarationOrExpression は宣言や式(オプション)

とします。

3.1.3 デフォルト引数とキーワード引数

Scala のすべてのメソッドとコンストラクターは、キーワード引数 (keyword parameter)デフォルト引数値 (default parameter value) に対応しています。

メソッドやコンストラクターを呼び出すときに、任意の順番で引数を指定するためのキーワードとして引数名を使用することができます。

new Person(lastName = "Last", firstName = "First")
// res7: Person = Person@5cce4fcc

これは、下記のように定義されたデフォルト引数値と組み合わせて使用するといっそう便利です。

def greet(firstName: String = "Some", lastName: String = "Body") =
  "Greetings, " + firstName + " " + lastName + "!"

引数がデフォルト値を持つのであれば、メソッド呼び出しでその引数を省略できます。

greet("Busy")
// res8: String = Greetings, Busy Body!

デフォルト引数値とキーワードを組み合わせることで、前の引数を省略し、後の引数だけに値を渡すこともできます。

greet(lastName = "Dave")
// res9: String = Greetings, Some Dave!

キーワード引数

キーワード引数は、引数の数や順番の変更に対して堅牢です。例えば、greet メソッドに title 引数を追加すると、キーワードなしのメソッド呼び出しにおいて意味が変化してしまいますが、キーワードありの呼び出しでは同じままです。

def greet(title: String = "Citizen", firstName: String = "Some", lastName: String = "Body") =
  "Greetings, " + title + " " + firstName + " " + lastName + "!"

greet("Busy") // これは正しくなくなりました
// res10: String = Greetings, Busy Some Body!

greet(firstName = "Busy") // これは正しいままです
// res11: String = Greetings, Citizen Busy Body!

これは、多数の引数を伴うメソッドやコンストラクターを作成するとき、特に便利です。

3.1.4 Scala の型階層

プリミティブ型とオブジェクト型を区別する Java とは異なり、Scala ではすべてがオブジェクトです。その結果、IntBoolean のような「プリミティブ」値の型は、クラスやトレイトと同じ型階層の一部を構成しています。

Scala 型階層

Scala は Any と呼ばれる最上位の基底型を持ち、その下に AnyValAnyRef という2つの型があります。AnyVal はすべての値型の基底型で、AnyRef はすべての参照型やクラスの基底型です。Scala や Java におけるすべてのクラスは AnyRef の派生型です4

それらの型のいくつかは、単純に Java に存在する型の Scala における別名で、IntintBooleanbooleanAnyRefjava.lang.Object です。

階層の最下位 (bottom) に2つの特別な型があります。Nothingthrow 式の型で、Nullnull 値の型です。それらの特別な型は、他のすべての型の派生型で、コードにおける他の型を健全に保ちながらも、thrownull に型を割り当てることを補助します。下記のコードはこのことを説明しています。

def badness = throw new Exception("Error")
// badness: Nothing

def otherbadness = null
// otherbadness: Null

val bar = if(true) 123 else badness
// bar: Int = 123

val baz = if(false) "it worked" else otherbadness
// baz: String = null

badnessotherbadness の型はそれぞれ NothingNull であるにも関わらず、barbaz の型は実用的なままです。なぜならば、IntIntNothing の最小共通基底型で、StringStringNull の最小共通基底型であるからです。

3.1.5 覚えておいてほしいこと

本節では、同じを持つ様々なオブジェクトの生成を可能にするクラスを定義する方法を学びました。このように、クラスを使うことで、似たような性質を持つオブジェクトを横断的に抽象化することができます。

クラスにおいてもオブジェクトの性質は、フィールドメソッドの形をとります。フィールドはオブジェクトに格納される事前に計算された値で、メソッドは呼び出すことができる計算です。

クラスを宣言するための文法は、

class Name(parameter: type, ...) {
  declarationOrExpression ...
}

です。

new キーワードを使用し、コンストラクターを呼び出すことによって、クラスからオブジェクトを生成します。

また、キーワード引数デフォルト引数についても学びました。

最後に、Scala の型階層について、Java の型階層との重複や、特殊な型である AnyAnyRefAnyValNothingNullUnit を含むこと、そして Java のクラスと Scala のクラスは、どちらも型階層の同じサブツリーを占有するという事実を学びました。

3.1.6 演習

今ではクラスで楽しく遊ぶために十分な機構を手に入れました。

3.1.6.1 猫、再び

以前の演習に登場した猫を思い出しましょう。

名前 (Name) 色 (Colour) エサ (Food)

オズワルド (Oswald)

黒 (Black)

ミルク (Milk)

ヘンダーソン (Henderson)

茶トラ (Ginger)

カリカリ (Chips)

クエンティン (Quentin)

トラ (Tabby and white)

カレー (Curry)

Cat クラスを定義し、上の表の各猫についてオブジェクトを生成してください。

これはクラスを定義する文法に慣れるための指の運動です。

class Cat(val colour: String, val food: String)

val oswald = new Cat("Black", "Milk")
val henderson = new Cat("Ginger", "Chips")
val quentin = new Cat("Tabby and white", "Curry")

3.1.6.2 猫、ぶらぶらする

willServe メソッドを伴う ChipShop オブジェクトを定義してください。このメソッドは Cat を受け取り、猫の好きなエサがカリカリ (chips) であれば true を返し、そうでなければ false を返します。

object ChipShop {
  def willServe(cat: Cat): Boolean =
    if(cat.food == "Chips")
      true
    else
      false
}

3.1.6.3 監督デビュー

下記のフィールドとメソッドを伴う、DirectorFilm の2つのクラスを書いてください。

下記のデモデータをコードにコピー&ペーストし、そのコードを変更せずに動作するようコンストラクターを調整してください。

val eastwood          = new Director("Clint", "Eastwood", 1930)
val mcTiernan         = new Director("John", "McTiernan", 1951)
val nolan             = new Director("Christopher", "Nolan", 1970)
val someBody          = new Director("Just", "Some Body", 1990)

val memento           = new Film("Memento", 2000, 8.5, nolan)
val darkKnight        = new Film("Dark Knight", 2008, 9.0, nolan)
val inception         = new Film("Inception", 2010, 8.8, nolan)

val highPlainsDrifter = new Film("High Plains Drifter", 1973, 7.7, eastwood)
val outlawJoseyWales  = new Film("The Outlaw Josey Wales", 1976, 7.9, eastwood)
val unforgiven        = new Film("Unforgiven", 1992, 8.3, eastwood)
val granTorino        = new Film("Gran Torino", 2008, 8.2, eastwood)
val invictus          = new Film("Invictus", 2009, 7.4, eastwood)

val predator          = new Film("Predator", 1987, 7.9, mcTiernan)
val dieHard           = new Film("Die Hard", 1988, 8.3, mcTiernan)
val huntForRedOctober = new Film("The Hunt for Red October", 1990, 7.6, mcTiernan)
val thomasCrownAffair = new Film("The Thomas Crown Affair", 1999, 6.8, mcTiernan)
eastwood.yearOfBirth
// res16: Int = 1930

dieHard.director.name
// res17: String = John McTiernan

invictus.isDirectedBy(nolan)
// res18: Boolean = false

さらに、copy と呼ばれる Film のメソッドを実装します。このメソッドは、コンストラクターと同じ引数を受け取り、映画の新しいコピーを生成します。各引数にデフォルト値を与えて、映画の値の一部を変更してコピーできるようにします。

highPlainsDrifter.copy(name = "L'homme des hautes plaines")
// res19: Film = Film(L'homme des hautes plaines,1973,7.7,Director(Clint,Eastwood,1930))

thomasCrownAffair.copy(yearOfRelease = 1968,
  director = new Director("Norman", "Jewison", 1926))
// res20: Film = Film(The Thomas Crown Affair,1968,6.8,Director(Norman,Jewison,1926))

inception.copy().copy().copy()
// res21: Film = Film(Inception,2010,8.8,Director(Christopher,Nolan,1970))

この演習は、Scala のクラスやフィールド、メソッドの記述をいくつかのハンズオンによる体験として提供します。模範解答は下記のようになります。

class Director(
  val firstName: String,
  val lastName: String,
  val yearOfBirth: Int) {

  def name: String =
    s"$firstName $lastName"

  def copy(
    firstName: String = this.firstName,
    lastName: String = this.lastName,
    yearOfBirth: Int = this.yearOfBirth): Director =
    new Director(firstName, lastName, yearOfBirth)
}

class Film(
  val name: String,
  val yearOfRelease: Int,
  val imdbRating: Double,
  val director: Director) {

  def directorsAge =
    yearOfRelease - director.yearOfBirth

  def isDirectedBy(director: Director) =
    this.director == director

  def copy(
    name: String = this.name,
    yearOfRelease: Int = this.yearOfRelease,
    imdbRating: Double = this.imdbRating,
    director: Director = this.director): Film =
    new Film(name, yearOfRelease, imdbRating, director)
}

3.1.6.4 シンプル・カウンター

Counter クラスを実装してください。コンストラクターは Int を受け取るようにしてください。カウンターを増加させる inc メソッドとカウンターを減少させる dec メソッドを、それぞれ新しい Counter を返すように実装してください。こちらが利用例です。

new Counter(10).inc.dec.inc.inc.count
// res23: Int = 12
class Counter(val count: Int) {
  def dec = new Counter(count - 1)
  def inc = new Counter(count + 1)
}

クラスやオブジェクトによる実践の傍ら、この演習には2つ目の目的があります。それは、なぜ incdec が、同じカウンターを直接更新する代わりに、新しい Counter を返すのかを考えることです。

val フィールドは不変なので、count の新しい値を伝える他の方法を思い付く必要があります。新しい Counter オブジェクトを返すメソッドは、代入による副作用なしに新しい状態を返す方法を与えてくれます。それはまたメソッドチェーン (method chaining) を可能にし、一連の更新全体をひとつの式で書くことを可能にします。

実際のところ、利用例 new Counter(10).inc.dec.inc.inc.count は、最後の Int 値を返すまでに Counter のインスタンスを5つ生成します。このような単純計算のための、余分なメモリと CPU のオーバーヘッドを気にするかもしれませんが、その心配はありません。JVM のような最新の実行環境では、このスタイルのプログラミングにおける余分なオーバーヘッドは、性能が最重要なコードを除き、無視して構わないものになっています。

3.1.6.5 高速カウント

前の演習に登場した Counter を拡張し、ユーザーが incdecInt 引数を任意に渡せるようにしてください。引数が省略されたときは、1 をデフォルトにしてください。

最も単純な解答はこのようになります。

class Counter(val count: Int) {
  def dec(amount: Int = 1) = new Counter(count - amount)
  def inc(amount: Int = 1) = new Counter(count + amount)
}

しかし、これは incdec に丸括弧を追加します。引数を省略したときにも、空の丸括弧を付与しなければなりません。

new Counter(10).inc
// <console>:14: error: missing argument list for method inc in class Counter
// Unapplied methods are only converted to functions when a function type is expected.
// You can make this conversion explicit by writing `inc _` or `inc(_)` instead of `inc`.
//        new Counter(10).inc
//                        ^

本来の丸括弧なしのメソッドを再現するには、メソッドのオーバーロード (method overloading) を使用して、丸括弧の付与を回避します。

class Counter(val count: Int) {
  def dec: Counter = dec()
  def inc: Counter = inc()
  def dec(amount: Int = 1): Counter = new Counter(count - amount)
  def inc(amount: Int = 1): Counter = new Counter(count + amount)
}

new Counter(10).inc.inc(10).count
// res25: Int = 21

3.1.6.6 追加カウント

こちらは Adder と呼ばれるシンプルなクラスです。

class Adder(amount: Int) {
  def add(in: Int) = in + amount
}

Counter を拡張し、adjust と呼ばれるメソッドを追加してください。このメソッドは Adder を受け入れ、countAdder を適用した結果を伴う新しい Counter を返します。

class Counter(val count: Int) {
  def dec = new Counter(count - 1)
  def inc = new Counter(count + 1)
  def adjust(adder: Adder) =
    new Counter(adder.add(count))
}

これは興味深いパターンで、Scala の機能を学ぶにつれて、より強力になっていくでしょう。計算を表現するために Adder を使用し、それを Counter に受け渡しています。メソッドは式ではないという先の議論を思い出してください。それらはフィールドに格納したり、データとして受け渡したりできません。しかしながら、Adder はオブジェクトであると同時に計算でもあるのです。

オブジェクト指向プログラミング言語において、計算としてオブジェクトを使用することは一般的なパラダイムです。例えば、Java の Swing における古典的な ActionListener を考えてみてください。

public class MyActionListener implements ActionListener {
  public void actionPerformed(ActionEvent evt) {
    // 何か計算を実行する
  }
}

AddersActionListener のようなオブジェクトの欠点は、特定の状況での使用に限定されることです。Scala には、オブジェクトとして様々な計算の表現を可能にする、関数 (function) と呼ばれるより一般的な概念が含まれています。本章では、関数の背後にある概念のいくつかを紹介していきます。

3.2 関数としてのオブジェクト

前節、最後の演習で Adder と呼ばれるクラスを定義しました。

class Adder(amount: Int) {
  def add(in: Int): Int = in + amount
}

議論の中で、計算を表現するオブジェクトとしての Adder を説明しました。それは、値として渡すことのできるメソッドを得られたようなものでした。

計算のように振る舞うオブジェクトは強力な概念で、Scala にはそれを生成するための言語機能が完全に備わっています。それらのオブジェクトは関数 (function) と呼ばれ、関数型プログラミング (functional programming) の基礎を成します。

3.2.1 apply メソッド

ここでは、関数型プログラミングをサポートする Scala の一機能関数適用文法 (function application syntax) を見ていきましょう。

Scala では、慣例によって、apply と呼ばれるメソッドを持つオブジェクトを関数のように「呼び出す」ことができます。apply と名付けられたメソッドによって、呼び出し文法 foo.apply(args)foo(args) になるという特別な略記法が与えられます。

例えば、Adderadd メソッドを apply に改名してみましょう。

class Adder(amount: Int) {
  def apply(in: Int): Int = in + amount
}
val add3 = new Adder(3)
// add3: Adder = Adder@3390766f

add3.apply(2)
// res0: Int = 5

add3(4) // add3.apply(4) の略記法
// res1: Int = 7

この簡単なトリックによって、オブジェクトは文法的に関数らしく「見える」ようになります。オブジェクトを、変数に代入したり、引数として受け渡したり、メソッドではできなかったことがたくさんできます。

関数適用文法

メソッド呼び出し object.apply(parameter, ...)object(parameter, ...) と書くこともできる。

3.2.2 覚えておいてほしいこと

本節では、オブジェクトを関数であるかのように「呼び出す」ための関数適用文法を見ました。

関数適用文法は、apply メソッドが定義されたどんなオブジェクトでも利用可能です。

関数適用文法によって、計算のように振る舞う第一級値(訳注:第一級関数)を持てるようになりました。メソッドと違って、オブジェクトはデータとして受け渡すことができます。これで、Scala における真の関数型プログラミングに一歩近付きました。

3.2.3 演習

3.2.3.1 関数が関数でないのはどんなとき?

次節の最後に、いくつかのコードを書く機会があります。ここで、重要な理論上の疑問について考えてみましょう。

関数適用文法は、計算を実行する真に再利用可能なオブジェクトを生成できることに、どのくらい近付いているのでしょうか?何が足りないのでしょうか?

主に足りていないものは、値を横断的に抽象化する方法であるです。

現時点では、数値を加算するという知識を捕捉するために Adder と呼ばれるクラスを定義できますが、他の開発者はその知識を使用するために、この特定のクラスについて知っている必要があるため、そのコードはコードベースを横断して可搬であるとは言えません。

HandlerCallbackAdderBinaryAdder などのような名前で共通する関数型のライブラリを定義できますが、これはすぐに非現実的になってしまいます。

後ほど、様々な状況で使用できる一般的な関数型一式を定義することで、Scala がこの問題にどのように対処しているのかを見ていきましょう。

3.3 コンパニオンオブジェクト

論理的にはクラスに所属していても、どの特定のオブジェクトからも独立しているメソッドを作成したいことがあります。そのために Java では静的メソッド (static method) を使用しますが、Scala にはシングルトンオブジェクトというもっと単純な解決策があります。

一般的な使用例は補助コンストラクターです。Scala がクラスについて複数のコンストラクターを定義できる文法を持つにも関わらず、Scala プログラマーはほとんどの場合、クラスと同じ名前を持つオブジェクトに、追加のコンストラクターとして apply メソッドを実装することを好みます。そのオブジェクトをクラスのコンパニオンオブジェクト (companion object) と呼びます。例えば、下記のようになります。

class Timestamp(val seconds: Long)

object Timestamp {
  def apply(hours: Int, minutes: Int, seconds: Int): Timestamp =
    new Timestamp(hours*60*60 + minutes*60 + seconds)
}
Timestamp(1, 1, 1).seconds
// res1: Long = 3661

コンソールを有効活用する

上記の例は、:paste コマンドを使用することに注意してください。コンパニオンオブジェクトは、後援するクラスと同じコンパイル単位で定義されなければなりません。通常のコードベースで、これはクラスとオブジェクトを同じファイルの中に定義することを意味しますが、REPL 上では :paste を使用し、それらをひとつのコマンドとして入力しなければなりません。

REPL 上に :help と入力することでより詳細を知ることができます。

前述のように、Scala は、型名 (type name) 空間と値名 (value name) 空間の2つの名前空間を持ちます。このように分離することで、衝突なくクラスとコンパニオンオブジェクトに同じ名前を付けることができます。

シングルトンオブジェクトは独自の型を伴うので、コンパニオンオブジェクトはクラスのインスタンスではないという重要なことに注意してください。

Timestamp // 型は `Timestamp.type` であり `Timestamp` ではないことに注意
// res2: Timestamp.type = Timestamp$@74d89b8f

コンパニオンオブジェクト文法

クラスのためにコンパニオンオブジェクトを定義するには、同じ名前のオブジェクトをクラスと同じファイルに定義します。

class Name {
  ...
}

object Name {
  ...
}

3.3.1 覚えておいてほしいこと

コンパニオンオブジェクトは、機能をクラスのインスタンスに関連付けることなく、クラスに関連付ける手段を提供します。一般的に、それらは追加のコンストラクターを提供するために使用されます。

コンパニオンオブジェクトは Java の静的メソッドを代替します。それらは等価な機能を提供し、より柔軟です。

コンパニオンオブジェクトは、関連付けられたクラスと同じ名前を持ちます。 これは、値の名前空間と型の名前空間という2つの名前空間を Scala が持つため、名前の衝突を引き起こしません。

コンパニオンオブジェクトは、関連付けられたクラスと同じファイルに定義されなければいけません。 REPL 上で入力するときは :paste モードを使用して、クラスとコンパニオンオブジェクトが同じコードブロックで入力されなければいけません。

3.3.2 演習

3.3.2.1 フレンドリーな人ファクトリ

姓と名を個別にではなく、姓名全体をひとつの文字列として受け取る apply メソッドを含む、Person のコンパニオンオブジェクトを実装してください。

ヒント:下記のようにして StringArray の要素に分割できます。

val parts = "John Doe".split(" ")
// parts: Array[String] = Array(John, Doe)

parts(0)
// res3: String = John

こちらがコードです。

class Person(val firstName: String, val lastName: String) {
  def name: String =
    s"$firstName $lastName"
}

object Person {
  def apply(name: String): Person = {
    val parts = name.split(" ")
    new Person(parts(0), parts(1))
  }
}

こちらが使用例です。

Person.apply("John Doe").firstName // 完全なメソッド呼び出し
// res5: String = John

Person("John Doe").firstName // 糖衣適用文法
// res6: String = John

3.3.2.2 業績の派生的な内容

下記のように、DirectorFilm のためのコンパニオンオブジェクトを記述してください。

この演習は、たくさんのコードを書く練習を提供することを意図しています。前節のクラス定義を含む模範回答はこのようになります。

class Director(
  val firstName: String,
  val lastName: String,
  val yearOfBirth: Int) {

  def name: String =
    s"$firstName $lastName"

  def copy(
    firstName: String = this.firstName,
    lastName: String = this.lastName,
    yearOfBirth: Int = this.yearOfBirth) =
    new Director(firstName, lastName, yearOfBirth)
}

object Director {
  def apply(firstName: String, lastName: String, yearOfBirth: Int): Director =
    new Director(firstName, lastName, yearOfBirth)

  def older(director1: Director, director2: Director): Director =
    if (director1.yearOfBirth < director2.yearOfBirth) director1 else director2
}

class Film(
  val name: String,
  val yearOfRelease: Int,
  val imdbRating: Double,
  val director: Director) {

  def directorsAge =
    director.yearOfBirth - yearOfRelease

  def isDirectedBy(director: Director) =
    this.director == director

  def copy(
    name: String = this.name,
    yearOfRelease: Int = this.yearOfRelease,
    imdbRating: Double = this.imdbRating,
    director: Director = this.director) =
    new Film(name, yearOfRelease, imdbRating, director)
}

object Film {
  def apply(
    name: String,
    yearOfRelease: Int,
    imdbRating: Double,
    director: Director): Film =
    new Film(name, yearOfRelease, imdbRating, director)

  def newer(film1: Film, film2: Film): Film =
    if (film1.yearOfRelease < film2.yearOfRelease) film1 else film2

  def highestRating(film1: Film, film2: Film): Double = {
    val rating1 = film1.imdbRating
    val rating2 = film2.imdbRating
    if (rating1 > rating2) rating1 else rating2
  }

  def oldestDirectorAtTheTime(film1: Film, film2: Film): Director =
    if (film1.directorsAge > film2.directorsAge) film1.director else film2.director
}

3.3.2.3 型か値か?

クラスとコンパニオンオブジェクトの名前付けによる類似は、新しい Scala 開発者の混乱を引き起こしがちです。コードのブロックを読んでいるとき、その部分がクラス()を示しているのか、シングルトンオブジェクト()を示しているのかについて知っていることが重要です。

「型か値か?」 という新しくヒットしそうなクイズを思いついたので、これから試してみましょう。単語 Film の参照しているものが型か値かをそれぞれの事例で特定してください。

val prestige: Film = bestFilmByChristopherNolan()

型!このコードは、型 Film の値 prestige を定義しています。

new Film("Last Action Hero", 1993, mcTiernan)

型!これは、Filmコンストラクターへの参照です。このコンストラクターは、であるクラス Film の一部です。

Film("Last Action Hero", 1993, mcTiernan)

値!これは、下記の略記法です。

Film.apply("Last Action Hero", 1993, mcTiernan)

apply は、シングルトンオブジェクト(値)Film に定義されたメソッドです。

Film.newer(highPlainsDrifter, thomasCrownAffair)

値!newer は、シングルトンオブジェクト Film に定義された別のメソッドです。

最後は難しいものを……

Film.type

値!これは巧妙です!これを間違えたとしても許されるでしょう。

Film.type は、シングルトンオブジェクト Film の型を参照するので、この場合の Film は値への参照です。しかしながら、コードの断片全体は型になります。

3.4 ケースクラス

ケースクラス (case class) は、クラスやコンパニオンオブジェクト、たくさんの実用的なデフォルト機能をまとめて定義してくれる非常に便利な略記法です。それは、やっかいごとを最小限にして、軽量なデータ保持クラスを生成するための理想的な方法になります。

ケースクラスは、クラス定義にキーワード case を前置するだけで生成されます。

case class Person(firstName: String, lastName: String) {
  def name = firstName + " " + lastName
}

ケースクラスを定義するとき、いつでも Scala は自動的にクラスとコンパニオンオブジェクトを生成します。

val dave = new Person("Dave", "Gurnell") // クラスを持つ
// dave: Person = Person(Dave,Gurnell)

Person // コンパニオンオブジェクトも
// res0: Person.type = Person

さらに、クラスとコンパニオンオブジェクトには、とても便利ないくつかの機能があらかじめ用意されています。

3.4.1 ケースクラスの機能

  1. 各コンストラクター引数のフィールド。コンストラクター定義に val と書く必要はありませんし、明示的に書いても悪影響はありません。
dave.firstName
// res1: String = Dave
  1. デフォルト toString メソッド。それは、クラスのコンストラクターに似た実用的な表現を印字します。もう @ 記号と謎めいた16進数とはおさらばです。
dave
// res2: Person = Person(Dave,Gurnell)
  1. 実用的な equals メソッドと hashCode メソッド。それらは、オブジェクトのフィールド値に基づいて動作します。

これは、ListSetMap のようなコレクションでケースクラスを使いやすくします。また、オブジェクトを参照 ID の代わりに、それらの内容に基づいて比較できるようになります。

new Person("Noel", "Welsh").equals(new Person("Noel", "Welsh"))
// res3: Boolean = true

new Person("Noel", "Welsh") == new Person("Noel", "Welsh")
// res4: Boolean = true
  1. copy メソッドcopy メソッドは、現在のオブジェクトと同じフィールド値を伴う新しいオブジェクトを生成します。
dave.copy()
// res5: Person = Person(Dave,Gurnell)

copy メソッドは、現在のオブジェクトの代わりに、クラスの新しいオブジェクトを生成して返すことに注意してください。

実のところ、copy メソッドは、それぞれのコンストラクター引数と一致する任意の引数を受け取ります。引数が指定されれば、新しいオブジェクトは、既存のオブジェクトに存在する値の代わりにその値を使用します。これは、1つ以上のフィールド値を変更してオブジェクトをコピーするときに、キーワード引数と一緒に使用するのが理想的です。

dave.copy(firstName = "Dave2")
// res6: Person = Person(Dave2,Gurnell)

dave.copy(lastName = "Gurnell2")
// res7: Person = Person(Dave,Gurnell2)

値と参照の等価性

Scala の == 演算子は Java のものとは異なります。それは、参照 ID を値として比較する代わりに equals に委譲します。

Scala は、Java の == と同じ振る舞いをする eq と呼ばれる演算子を持ちます。しかしながら、それをアプリケーションコードで使用することはめったにありません。

new Person("Noel", "Welsh") eq (new Person("Noel", "Welsh"))
// res8: Boolean = false

dave eq dave
// res9: Boolean = true
  1. ケースクラスは java.io.Serializablescala.Product という2つのトレイトを実装します。どちらも直接使用されることはありません。後者は、ケースクラスの名前とフィールド数を調べるためのメソッドを提供します。

3.4.2 ケースクラスコンパニオンオブジェクトの機能

コンパニオンオブジェクトは、クラスのコンストラクターと同じ引数を持つ apply メソッドを含みます。Scala プログラマーは、new を省略できる簡潔さのために、コンストラクターより apply メソッドを好む傾向があり、コンストラクターが式の中で読み易くなるようにします。

Person("Dave", "Gurnell") == Person("Noel", "Welsh")
// res10: Boolean = false

Person("Dave", "Gurnell") == Person("Dave", "Gurnell")
// res11: Boolean = true

最後に、コンパニオンオブジェクトは、パターンマッチング (pattern matching) に使用する抽出子パターン (extractor pattern) を実装するコードも含みます。本章で後ほど見ていきます。

ケースクラス宣言文法

ケースクラスを宣言するための文法は、

case class Name(parameter: type, ...) {
  declarationOrExpression ...
}

です。ここで、

  • Name はケースクラスの名前
  • parameter はコンストラクター引数として与えられた名前(オプション)
  • type はコンストラクター引数の型
  • declarationOrExpression は宣言か式(オプション)

とします。

3.4.3 ケースオブジェクト

最後の覚え書きです。コンストラクター引数なしでケースクラスを定義しているものをみつけたら、その代わりにケースオブジェクト (case object) を定義できます。ケースオブジェクトは、単に通常のシングルトンオブジェクトのように定義されますが、もっと意味のある toString メソッドを持ち、ProductSerializable トレイトを継承しています。

case object Citizen {
  def firstName = "John"
  def lastName  = "Doe"
  def name = firstName + " " + lastName
}
Citizen.toString
// res12: String = Citizen

3.4.4 覚えておいてほしいこと

ケースクラスは Scala データ型の真髄です。それを使って、学んで、好きになってください。

ケースクラスを宣言するための文法は、case を付与すること以外はクラスを宣言するためのものと同じです。

case class Name(parameter: type, ...) {
  declarationOrExpression ...
}

ケースクラスは、タイピングの手間を省く、たくさんの自動生成されたメソッドと機能を持ちます。関連するメソッドを実装することによって、その動作を個別にオーバーライドできます。

Scala 2.10 以前は、0〜22のフィールドを含むケースクラスしか定義できませんでした。Scala 2.11 で、任意サイズのケースクラスを定義する能力を獲得しました。

3.4.5 演習

3.4.5.1 ケースキャット

Cat が色とエサを String で持っていることを思い出してください。Cat を表現するケースクラスを定義しましょう。

もうひとつの簡単な指の運動です。

case class Cat(colour: String, food: String)

3.4.5.2 ロジャー・イーバートは言う……

良くない映画は長すぎるが、悪くない映画は十分に短い。

コードについては必ずしも同じことを言えませんが、この場合、DirectorFilm をケースクラスに変換することによって、多くの定型文を取り除くことができます。この変換を実行し、どのコードを削除できるか試してみましょう。

ケースクラスは copy メソッドと apply メソッドを提供し、各コンストラクター引数の前にある val を記述する必要性を取り除きます。最終的なコードベースは下記のようになります。

case class Director(firstName: String, lastName: String, yearOfBirth: Int) {
  def name: String =
    s"$firstName $lastName"
}

object Director {
  def older(director1: Director, director2: Director): Director =
    if (director1.yearOfBirth < director2.yearOfBirth) director1 else director2
}

case class Film(
  name: String,
  yearOfRelease: Int,
  imdbRating: Double,
  director: Director) {

  def directorsAge =
    yearOfRelease - director.yearOfBirth

  def isDirectedBy(director: Director) =
    this.director == director
}

object Film {
  def newer(film1: Film, film2: Film): Film =
    if (film1.yearOfRelease < film2.yearOfRelease) film1 else film2

  def highestRating(film1: Film, film2: Film): Double = {
    val rating1 = film1.imdbRating
    val rating2 = film2.imdbRating
    if (rating1 > rating2) rating1 else rating2
  }

  def oldestDirectorAtTheTime(film1: Film, film2: Film): Director =
    if (film1.directorsAge > film2.directorsAge) film1.director else film2.director
}

このコードはかなり短くなっただけでなく、equals メソッドや toString メソッド、後ほどの演習の準備にもなるパターンマッチング機能を提供します。

3.4.5.3 ケースクラスカウンター

ケースクラスとして Counter を再実装し、適切なところで copy を使用してください。さらに、0 をデフォルト値として count を初期化しましょう。

case class Counter(count: Int = 0) {
  def dec = copy(count = count - 1)
  def inc = copy(count = count + 1)
}

これは、ほとんど引っ掛け問題です。前回の実装と、ほんの少しの違いしかありません。しかしながら、無料で得られている追加の機能に注目してください。

Counter(0) // `new` なしでオブジェクトを構築
// res16: Counter = Counter(0)

Counter().inc // 印字で `count` の値を表示
// res17: Counter = Counter(1)

Counter().inc.dec == Counter().dec.inc // 意味のある等価性を検証
// res18: Boolean = true

3.4.5.4 適用、適用、適用

ケースクラスにコンパニオンオブジェクトを定義すると何が起こるでしょうか?見てみましょう。

前節の Person クラスを題材に、ケースクラスへの変換を試してみましょう。(ヒント:コードは上にあります。)代替 apply メソッドを伴うコンパニオンオブジェクトを依然として持っていることを確認してください。

こちらがコードです。

case class Person(firstName: String, lastName: String) {
  def name = firstName + " " + lastName
}

object Person {
  def apply(name: String): Person = {
    val parts = name.split(" ")
    apply(parts(0), parts(1))
  }
}

Person のためにコンパニオンオブジェクトを定義しているにも関わらず、依然として Scala のケースクラスのコードは期待どおりに動作します。自動生成されたメソッドを定義したコンパニオンオブジェクトに追加するので、ひとつのコンパイル単位にクラスとコンパニオンオブジェクトを配置する必要があります。

これは、最終的に apply メソッドのオーバーロードによって、2つの型シグネチャをコンパニオンオブジェクトが持つこと意味します。

def apply(name: String): Person =
  // etc...

def apply(firstName: String, lastName: String): Person =
  // etc...

3.5 パターンマッチング

これまでは、メソッドを呼び出したり、フィールドにアクセスしたりしてオブジェクトを作用させてきました。ケースクラスを使用すると、パターンマッチング (pattern matching) を通じた別の方法で作用させることができます。

パターンマッチングは、データの「形」に応じて式を評価できるように拡張した if 式のようなものです。前の例で見た Person ケースクラスを思い出してください。

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

反乱軍のメンバーを探している Stormtrooper を実装したいとしましょう。下記のようにパターンマッチングを使用することができます。

object Stormtrooper {
  def inspect(person: Person): String =
    person match {
      case Person("Luke", "Skywalker") => "Stop, rebel scum!"
      case Person("Han", "Solo") => "Stop, rebel scum!"
      case Person(first, last) => s"Move along, $first"
    }
}

パターンの文法 Person("Luke", "Skywalker") は、パターンマッチングするオブジェクトを構築するための文法 Person("Luke", "Skywalker") と一致していることに注意してください。

使用例はこちらです。

Stormtrooper.inspect(Person("Noel", "Welsh"))
// res0: String = Move along, Noel

Stormtrooper.inspect(Person("Han", "Solo"))
// res1: String = Stop, rebel scum!

パターンマッチング文法

パターンマッチング式の文法は、

expr0 match {
  case pattern1 => expr1
  case pattern2 => expr2
  ...
}

です。ここで、

  • expr0 はパターンマッチングする値に評価される式
  • pattern1pattern2 などは値に対して順番に検証されるパターンもしくはガード (guard)
  • expr1expr2 などは式で、最初にマッチングしたパターンの右辺式が評価される5

とします。パターンマッチング自体は式であるため、マッチングしたパターンの右辺式の値に評価されます。

3.5.1 パターン文法

Scala は、パターンやガードを記述するための表現力豊かな文法を持っています。ケースクラスの場合、パターン文法はコンストラクター文法と一致します。データを取り上げてみましょう。

Person("Noel", "Welsh")
// res2: Person = Person(Noel,Welsh)

Person 型に対してマッチするパターンは、下記のように記述されます。

Person(pat0, pat1)

ここで、pat0pat1 は、それぞれ firstNamelastName に対してマッチするパターンです。pat0pat1 の位置に使用できるパターンは4つあります。

  1. 名前。それは、その位置にある任意の値にマッチし、与えられた名前に束縛されます。例えば、パターン Person(first, last) は、名前 first に値 "Noel" を、名前 last に値 "Welsh" を束縛します。

  2. アンダースコア (_)。それは、任意の値にマッチし、その値を無視します。例えば、ストームトルーパーが一般市民の名についてのみ気にするのであれば、lastName の値を名前に束縛することを避け、単に Person(first, _) と書くことができます。

  3. リテラル。それは、単にリテラルが表現する値に首尾よくマッチします。例えば、パターン Person("Han", "Solo") は、名が "Han" で、姓が "Solo" である Person にマッチするというわけです。

  4. 同様のコンストラクタースタイル文法を使用している別のケースクラス。(訳注:ケースクラスを入れ子にできるということです。本節の演習に例があります。)

パターンマッチングでできることは他にもたくさんあり、パターンマッチングは拡張可能であることも覚えておいてください。後ほどの節でそれらの機能を見ていきます。

3.5.2 覚えておいてほしいこと

ケースクラスは、パターンマッチングと呼ばれる相互作用の新しい形を可能にします。パターンマッチングでは、ケースクラスによって分析し、ケースクラスに含まれる内容に応じて異なる式を評価することができます。

パターンマッチングのための文法は、

expr0 match {
  case pattern1 => expr1
  case pattern2 => expr2
  ...
}

です。パターンは、下記のいずれかになります。

  1. 名前。その名前に任意の値が束縛される
  2. アンダースコア。任意の値がマッチし、その値は無視される
  3. リテラル。リテラルが意味する値にマッチする
  4. ケースクラスによるコンストラクタースタイルパターン

3.5.3 演習

3.5.3.1 猫にエサをあげる

willServe メソッドを伴う ChipShop オブジェクトを定義してください。このメソッドは Cat を受け取り、猫の好きなエサがカリカリ (chips) であれば true を、そうでなければ false を返します。パターンマッチングを使用しましょう。

問題文が示唆しているスケルトンを記述することから始めましょう。

case class Cat(name: String, colour: String, food: String)

object ChipShop {
  def willServe(cat: Cat): Boolean =
    cat match {
      case Cat(???, ???, ???) => ???
    }
}

返却型は Boolean なので、少なくとも2つのケース、ひとつは true、もうひとつは false が必要であることがわかります。演習の文章は、それがカリカリが好きな猫とそれ以外の猫であることを示しています。これをリテラルパターンと _ パターンで実装することができます。

object ChipShop {
  def willServe(cat: Cat): Boolean =
    cat match {
      case Cat(_, _, "Chips") => true
      case Cat(_, _, _) => false
    }
}

3.5.3.2 私の芝生から出ていけ!

この演習では、映画評論家である私の父のシミュレーターを書こうと思います。それはとても単純で、クリント・イーストウッドが監督した映画はどれでも10.0、ジョン・マクティアナンが監督した映画はどれでも7.0、ほかの映画はどれでも3.0に評価されます。Film を受け取り、Double を返す rate メソッドを伴う Dad と呼ばれるオブジェクトを実装してください。パターンマッチングを使用しましょう。

object Dad {
  def rate(film: Film): Double =
    film match {
      case Film(_, _, _, Director("Clint", "Eastwood", _)) => 10.0
      case Film(_, _, _, Director("John", "McTiernan", _)) => 7.0
      case _ => 3.0
    }
}

この場合、パターンマッチングはいささか冗長になります。後ほど、定数パターン (constant pattern) と呼ばれる特定の値にマッチするパターンマッチングを使用する方法を学びます。

3.6 まとめ

本章ではクラスを探求しました。そのクラスが、オブジェクトを超えた抽象化を可能にすることも見てきました。それによって、共通のプロパティを共有し、共通の型を持つオブジェクトを定義できます。

コンパニオンオブジェクトについてもまた見てきました。それは、Scala において、補助コンストラクターや、クラスに属さないユーティリティメソッドを定義するために使用されます。

最後に、ケースクラスを紹介しました。それは、ボイラープレートコードを大いに削減し、メソッド呼び出しに加えて、パターンマッチングというオブジェクトとの新しい相互作用の方法を可能にしました。

4 トレイトによるデータモデリング

前章では、クラスについて詳細に見てきました。クラスは、同じ形のプロパティを持つオブジェクトを横断的に抽象化する方法を提供し、クラス内のどのようなオブジェクトでも動作するコードを書くことを可能にします。

本章では、クラスを横断的に抽象化することを探求します。それは、異なるクラスのオブジェクトで動作するコードを書くことを可能にします。これを、トレイト (trait) と呼ばれる機構で実現します。

また、本章は視点を変更する章になります。以前の章では、Scala コードを構築するための技術的側面に取り組んできました。本章の最初こそトレイトの技術的側面に焦点を当てますが、その焦点は思考を表現するための手段として Scala を使用することに変化していきます。

さらに、代数的データ型 (algebraic datatype) と呼ばれるデータの記述をどのように機械的にコードに変換できるかを見ていきます。また、構造的再帰 (structural recursion) を使用して、代数的データ型を変換するコードを機械的に書けることも合わせて見ていきます。

4.1 トレイト

オブジェクトを生成するためのテンプレートであるクラスと同じように、トレイトはクラスを生成するためのテンプレートです。トレイトは、2つ以上のクラスを同じものとみなし、同じ操作を実装するという表現を可能にします。言い換えれば、トレイトは、すべてのクラスが共有している Any 基底型の外側で、共通の基底型を共有する複数のクラスを表現できるということです。

トレイト対 Java インターフェイス

トレイトは、Java 8 のデフォルトメソッド (default method) を伴うインターフェイス (interface) にとてもよく似ています。Java 8 を使用したことがなければ、インターフェイスと抽象クラス (abstract class) を掛け合わせたようなものがトレイトであると考えてください。

4.1.1 トレイトの例

トレイトの例から始めましょう。Web サイトの訪問者をモデリングすることを想像してください。訪問者には2つのタイプがあり、ひとつはサイトに登録済みの訪問者で、もうひとつは匿名の訪問者です。2つのクラスでこれをモデル化します。

import java.util.Date

case class Anonymous(id: String, createdAt: Date = new Date())

case class User(
  id: String,
  email: String,
  createdAt: Date = new Date()
)

これらのクラス定義で、匿名訪問者と登録済み訪問者は、ID と作成日を持つと言えます。しかし、私たちは登録済み訪問者のメールアドレスしかわかりません。

ここには明らかな重複があり、同じ定義を2回書かない方が望ましいです。しかし、もっと重要なのは、2種類の訪問者のために何らかの共通の型を作るということです。AnyRefAny ではない、何らかの共通の型を持てれば、どんな種類の訪問者でも動作するメソッドを書くことができます。それは、下記のようにトレイトで実現できます。

import java.util.Date

trait Visitor {
  def id: String      // 各ユーザーに付与される一意の ID
  def createdAt: Date // ユーザーがサイトに初めて訪れた日付

  // この訪問者が初めて訪れてからどのくらいになるのか?
  def age: Long = new Date().getTime - createdAt.getTime
}

case class Anonymous(
  id: String,
  createdAt: Date = new Date()
) extends Visitor

case class User(
  id: String,
  email: String,
  createdAt: Date = new Date()
) extends Visitor

2つの変更に注目してください。

Visitor トレイトは、どんな派生型でも必ず実装しなければならないインターフェイスを表現するので、String 型の idDate 型の createdAt を実装しなければなりません。また、Visitor のどんな派生型でも、自動的に Visitor で定義されている age メソッドを持ちます。

Visitor トレイトを定義することによって、下記のように訪問者のどんな派生型でも動作するメソッドを書くことができます。

def older(v1: Visitor, v2: Visitor): Boolean =
  v1.createdAt.before(v2.createdAt)
older(Anonymous("1"), User("2", "test@example.com"))
// res5: Boolean = true

こちらの older メソッドは、Visitor の派生型である AnonymousUser のいずれでも呼び出すことができます。

トレイト文法

トレイトを宣言するには、

trait TraitName {
  declarationOrExpression ...
}

と書きます。トレイトの派生型であるクラスを宣言するには、

class Name(...) extends TraitName {
  ...
}

と書きます。より一般的には、ケースクラスを使用しますが、その文法は同じです。

case class Name(...) extends TraitName {
 ...
}

4.1.2 トレイトとクラスの比較

クラスのように、トレイトはフィールド定義とメソッド定義の名前付き集合です。しかしながら、いくつかの重要な点においてトレイトはクラスと異なっています。

抽象的な定義のより深く探索するために Visitor トレイトに立ち返ってみましょう。Visitor の定義を思い出してください。

import java.util.Date

trait Visitor {
  def id: String      // 各ユーザーに付与される一意の ID
  def createdAt: Date // ユーザーがサイトに初めて訪れた日付

  // この訪問者が初めて訪れてからどのくらいになるのか?
  def age: Long = new Date().getTime - createdAt.getTime
}

Visitor は2つの抽象メソッドを規定しています。そのメソッドは実装を持っていませんが、派生クラスでは必ず実装しなければなりません。それらは idcreatedAt です。また、age という具象メソッドも定義しており、それは抽象メソッドのひとつを使用して定義されています。

Visitor は、AnonymousUser の2つのクラスの構成要素として使用されています。Visitor を継承 (extends) した各クラスは、そのすべてのフィールドとメソッドを継承していることを意味します。

val anon = Anonymous("anon1")
// anon: Anonymous = Anonymous(anon1,Sat Jun 20 09:15:55 UTC 2020)

anon.createdAt
// res7: java.util.Date = Sat Jun 20 09:15:55 UTC 2020

anon.age
// res8: Long = 281

idcreatedAt は抽象なので、派生クラスにおいて必ず定義されなければなりません。私たちのクラスでは、def の代わりに val によってそれらを実装しています。Scala においてこれは認められており、defval のより一般的なバージョンと見なしています6。これはよい習慣で、トレイトにおいては val で絶対に定義せず、def を使用してください。これによって、具象実装では適切に defval を使用して実装できます。

4.1.3 覚えておいてほしいこと

トレイトは、クラスがオブジェクトを横断的に抽象化するための方法であるように、類似のプロパティを持つクラスを横断的に抽象化する方法です。

トレイトを使用するには、2つの段階があります。まずは、トレイトを宣言します。

trait TraitName {
  declarationOrExpression ...
}

そして、クラス(普通はケースクラス)でトレイトを継承します。

case class Name(...) extends TraitName {
  ...
}

4.1.4 演習

4.1.4.1 猫、もっと猫

Cat Simulator 1.0 への需要が爆発的に高まっています!バージョン2では、家庭の猫を越えて、トラ (Tiger) やライオン (Lion)、パンサー (Panther) をイエネコ (Cat) に加えてモデル化します。Feline トレイトを定義し、ネコ科 (Feline) の派生型として、すべての異なる種を定義します。面白くするために、

定義します。

これは、ほぼトレイト文法に慣れるための指の運動ですが、解答にはいくつかの面白い点があります。

trait Feline {
  def colour: String
  def sound: String
}

case class Lion(colour: String, maneSize: Int) extends Feline {
  val sound = "roar"
}

case class Tiger(colour: String) extends Feline {
  val sound = "roar"
}

case class Panther(colour: String) extends Feline {
  val sound = "roar"
}

case class Cat(colour: String, food: String) extends Feline {
  val sound = "meow"
}

sound がコンストラクター引数として定義されていないことに注意してください。それが定数である以上、それを変更する機会がユーザーに与えられることは妥当ではありません。sound の定義にはたくさんの重複があります。このように、Feline の中でデフォルト値を定義できます。

trait Feline {
  def colour: String
  def sound: String = "roar"
}

これは、一般的にはよくない習慣です。デフォルト実装を定義するのであれば、それはすべての派生型にふさわしい実装であるべきです。

"roar" という鳴き声を定義する、おそらく BigCat と呼ばれる中間型を定義するという別の選択肢があります。これは、よりよい解決策です。

trait BigCat extends Feline {
  override val sound = "roar"
}

case class Tiger(...) extends BigCat
case class Lion(...) extends BigCat
case class Panther(...) extends BigCat

4.1.4.2 トレイトでシェイプアップ

Shape と呼ばれるトレイトを定義し、3つの抽象メソッドをそれに持たせます。

図形 (Shape) を3つのクラス、円 (Circle) ・長方形 (Rectangle)・正方形 (Square) で実装します。それぞれの場合で、3つのメソッドについての実装をそれぞれ提供します。各図形のメインコンストラクターの引数(例えば、円の半径)は、フィールドとしてアクセス可能であることを確実にしてください。

ヒント: π の値は math.Pi として参照します。

trait Shape {
  def sides: Int
  def perimeter: Double
  def area: Double
}

case class Circle(radius: Double) extends Shape {
  val sides = 1
  val perimeter = 2 * math.Pi * radius
  val area = math.Pi * radius * radius
}

case class Rectangle(
  width: Double,
  height: Double
) extends Shape {
  val sides = 4
  val perimeter = 2 * width + 2 * height
  val area = width * height
}

case class Square(size: Double) extends Shape {
  val sides = 4
  val perimeter = 4 * size
  val area = size * size
}

4.1.4.3 トレイトでシェイプアップ 2

前回の演習の解答は、図形について別々の3つの型を与えました。しかしながら、それは3つの間にある関係性を正しくモデル化できていません。正方形 (Square) は、単に図形 (Shape) ではなく、幅と高さが同じ長方形の型でもあります。

前回の演習の解答を、正方形 (Square) と長方形 (Rectangle) が共通の型、方形 (Rectangular) の派生型であるようにリファクタリングしてみましょう。

ヒント:トレイトは別のトレイトを継承できます。

新しいコードはこのようになります。

// trait Shape ...

// case class Circle ...

sealed trait Rectangular extends Shape {
  def width: Double
  def height: Double
  val sides = 4
  override val perimeter = 2*width + 2*height
  override val area = width*height
}

case class Square(size: Double) extends Rectangular {
  val width = size
  val height = size
}

case class Rectangle(
  val width: Double,
  val height: Double
) extends Rectangular

確実にトレイトを sealed であるようにすることで、Rectangular 型や Shape 型のオブジェクトを取り扱うどんなコードを書いたときでも、コンパイラーが徹底的に検証してくれます。

4.2 This or That and Nothing Else: Sealed Traits

In many cases we can enumerate all the possible classes that can extend a trait. For example, we previously modelled a website visitor as Anonymous or a logged in User. These two cases cover all the possibilities as one is the negation of the other. We can model this case with a sealed trait, which allows the compiler to provide extra checks for us.

We create a sealed trait by simply writing sealed in front of our trait declaration:

import java.util.Date

sealed trait Visitor {
  def id: String
  def createdAt: Date
  def age: Long = new Date().getTime() - createdAt.getTime()
}

When we mark a trait as sealed we must define all of its subtypes in the same file. Once the trait is sealed, the compiler knows the complete set of subtypes and will warn us if a pattern matching expression is missing a case:

def missingCase(v: Visitor) =
  v match {
    case User(_, _, _) => "Got a user"
  }
// <console>:17: warning: match may not be exhaustive.
// It would fail on the following input: Anonymous(_, _)
//          v match {
//          ^
// error: No warnings can be incurred under -Xfatal-warnings.

We will not get a similar warning from an unsealed trait.

We can still extend the subtypes of a sealed trait outside of the file where they are defined. For example, we could extend User or Anonymous further elsewhere. If we want to prevent this possibility we should declare them as sealed (if we want to allow extensions within the file) or final if we want to disallow all extensions. For the visitors example it probably doesn’t make sense to allow any extension to User or Anonymous, so the simplified code should look like this:

sealed trait Visitor { /* ... */ }
final case class User(/* ... */) extends Visitor
final case class Anonymous(/* ... */) extends Visitor

This is a very powerful pattern and one we will use frequently.

Sealed Trait Pattern

If all the subtypes of a trait are known, seal the trait

sealed trait TraitName {
  ...
}

Consider making subtypes final if there is no case for extending them

final case class Name(...) extends TraitName {
  ...
}

Remember subtypes must be defined in the same file as a sealed trait.

4.2.1 Take home points

Sealed traits and final (case) classes allow us to control extensibility of types. The majority of cases should use the sealed trait / final case class pattern.

sealed trait TraitName { ... }
final case class Name(...) extends TraitName

The main advantages of this pattern are:

4.2.2 Exercises

4.2.2.1 Printing Shapes

Let’s revisit the Shapes example from Section [@sec:traits:shaping-up-2].

First make Shape a sealed trait. Then write a singleton object called Draw with an apply method that takes a Shape as an argument and returns a description of it on the console. For example:

Draw(Circle(10))
// res1: String = A circle of radius 10.0cm

Draw(Rectangle(3, 4))
// res2: String = A rectangle of width 3.0cm and height 4.0cm

Finally, verify that the compiler complains when you comment out a case clause.

object Draw {
  def apply(shape: Shape): String = shape match {
    case Rectangle(width, height) =>
      s"A rectangle of width ${width}cm and height ${height}cm"

    case Square(size) =>
      s"A square of size ${size}cm"

    case Circle(radius) =>
      s"A circle of radius ${radius}cm"
  }
}

4.2.2.2 The Color and the Shape

Write a sealed trait Color to make our shapes more interesting.

A lot of this exercise is left deliberately open to interpretation. The important thing is to practice working with traits, classes, and objects.

Decisions such as how to model colours and what is considered a light or dark colour can either be left up to you or discussed with other class members.

Edit the code for Shape and its subtypes to add a colour to each shape.

Finally, update the code for Draw.apply to print the colour of the argument as well as its shape and dimensions:

Draw(Circle(10, Yellow))
// res8: String = A yellow circle of radius 10.0cm

You may want to deal with the colour in a helper method.

One solution to this exercise is presented below. Remember that a lot of the implementation details are unimportant—the crucial aspects of a correct solution are:

  • There must be a sealed trait Color:
    • The trait should contain three def methods for the RGB values.
    • The trait should contains the isLight method, defined in terms of the RGB values.
  • There must be three objects representing the predefined colours:
    • Each object must extend Color.
    • Each object should override the RGB values as vals.
    • Marking the objects as final is optional.
    • Making the objects case objects is also optional.
  • There must be a class representing custom colours:
    • The class must extend Color.
    • Marking the class final is optional.
    • Making the class a case class is optional (although highly recommended).
  • There should ideally be two methods in Draw:
    • One method should accept a Color as a parameter and one a Shape.
    • The method names are unimportant.
    • Each method should perform a match on the supplied value and provide enough cases to cover all possible subtypes.
  • The whole codebase should compile and produce sensible values when tested!
// Shape uses Color so we define Color first:
sealed trait Color {
  // We decided to store RGB values as doubles between 0.0 and 1.0.
  //
  // It is always good practice to define abstract members as `defs`
  // so we can implement them with `defs`, `vals` or `vars`.
  def red: Double
  def green: Double
  def blue: Double

  // We decided to define a "light" colour as one with
  // an average RGB of more than 0.5:
  def isLight = (red + green + blue) / 3.0 > 0.5
  def isDark = !isLight
}

case object Red extends Color {
  // Here we have implemented the RGB values as `vals`
  // because the values cannot change:
  val red = 1.0
  val green = 0.0
  val blue = 0.0
 }

case object Yellow extends Color {
  // Here we have implemented the RGB values as `vals`
  // because the values cannot change:
  val red = 1.0
  val green = 1.0
  val blue = 0.0
}

case object Pink extends Color {
  // Here we have implemented the RGB values as `vals`
  // because the values cannot change:
  val red = 1.0
  val green = 0.0
  val blue = 1.0
}

// The arguments to the case class here generate `val` declarations
// that implement the RGB methods from `Color`:
final case class CustomColor(
  red: Double,
  green: Double,
  blue: Double) extends Color

// The code from the previous exercise comes across almost verbatim,
// except that we add a `color` field to `Shape` and its subtypes:
sealed trait Shape {
  def sides: Int
  def perimeter: Double
  def area: Double
  def color: Color
}

final case class Circle(radius: Double, color: Color) extends Shape {
  val sides = 1
  val perimeter = 2 * math.Pi * radius
  val area = math.Pi * radius * radius
}

sealed trait Rectangular extends Shape {
  def width: Double
  def height: Double
  val sides = 4
  val perimeter = 2 * width + 2 * height
  val area = width * height
}

final case class Square(size: Double, color: Color) extends Rectangular {
  val width = size
  val height = size
}

final case class Rectangle(
  width: Double,
  height: Double,
  color: Color
) extends Rectangular

// We decided to overload the `Draw.apply` method for `Shape` and
// `Color` on the basis that we may want to reuse the `Color` code
// directly elsewhere:
object Draw {
  def apply(shape: Shape): String = shape match {
    case Circle(radius, color) =>
      s"A ${Draw(color)} circle of radius ${radius}cm"

    case Square(size, color) =>
      s"A ${Draw(color)} square of size ${size}cm"

    case Rectangle(width, height, color) =>
      s"A ${Draw(color)} rectangle of width ${width}cm and height ${height}cm"
  }

  def apply(color: Color): String = color match {
    // We deal with each of the predefined Colors with special cases:
    case Red    => "red"
    case Yellow => "yellow"
    case Pink   => "pink"
    case color  => if(color.isLight) "light" else "dark"
  }
}

// Test code:

Draw(Circle(10, Pink))
// res29: String = A pink circle of radius 10.0cm

Draw(Rectangle(3, 4, CustomColor(0.4, 0.4, 0.6)))
// res30: String = A dark rectangle of width 3.0cm and height 4.0cm

4.2.2.3 A Short Division Exercise

Good Scala developers don’t just use types to model data. Types are a great way to put artificial limitations in place to ensure we don’t make mistakes in our programs. In this exercise we will see a simple (if contrived) example of this—using types to prevent division by zero errors.

Dividing by zero is a tricky problem—it can lead to exceptions. The JVM has us covered as far as floating point division is concerned but integer division is still a problem:

1.0 / 0.0
// res31: Double = Infinity
1 / 0
// java.lang.ArithmeticException: / by zero
//   ... 1024 elided

Let’s solve this problem once and for all using types!

Create an object called divide with an apply method that accepts two Ints and returns DivisionResult. DivisionResult should be a sealed trait with two subtypes: a Finite type encapsulating the result of a valid division, and an Infinite type representing the result of dividing by 0.

Here’s some example usage:

val x = divide(1, 2)
// x: DivisionResult = Finite(0)

val y = divide(1, 0)
// y: DivisionResult = Infinite

Finally, write some sample code that calls divide, matches on the result, and returns a sensible description.

Here’s the code:

sealed trait DivisionResult
final case class Finite(value: Int) extends DivisionResult
case object Infinite extends DivisionResult

object divide {
  def apply(num: Int, den: Int): DivisionResult =
    if(den == 0) Infinite else Finite(num / den)
}

divide(1, 0) match {
  case Finite(value) => s"It's finite: ${value}"
  case Infinite      => s"It's infinite"
}
// res34: String = It's infinite

The result of divide.apply is a DivisionResult, which is a sealed trait with two subtypes. The subtype Finite is a case class encapsulting the result, but the subtype Infinite can simply be an object. We’ve used a case object for parity with Finite.

The implementation of divide.apply is simple - we perform a test and return a result. Note that we haven’t annotated the method with a result type—Scala is capable of inferring the type DivisionResult as the least upper bound of Infinite and Finite.

Finally, the match illustrates a case class pattern with the parentheses, and a case object pattern without.

4.3 Modelling Data with Traits

In this section we’re going to shift our focus from language features to programming patterns. We’re going to look at modelling data and learn a process for expressing in Scala any data model defined in terms of logical ors and ands. Using the terminology of object-oriented programming, we will express is-a and has-a relationships. In the terminology of functional programming we are learning about sum and product types, which are together called algebraic data types.

Our goal in this section is to see how to translate a data model into Scala code. In the next section we’ll see patterns for code that uses algebraic data types.

4.3.1 The Product Type Pattern

Our first pattern is to model data that contains other data. We might describe this as “A has a B and C”. For example, a Cat has a colour and a favourite food; a Visitor has an id and a creation date; and so on.

The way we write this is to use a case class. We’ve already done this many times in exercises; now we’re formalising the pattern.

Product Type Pattern

If A has a b (with type B) and a c (with type C) write

case class A(b: B, c: C)

or

trait A {
  def b: B
  def c: C
}

4.4 The Sum Type Pattern

Our next pattern is to model data that is two or more distinct cases. We might describe this as “A is a B or C”. For example, a Feline is a Cat, Lion, or Tiger; a Visitor is an Anonymous or User; and so on.

We write this using the sealed trait / final case class pattern.

Sum Type Pattern

If A is a B or C write

sealed trait A
final case class B() extends A
final case class C() extends A

4.4.1 Algebraic Data Types

An algebraic data type is any data that uses the above two patterns. In the functional programming literature, data using the “has-a and” pattern is known as a product type, and the “is-a or” pattern is a sum type.

4.4.2 The Missing Patterns

We have looked at relationships along two dimensions: is-a/has-a, and and/or. We can draw up a little table and see we only have patterns for two of the four table cells.

And Or

Is-a

Sum type

Has-a

Product type

What about the missing two patterns?

The “is-a and” pattern means that A is a B and C. This pattern is in some ways the inverse of the sum type pattern, and we can implement it as

trait B
trait C
trait A extends B with C

In Scala a trait can extend as many traits as we like using the with keyword like A extends B with C with D and so on. We aren’t going to use this pattern in this course. If we want to represent that some data conforms to a number of different interfaces we will often be better off using a type class, which we will explore later. There are, however, several legitimate uses of this pattern:

The “has-a or” patterns means that A has a B or C. There are two ways we can implement this. We can say that A has a d of type D, where D is a B or C. We can mechanically apply our two patterns to implement this:

trait A {
  def d: D
}
sealed trait D
final case class B() extends D
final case class C() extends D

Alternatively we could implement this as A is a D or E, and D has a B and E has a C. Again this translates directly into code

sealed trait A
final case class D(b: B) extends A
final case class E(c: C) extends A

4.4.3 Take Home Points

We have seen that we can mechanically translate data using the “has-a and” and “is-a or” patterns (or, more succinctly, the product and sum types) into Scala code. This type of data is known as an algebraic data type. Understanding these patterns is very important for writing idiomatic Scala code.

4.4.4 Exercises

4.4.4.1 Stop on a Dime

A traffic light is red, green, or yellow. Translate this description into Scala code.

This is a direct application of the sum type pattern.

sealed trait TrafficLight
case object Red extends TrafficLight
case object Green extends TrafficLight
case object Yellow extends TrafficLight

As there are fields or methods on the three cases, and thus there is no need to create than one instance of them, I used case objects instead of case classes.

4.4.4.2 Calculator

A calculation may succeed (with an Int result) or fail (with a String message). Implement this.

sealed trait Calculation
final case class Success(result: Int) extends Calculation
final case class Failure(reason: String) extends Calculation

4.4.4.3 Water, Water, Everywhere

Bottled water has a size (an Int), a source (which is a well, spring, or tap), and a Boolean carbonated. Implement this in Scala.

Crank the handle on the product and sum type patterns.

sealed trait Source
case object Well extends Source
case object Spring extends Source
case object Tap extends Source
final case class BottledWater(size: Int, source: Source, carbonated: Boolean)

4.5 Working With Data

In the previous section we saw how to define algebraic data types using a combination of the sum (or) and product type (and) patterns. In this section we’ll see a pattern for using algebraic data types, known as structural recursion. We’ll actually see two variants of this pattern: one using polymorphism and one using pattern matching.

Structural recursion is the precise opposite of the process of building an algebraic data type. If A has a B and C (the product-type pattern), to construct an A we must have a B and a C. The sum and product type patterns tell us how to combine data to make bigger data. Structural recursion says that if we have an A as defined before, we must break it into its constituent B and C that we then combine in some way to get closer to our desired answer. Structural recursion is essentially the process of breaking down data into smaller pieces.

Just as we have two patterns for building algebraic data types, we will have two patterns for decomposing them using structural recursion. We will actually have two variants of each pattern, one using polymorphism, which is the typical object-oriented style, and one using pattern matching, which is typical functional style. We’ll end this section with some rules for choosing which pattern to use.

4.5.1 Structural Recursion using Polymorphism

Polymorphic dispatch, or just polymorphism for short, is a fundamental object-oriented technique. If we define a method in a trait, and have different implementations in classes extending that trait, when we call that method the implementation on the actual concrete instance will be used. Here’s a very simple example. We start with a simple definition using the familiar sum type (or) pattern.

sealed trait A {
  def foo: String
}
final case class B() extends A {
  def foo: String =
    "It's B!"
}
final case class C() extends A {
  def foo: String =
    "It's C!"
}

We declare a value with type A but we see the concrete implementation on B or C is used.

val anA: A = B()
// anA: A = B()

anA.foo
// res0: String = It's B!

val anA: A = C()
// anA: A = C()

anA.foo
// res1: String = It's C!

We can define an implementation in a trait, and change the implementation in an extending class using the override keyword.

sealed trait A {
  def foo: String =
    "It's A!"
}
final case class B() extends A {
  override def foo: String =
    "It's B!"
}
final case class C() extends A {
  override def foo: String =
    "It's C!"
}

The behaviour is as before; the implementation on the concrete class is selected.

val anA: A = B()
// anA: A = B()

anA.foo
// res2: String = It's B!

Remember that if you provide a default implementation in a trait, you should ensure that implementation is valid for all subtypes.

Now we understand how polymorphism works, how do we use it with an algebraic data types? We’ve actually seen everything we need, but let’s make it explicit and see the patterns.

The Product Type Polymorphism Pattern

If A has a b (with type B) and a c (with type C), and we want to write a method f returning an F, simply write the method in the usual way.

case class A(b: B, c: C) {
  def f: F = ???
}

In the body of the method we must use b, c, and any method parameters to construct the result of type F.

The Sum Type Polymorphism Pattern

If A is a B or C, and we want to write a method f returning an F, define f as an abstract method on A and provide concrete implementations in B and C.

sealed trait A {
  def f: F
}
final case class B() extends A {
  def f: F =
    ???
}
final case class C() extends A {
  def f: F =
    ???
}

4.5.2 Structural Recursion using Pattern Matching

Structural recursion with pattern matching proceeds along the same lines as polymorphism. We simply have a case for every subtype, and each pattern matching case must extract the fields we’re interested in.

The Product Type Pattern Matching Pattern

If A has a b (with type B) and a c (with type C), and we want to write a method f that accepts an A and returns an F, write

def f(a: A): F =
  a match {
    case A(b, c) => ???
  }

In the body of the method we use b and c to construct the result of type F.

The Sum Type Pattern Matching Pattern

If A is a B or C, and we want to write a method f accepting an A and returning an F, define a pattern matching case for B and C.

def f(a: A): F =
  a match {
    case B() => ???
    case C() => ???
  }

4.5.3 A Complete Example

Let’s look at a complete example of the algebraic data type and structural recursion patterns, using our familiar Feline data type.

We start with a description of the data. A Feline is a Lion, Tiger, Panther, or Cat. We’re going to simplify the data description, and just say that a Cat has a String favouriteFood. From this description we can immediately apply our pattern to define the data.

sealed trait Feline
final case class Lion() extends Feline
final case class Tiger() extends Feline
final case class Panther() extends Feline
final case class Cat(favouriteFood: String) extends Feline

Now let’s implement a method using both polymorphism and pattern matching. Our method, dinner, will return the appropriate food for the feline in question. For a Cat their dinner is their favouriteFood. For Lions it is antelope, for Tigers it is tiger food, and for Panthers it is licorice.

We could represent food as a String, but we can do better and represent it with a type. This avoids, for example, spelling mistakes in our code. So let’s define our Food type using the now familiar patterns.

sealed trait Food
case object Antelope extends Food
case object TigerFood extends Food
case object Licorice extends Food
final case class CatFood(food: String) extends Food

Now we can implement dinner as a method returning Food. First using polymorphism:

sealed trait Feline {
  def dinner: Food
}
final case class Lion() extends Feline {
  def dinner: Food =
    Antelope
}
final case class Tiger() extends Feline {
  def dinner: Food =
    TigerFood
}
final case class Panther() extends Feline {
  def dinner: Food =
    Licorice
}
final case class Cat(favouriteFood: String) extends Feline {
  def dinner: Food =
    CatFood(favouriteFood)
}

Now using pattern matching. We actually have two choices when using pattern matching. We can implement our code in a single method on Feline or we can implement it in a method on another object. Let’s see both.

sealed trait Feline {
  def dinner: Food =
    this match {
      case Lion() => Antelope
      case Tiger() => TigerFood
      case Panther() => Licorice
      case Cat(favouriteFood) => CatFood(favouriteFood)
    }
}

object Diner {
  def dinner(feline: Feline): Food =
    feline match {
      case Lion() => Antelope
      case Tiger() => TigerFood
      case Panther() => Licorice
      case Cat(food) => CatFood(food)
    }
}

Note how we can directly apply the patterns, and the code falls out. This is the main point we want to make with structural recursion: the code follows the shape of the data, and can be produced in an almost mechanical way.

4.5.4 Choosing Which Pattern to Use

We have three way of implementing structural recursion:

  1. polymorphism;
  2. pattern matching in the base trait; and
  3. pattern matching in an external object (as in the Diner example above).

Which should we use? The first two methods give the same result: a method defined on the classes of interest. We should use whichever is more convenient. This normally ends up being pattern matching on the base trait as it requires less code duplication.

When we implement a method in the classes of interest we can have only one implementation of the method, and everything that method requires to work must be contained within the class and parameters we pass to the method. When we implement methods using pattern matching in an external object we can provide multiple implementations, one per object (multiple Diners in the example above).

The general rule is: if a method only depends on other fields and methods in a class it is a good candidate to be implemented inside the class. If the method depends on other data (for example, if we needed a Cook to make dinner) consider implementing it using pattern matching outside of the classes in question. If we want to have more than one implementation we should use pattern matching and implement it outside the classes.

4.5.5 Object-Oriented vs Functional Extensibility

In classic functional programming style we have no objects, only data without methods and functions. This style of programming makes extensive use of pattern matching. We can mimic it in Scala using the algebraic data type pattern and pattern matching in methods defined on external objects.

Classic object oriented style uses polymorphism and allow open extension of classes. In Scala terms this means no sealed traits.

What are the tradeoffs we make in the two different styles?

One advantage of functional style is it allows the compiler to help us more. By sealing traits we are telling the compiler it knows all the possible subtypes of that trait. It can then tell us if we miss out a case in our pattern matching. This is especially useful if we add or remove subtypes later in development. We could argue we get the same benefit from object-oriented style, as we must implement all methods defined on the base trait in any subtypes. This is true, but in practice classes with a large number of methods are very difficult to maintain and we’ll inevitably end up factoring some of the code into different classes – essentially duplicating the functional style.

This doesn’t mean functional style is to be preferred in all cases. There is a fundamental difference between the kind of extensibility that object-oriented style and functional style gives us. With OO style we can easily add new data, by extending a trait, but adding a new method requires us to change existing code. With functional style we can easily add a new method but adding new data requires us to modify existing code. In tabular form:

Add new method Add new data

OO

Change existing code

Existing code unchanged

FP

Existing code unchanged

Change existing code

In Scala we have the flexibility to use both polymorphism and pattern matching, and we should use whichever is appropriate. However we generally prefer sealed traits as it gives us greater guarantees about our code’s semantics, and we can use typeclasses, which we’ll explore later, to get us OO-style extensibility.

4.5.6 Exercises

4.5.6.1 Traffic Lights

In the previous section we implemented a TrafficLight data type like so:

sealed trait TrafficLight
case object Red extends TrafficLight
case object Green extends TrafficLight
case object Yellow extends TrafficLight

Using polymorphism and then using pattern matching implement a method called next which returns the next TrafficLight in the standard Red -> Green -> Yellow -> Red cycle. Do you think it is better to implement this method inside or outside the class? If inside, would you use pattern matching or polymorphism? Why?

First with polymorphism:

sealed trait TrafficLight {
  def next: TrafficLight
}
case object Red extends TrafficLight {
  def next: TrafficLight =
    Green
}
case object Green extends TrafficLight {
  def next: TrafficLight =
    Yellow
}
case object Yellow extends TrafficLight {
  def next: TrafficLight =
    Red
}

Now with pattern matching:

sealed trait TrafficLight {
  def next: TrafficLight =
    this match {
      case Red => Green
      case Green => Yellow
      case Yellow => Red
    }
}
case object Red extends TrafficLight
case object Green extends TrafficLight
case object Yellow extends TrafficLight

In this case I think implementing inside the class using pattern matching is best. Next doesn’t depend on any external data and we probably only want one implementation of it. Pattern matching makes the structure of the state machine clearer than polymorphism.

Ultimately there are no hard-and-fast rules, and we must consider our design decisions in the context of the larger program we are writing.

4.5.6.2 Calculation

In the last section we created a Calculation data type like so:

sealed trait Calculation
final case class Success(result: Int) extends Calculation
final case class Failure(reason: String) extends Calculation

We’re now going to write some methods that use a Calculation to perform a larger calculation. These methods will have a somewhat unusual shape—this is a precursor to things we’ll be exploring soon—but if you follow the patterns you will be fine.

Create a Calculator object. On Calculator define methods + and - that accept a Calculation and an Int, and return a new Calculation. Here are some examples

assert(Calculator.+(Success(1), 1) == Success(2))
assert(Calculator.-(Success(1), 1) == Success(0))
assert(Calculator.+(Failure("Badness"), 1) == Failure("Badness"))

Start by implementing the framework the exercise calls for:

object Calculator {
  def +(calc: Calculation, operand: Int): Calculation = ???
  def -(calc: Calculation, operand: Int): Calculation = ???
}

Now apply the structural recursion pattern:

object Calculator {
  def +(calc: Calculation, operand: Int): Calculation =
    calc match {
        case Success(result) => ???
        case Failure(reason) => ???
    }
  def -(calc: Calculation, operand: Int): Calculation =
    calc match {
      case Success(result) => ???
      case Failure(reason) => ???
    }
}

To write the remaining bodies of the methods we can no longer rely on the patterns. However, a bit of thought quickly leads us to the correct answer. We know that + and - are binary operations; we need two integers to use them. We also know we need to return a Calculation. Looking at the Failure cases, we don’t have two Ints available. The only result that makes sense to return is Failure. On the Success side, we do have two Ints and thus we should return Success. This gives us:

object Calculator {
  def +(calc: Calculation, operand: Int): Calculation =
    calc match {
        case Success(result) => Success(result + operand)
        case Failure(reason) => Failure(reason)
    }
  def -(calc: Calculation, operand: Int): Calculation =
    calc match {
      case Success(result) => Success(result - operand)
      case Failure(reason) => Failure(reason)
    }
}

Now write a division method that fails if the divisor is 0. The following tests should pass. Note the behavior for the last test. This indicates “fail fast” behavior. If a calculation has already failed we keep that failure and don’t process any more data even if, as is the case in the test, doing so would lead to another failure.

assert(Calculator./(Success(4), 2) == Success(2))
assert(Calculator./(Success(4), 0) == Failure("Division by zero"))
assert(Calculator./(Failure("Badness"), 0) == Failure("Badness"))

The important points here are:

  1. We have the same general pattern as before, matching on the Calculation first to implement our fail fast behavior.
  2. After matching on our Calculation we then check for division by zero.
def /(calc: Calculation, operand: Int): Calculation =
  calc match {
    case Success(result) =>
      operand match {
        case 0 => Failure("Division by zero")
        case _ => Success(result / operand)
      }
    case Failure(reason) => Failure(reason)
  }

4.5.6.3 Email

Recall the Visitor trait we looked at earlier: a website Visitor is either Anonymous or a signed-in User. Now imagine we wanted to add the ability to send emails to visitors. We can only email signed-in users, and sending an email requires a lot of knowledge about SMTP settings, MIME headers, and so on. Would an email method be better implemented using polymorphism on the Visitor trait or using pattern matching in an EmailService object? Why?

I would implement the method in an EmailService object. There are a lot of details to do with sending an email that have nothing to do with our Visitor class. I would rather keep these details in a separate abstraction.

4.6 Recursive Data

A particular use of algebraic data types that comes up very often is defining recursive data. This is data that is defined in terms of itself, and allows us to create data of potentially unbounded size (though any concrete instance will be finite).

We can’t define recursive data like7

final case class Broken(broken: Broken)

as we could never actually create an instance of such a type—the recursion never ends. To define valid recursive data we must define a base case, which is the case that ends the recursion.

Here is a more useful recursive definition: an IntList is either the empty list End, or a Pair8 containing an Int and an IntList. We can directly translate this to code using our familiar patterns:

sealed trait IntList
case object End extends IntList
final case class Pair(head: Int, tail: IntList) extends IntList

Here End is the base case. We construct the list containing 1, 2, and 3 as follows:

Pair(1, Pair(2, Pair(3, End)))

This data structure is known as a singly-linked list. In this example we have four links in our chain. We can write this out in a longer form to better understand the structure of the list. Below, d represents an empty list, and a, b, and c are pairs built on top of it.

val d = End()
val c = Pair(3, d)
val b = Pair(2, c)
val a = Pair(1, b)

In addition to being links in a chain, these data structures all represent complete sequences of integers:

Using this implementation, we can build lists of arbitrary length by repeatedly taking an existing list and prepending a new element9.

We can apply the same structural recursion patterns to process a recursive algebraic data type. The only wrinkle is that we must make a recursive call when the data definition is recursion.

Let’s add together all the elements of an IntList. We’ll use pattern matching, but as we know the same process applies to using polymorphism.

Start with the tests and method declaration.

val example = Pair(1, Pair(2, Pair(3, End)))
assert(sum(example) == 6)
assert(sum(example.tail) == 5)
assert(sum(End) == 0)

def sum(list: IntList): Int = ???

Note how the tests define 0 to be the sum of the elements of an End list. It is important that we define an appropriate base case for our method as we will build our final result of this base case.

Now we apply our structural recursion pattern to fill out the body of the method.

def sum(list: IntList): Int =
  list match {
    case End => ???
    case Pair(hd, tl) => ???
  }

Finally we have to decide on the bodies of our cases. We have already decided that 0 is answer for End. For Pair we have two bits of information to guide us. We know we need to return an Int and we know that we need to make a recursive call on tl. Let’s fill in what we have.

def sum(list: IntList): Int =
  list match {
    case End => 0
    case Pair(hd, tl) => ??? sum(tl)
  }

The recursive call will return the sum of the tail of the list, by definition. Thus the correct thing to do is to add hd to this result. This gives us our final result:

def sum(list: IntList): Int =
  list match {
    case End => 0
    case Pair(hd, tl) => hd + sum(tl)
  }

4.6.1 Understanding the Base Case and Recursive Case

Our patterns will carry us most of the way to a correct answer, but we still need to supply the method bodies for the base and recursive cases. There is some general guidance we can use:

Recursive Algebraic Data Types Pattern

When defining recursive algebraic data types, there must be at least two cases: one that is recursive, and one that is not. Cases that are not recursive are known as base cases. In code, the general skeleton is:

sealed trait RecursiveExample
final case class RecursiveCase(recursion: RecursiveExample) extends RecursiveExample
case object BaseCase extends RecursiveExample

Recursive Structural Recursion Pattern

When writing structurally recursive code on a recursive algebraic data type:

  • whenever we encounter a recursive element in the data we make a recursive call to our method; and
  • whenever we encounter a base case in the data we return the identity for the operation we are performing.

4.6.2 Tail Recursion

You may be concerned that recursive calls will consume excessive stack space. Scala can apply an optimisation, called tail recursion, to many recursive functions to stop them consuming stack space.

A tail call is a method call where the caller immediately returns the value. So this is a tail call

def method1: Int =
  1

def tailCall: Int =
  method1

because tailCall immediately returns the result of calling method1 while

def notATailCall: Int =
  method1 + 2

because notATailCall does not immediatley return—it adds an number to the result of the call.

A tail call can be optimised to not use stack space. Due to limitations in the JVM, Scala only optimises tail calls where the caller calls itself. Since tail recursion is an important property to maintain, we can use the @tailrec annotation to ask the compiler to check that methods we believe are tail recursion really are. Here we have two versions of sum annotated. One is tail recursive and one is not. You can see the compiler complains about the method that is not tail recursive.

import scala.annotation.tailrec
@tailrec
def sum(list: IntList): Int =
  list match {
    case End => 0
    case Pair(hd, tl) => hd + sum(tl)
  }
// <console>:20: error: could not optimize @tailrec annotated method sum: it contains a recursive call not in tail position
//          list match {
//          ^
@tailrec
def sum(list: IntList, total: Int = 0): Int =
  list match {
    case End => total
    case Pair(hd, tl) => sum(tl, total + hd)
  }
// sum: (list: IntList, total: Int)Int

Any non-tail recursion function can be transformed into a tail recursive version by adding an accumulator as we have done with sum above. This transforms stack allocation into heap allocation, which sometimes is a win, and other times is not.

In Scala we tend not to work directly with tail recursive functions as there is a rich collections library that covers the most common cases where tail recursion is used. Should you need to go beyond this, because you’re implementing your own datatypes or are optimising code, it is useful to know about tail recursion.

4.6.3 Exercises

4.6.3.1 A List of Methods

Using our definition of IntList

sealed trait IntList
case object End extends IntList
final case class Pair(head: Int, tail: IntList) extends IntList

define a method length that returns the length of the list. There is test data below you can use to check your solution. For this exercise it is best to use pattern matching in the base trait.

val example = Pair(1, Pair(2, Pair(3, End)))

assert(example.length == 3)
assert(example.tail.length == 2)
assert(End.length == 0)
sealed trait IntList {
  def length: Int =
    this match {
      case End => 0
      case Pair(hd, tl) => 1 + tl.length
    }
}
case object End extends IntList
final case class Pair(head: Int, tail: IntList) extends IntList

Define a method to compute the product of the elements in an IntList. Test cases are below.

assert(example.product == 6)
assert(example.tail.product == 6)
assert(End.product == 1)
sealed trait IntList {
  def product: Int =
    this match {
      case End => 1
      case Pair(hd, tl) => hd * tl.product
    }
}
case object End extends IntList
final case class Pair(head: Int, tail: IntList) extends IntList

Define a method to double the value of each element in an IntList, returning a new IntList. The following test cases should hold:

assert(example.double == Pair(2, Pair(4, Pair(6, End))))
assert(example.tail.double == Pair(4, Pair(6, End)))
assert(End.double == End)
sealed trait IntList {
  def double: IntList =
    this match {
      case End => End
      case Pair(hd, tl) => Pair(hd * 2, tl.double)
    }
}
case object End extends IntList
final case class Pair(head: Int, tail: IntList) extends IntList

4.6.3.2 The Forest of Trees

A binary tree of integers can be defined as follows:

A Tree is a Node with a left and right Tree or a Leaf with an element of type Int.

Implement this algebraic data type.

sealed trait Tree
final case class Node(l: Tree, r: Tree) extends Tree
final case class Leaf(elt: Int) extends Tree

Implement sum and double on Tree using polymorphism and pattern matching.

object TreeOps {
  def sum(tree: Tree): Int =
    tree match {
      case Leaf(elt) => elt
      case Node(l, r) => sum(l) + sum(r)
    }

  def double(tree: Tree): Tree =
    tree match {
      case Leaf(elt) => Leaf(elt * 2)
      case Node(l, r) => Node(double(l), double(r))
    }
}

sealed trait Tree {
  def sum: Int
  def double: Tree
}
final case class Node(l: Tree, r: Tree) extends Tree {
  def sum: Int =
    l.sum + r.sum

  def double: Tree =
    Node(l.double, r.double)
}
final case class Leaf(elt: Int) extends Tree {
  def sum: Int =
    elt

  def double: Tree =
    Leaf(elt * 2)
}

4.7 Extended Examples

To test your skills with algebraic data types and structural recursion here are some larger projects to attempt.

4.7.0.1 A Calculator

In this exercise we’ll implement a simple interpreter for programs containing only numeric operations.

We start by defining some types to represent the expressions we’ll be operating on. In the compiler literature this is known as an abstract syntax tree.

Our representation is:

Implement this in Scala.

This is a straightforward algebraic data type.

sealed trait Expression
final case class Addition(left: Expression, right: Expression) extends Expression
final case class Subtraction(left: Expression, right: Expression) extends Expression
final case class Number(value: Double) extends Expression

Now implement a method eval that converts an Expression to a Double. Use polymorphism or pattern matching as you see fit. Explain your choice of implementation method.

I used pattern matching as it’s more compact and I feel this makes the code easier to read.

sealed trait Expression {
  def eval: Double =
    this match {
      case Addition(l, r) => l.eval + r.eval
      case Subtraction(l, r) => l.eval - r.eval
      case Number(v) => v
    }
}
final case class Addition(left: Expression, right: Expression) extends Expression
final case class Subtraction(left: Expression, right: Expression) extends Expression
final case class Number(value: Int) extends Expression

We’re now going to add some expressions that call fail: division and square root. Start by extending the abstract syntax tree to include representations for Division and SquareRoot.

sealed trait Expression
final case class Addition(left: Expression, right: Expression) extends Expression
final case class Subtraction(left: Expression, right: Expression) extends Expression
final case class Division(left: Expression, right: Expression) extends Expression
final case class SquareRoot(value: Expression) extends Expression
final case class Number(value: Double) extends Expression

Now we’re going to change eval to represent that a computation can fail. (Double uses NaN to indicate a computation failed, but we want to be helpful to the user and tell them why the computation failed.) Implement an appropriate algebraic data type.

We did this in the previous section.

sealed trait Calculation
final case class Success(result: Double) extends Calculation
final case class Failure(reason: String) extends Calculation

Now change eval to return your result type, which I have called Calculation in my implementation. Here are some examples:

assert(Addition(SquareRoot(Number(-1.0)), Number(2.0)).eval ==
       Failure("Square root of negative number"))
assert(Addition(SquareRoot(Number(4.0)), Number(2.0)).eval == Success(4.0))
assert(Division(Number(4), Number(0)).eval == Failure("Division by zero"))

All this repeated pattern matching gets very tedious, doesn’t it! We’re going to see how we can abstract this in the next section.

sealed trait Expression {
  def eval: Calculation =
    this match {
      case Addition(l, r) =>
          l.eval match {
            case Failure(reason) => Failure(reason)
            case Success(r1) =>
              r.eval match {
                case Failure(reason) => Failure(reason)
                case Success(r2) => Success(r1 + r2)
              }
          }
      case Subtraction(l, r) =>
          l.eval match {
            case Failure(reason) => Failure(reason)
            case Success(r1) =>
              r.eval match {
                case Failure(reason) => Failure(reason)
                case Success(r2) => Success(r1 - r2)
              }
          }
      case Division(l, r) =>
        l.eval match {
          case Failure(reason) => Failure(reason)
          case Success(r1) =>
            r.eval match {
              case Failure(reason) => Failure(reason)
              case Success(r2) =>
                if(r2 == 0)
                  Failure("Division by zero")
                else
                  Success(r1 / r2)
            }
        }
      case SquareRoot(v) =>
        v.eval match {
          case Success(r) =>
            if(r < 0)
              Failure("Square root of negative number")
            else
              Success(Math.sqrt(r))
          case Failure(reason) => Failure(reason)
        }
      case Number(v) => Success(v)
    }
}
final case class Addition(left: Expression, right: Expression) extends Expression
final case class Subtraction(left: Expression, right: Expression) extends Expression
final case class Division(left: Expression, right: Expression) extends Expression
final case class SquareRoot(value: Expression) extends Expression
final case class Number(value: Int) extends Expression

4.7.0.2 JSON

In the calculator exercise we gave you the algebraic data type representation. In this exercise we want you to design the algebraic data type yourself. We’re going to work in what is hopefully a familiar domain: JSON.

Design an algebraic data type to represent JSON. Don’t go directly to code. Start by sketching out the design in terms of logical ands and ors—the building blocks of algebraic data types. You might find it useful to use a notation similar to BNF. For example, we could represent the Expression data type from the previous exercise as follows:

Expression ::= Addition left:Expression right:Expression
             | Subtraction left:Expression right:Expression
             | Division left:Expression right:Expression
             | SquareRoot value:Expression
             | Number value:Int

This simplified notation allows us to concentrate on the structure of the algebraic data type without worrying about the intricacies of Scala syntax.

Note you’ll need a sequence type to model JSON, and we haven’t looked at Scala’s collection library yet. However we have seen how to implement a list as an algebraic data type.

Here are some examples of JSON you’ll need to be able to represent

["a string", 1.0, true]
{
  "a": [1,2,3],
  "b": ["a","b","c"]
  "c": { "doh":true, "ray":false, "me":1 }
}

There are many possible ways to model JSON. Here’s one, which is a fairly direct translation of the railroad diagrams in the JSON spec.

Json ::= JsNumber value:Double
       | JsString value:String
       | JsBoolean value:Boolean
       | JsNull
       | JsSequence
       | JsObject
JsSequence ::= SeqCell head:Json tail:JsSequence
             | SeqEnd
JsObject ::= ObjectCell key:String value:Json tail:JsObject
           | ObjectEnd

Translate your representation to Scala code.

This should be a mechanical process. This is the point of algebraic data types—we do the work in modelling the data, and the code follows directly from that model.

sealed trait Json
final case class JsNumber(value: Double) extends Json
final case class JsString(value: String) extends Json
final case class JsBoolean(value: Boolean) extends Json
case object JsNull extends Json
sealed trait JsSequence extends Json
final case class SeqCell(head: Json, tail: JsSequence) extends JsSequence
case object SeqEnd extends JsSequence
sealed trait JsObject extends Json
final case class ObjectCell(key: String, value: Json, tail: JsObject) extends JsObject
case object ObjectEnd extends JsObject

Now add a method to convert your JSON representation to a String. Make sure you enclose strings in quotes, and handle arrays and objects properly.

This is an application of structural recursion, as all transformations on algebraic data types are, with the wrinkle that we have to treat the sequence types specially. Here is my solution.

object json {
  sealed trait Json {
    def print: String = {
      def quote(s: String): String =
        '"'.toString ++ s ++ '"'.toString
      def seqToJson(seq: SeqCell): String =
        seq match {
          case SeqCell(h, t @ SeqCell(_, _)) =>
            s"${h.print}, ${seqToJson(t)}"
          case SeqCell(h, SeqEnd) => h.print
        }

      def objectToJson(obj: ObjectCell): String =
        obj match {
          case ObjectCell(k, v, t @ ObjectCell(_, _, _)) =>
            s"${quote(k)}: ${v.print}, ${objectToJson(t)}"
          case ObjectCell(k, v, ObjectEnd) =>
            s"${quote(k)}: ${v.print}"
        }

      this match {
        case JsNumber(v) => v.toString
        case JsString(v) => quote(v)
        case JsBoolean(v) => v.toString
        case JsNull => "null"
        case s @ SeqCell(_, _) => "[" ++ seqToJson(s) ++ "]"
        case SeqEnd => "[]"
        case o @ ObjectCell(_, _, _) => "{" ++ objectToJson(o) ++ "}"
        case ObjectEnd => "{}"
      }
    }
  }
  final case class JsNumber(value: Double) extends Json
  final case class JsString(value: String) extends Json
  final case class JsBoolean(value: Boolean) extends Json
  case object JsNull extends Json
  sealed trait JsSequence extends Json
  final case class SeqCell(head: Json, tail: JsSequence) extends JsSequence
  case object SeqEnd extends JsSequence
  sealed trait JsObject extends Json
  final case class ObjectCell(key: String, value: Json, tail: JsObject) extends JsObject
  case object ObjectEnd extends JsObject
}

Test your method works. Here are some examples using the representation I chose.

SeqCell(JsString("a string"), SeqCell(JsNumber(1.0), SeqCell(JsBoolean(true), SeqEnd))).print
// res0: String = ["a string", 1.0, true]

ObjectCell(
  "a", SeqCell(JsNumber(1.0), SeqCell(JsNumber(2.0), SeqCell(JsNumber(3.0), SeqEnd))),
  ObjectCell(
    "b", SeqCell(JsString("a"), SeqCell(JsString("b"), SeqCell(JsString("c"), SeqEnd))),
    ObjectCell(
      "c", ObjectCell("doh", JsBoolean(true),
             ObjectCell("ray", JsBoolean(false),
               ObjectCell("me", JsNumber(1.0), ObjectEnd))),
      ObjectEnd
    )
  )
).print
// res1: String = {"a": [1.0, 2.0, 3.0], "b": ["a", "b", "c"], "c": {"doh": true, "ray": false, "me": 1.0}}

4.7.0.3 Music

In the JSON exercise there was a well defined specification to model. In this exercise we want to work on modelling skills given a rather fuzzy specification. The goal is to model music. You can choose to interpret this how you want, making your model as simple or complex as you like. The critical thing is to be able to justify the decisions you made, and to understand the limits of your model.

You might find it easiest to use the BNF notation, introduced in the JSON exercise, to write down your model.

My solution models a very simplified version of Western music. My fundamental “atom” is the note, which consists of a pitch and a duration.

Note ::= pitch:Pitch duration:Duration

I’m assuming I have a data for Pitch representing tones on the standard musical scale from C0 (about 16Hz) to C8. Something like

Pitch ::= C0 | CSharp0 | D0 | DSharp0 | F0 | FSharp0 | ... | C8 | Rest

Note that I included Rest as a pitch, so I can model silence.

We already seem some limitations. I’m not modelling notes that fall outside the scale (microtones) or music systems that use other scales. Furthermore, in most tuning systems flats and their enharmonic sharps (e.g. C-sharp and D-flat) are not the same note, but I’m ignoring that distinction here.

We could break this representation down further into a tone

Tone ::= C | CSharp | D | DSharp | F | FSharp | ... | B

and an octave

Octave ::= 0 | 1 | 2 | ... | 8

and then

Pitch ::= tone:Tone octave:Octave

Durations are a mess in standard musical notation. There are a bunch of named durations (semitone, quaver, etc.) along with dots and tied notes to represent other durations. We can do better by simply saying our music has an atomic unit of time, which we’ll call a beat, and each duration is zero or more beats.

Duration ::= 0 | 1 | 2 | ...

In other words, Duration is a natural number. In Scala we might model this with an Int, or create a type to represent the additional constraint we put over Int.

Again, this representation comes with limitations. Namely we can’t represent music that doesn’t fit cleanly into some division of time—so called free time music.

Finally we should get to means of composition of notes. There are two main ways: we can play notes in sequence or at the same time.

Phrase ::= Sequence | Parallek
Sequence ::= SeqCell phrase:Phrase tail:Sequence
           | SeqEnd

Parallel ::= ParCell phrase:Phrase tail:Parallel
           | ParEnd

This representation allows us to arbitrarily nest parallel and sequential units of notes. We might prefer a normalised representation, such as

Sequence ::= SeqCell note:Note tail:Sequence
           | SeqEnd

Parallel ::= ParCell sequence:Sequence tail:Parallel
           | ParEnd

There are many things missing from this model. Some of them include:

  • We don’t model musical dynamics in any way. Notes can be louder or softer, and volume can change while a note is being played. Notes do not always have constant pitch, either. Pitch bends or slurs are examples of changing pitches in a single note

  • We haven’t modelled different instruments at all.

  • We haven’t modelled effects, like echo and distortion, that make up an important part of modern music.

4.8 Conclusions

In this chapter we have made an extremely important change in our focus, away from language features and towards the programming patterns they support. This continues for the rest of the book.

We have explored two extremely important patterns: algebraic data types and structural recursion. These patterns allow us to go from a mental model of data, to the representation and processing of that data in Scala in an almost entirely mechanical way. Not only in the structure of our code formulaic, and thus easy to comprehend, but the compiler can catch common errors for us which makes development and maintenance easier. These two tools are among the most commonly used in idiomatic functional code, and it is hard to over-emphasize their importance.

In the exercises we developed a few common data structures, but we were limited to storing a fixed type of data, and our code contained a lot of repetition. In the next section we will look at how we can abstract over types and methods, and introduce some important concepts of sequencing operations.

5 Sequencing Computations

In this section we’re going to look at two more language features, generics and functions, and see some abstractions we can build using these features: functors, and monads.

Our starting point is code that we developed in the previous section. We developed IntList, a list of integers, and wrote code like the following:

sealed trait IntList {
  def length: Int =
    this match {
      case End => 0
      case Pair(hd, tl) => 1 + tl.length
    }
  def double: IntList =
    this match {
      case End => End
      case Pair(hd, tl) => Pair(hd * 2, tl.double)
    }
  def product: Int =
    this match {
      case End => 1
      case Pair(hd, tl) => hd * tl.product
    }
  def sum: Int =
    this match {
      case End => 0
      case Pair(hd, tl) => hd + tl.sum
    }
}
case object End extends IntList
final case class Pair(head: Int, tail: IntList) extends IntList

There are two problems with this code. The first is that our list is restricted to storing Ints. The second problem is that here is a lot of repetition. The code has the same general structure, which is unsurprising given we’re using our structural recursion pattern, and it would be nice to reduce the amount of duplication.

We will address both problems in this section. For the former we will use generics to abstract over types, so we can create data that works with user specified types. For the latter we will use functions to abstract over methods, so we can reduce duplication in our code.

As we work with these techniques we’ll see some general patterns emerge. We’ll name and investigate these patterns in more detail at the end of this section.

5.1 Generics

Generic types allow us to abstract over types. There are useful for all sorts of data structures, but commonly encountered in collections so that’s where we’ll start.

5.1.1 Pandora’s Box

Let’s start with a collection that is even simpler than our list—a box that stores a single value. We don’t care what type is stored in the box, but we want to make sure we preserve that type when we get the value out of the box. To do this we use a generic type.

final case class Box[A](value: A)
Box(2)
// res0: Box[Int] = Box(2)

res0.value
// res1: Int = 2

Box("hi") // if we omit the type parameter, scala will infer its value
// res2: Box[String] = Box(hi)

res2.value
// res3: String = hi

The syntax [A] is called a type parameter. We can also add type parameters to methods, which limits the scope of the parameter to the method declaration and body:

def generic[A](in: A): A = in
generic[String]("foo")
// res4: String = foo

generic(1) // again, if we omit the type parameter, scala will infer it
// res5: Int = 1

Type parameters work in a way analogous to method parameters. When we call a method we bind the method’s parameter names to the values given in the method call. For example, when we call generic(1) the name in is bound to the value 1 within the body of generic.

When we call a method or construct a class with a type parameter, the type parameter is bound to the concrete type within the method or class body. So when we call generic(1) the type parameter A is bound to Int in the body of generic.

Type Parameter Syntax

We declare generic types with a list of type names within square brackets like [A, B, C]. By convention we use single uppercase letters for generic types.

Generic types can be declared in a class or trait declaration in which case they are visible throughout the rest of the declaration.

case class Name[A](...){ ... }
trait Name[A]{ ... }

Alternatively they may be declared in a method declaration, in which case they are only visible within the method.

def name[A](...){ ... }

5.1.2 Generic Algebraic Data Types

We described type parameters as analogous to method parameters, and this analogy continues when extending a trait that has type parameters. Extending a trait, as we do in a sum type, is the type level equivalent of calling a method and we must supply values for any type parameters of the trait we’re extending.

In previous sections we’ve seen sum types like the following:

sealed trait Calculation
final case class Success(result: Double) extends Calculation
final case class Failure(reason: String) extends Calculation

Let’s generalise this so that our result is not restricted to a Double but can be some generic type. In doing so let’s change the name from Calculation to Result as we’re not restricted to numeric calculations anymore. Now our data definition becomes:

A Result of type A is either a Success of type A or a Failure with a String reason. This translates to the following code

sealed trait Result[A]
case class Success[A](result: A) extends Result[A]
case class Failure[A](reason: String) extends Result[A]

Notice that both Success and Failure introduce a type parameter A which is passed to Result when it is extended. Success also has a value of type A, but Failure only introduces A so it can pass it onward to Result. In a later section we’ll introduce variance, giving us a cleaner way to implement this, but for now this is the pattern we’ll use.

Invariant Generic Sum Type Pattern

If A of type T is a B or C write

sealed trait A[T]
final case class B[T]() extends A[T]
final case class C[T]() extends A[T]

5.1.3 Exercises

5.1.3.1 Generic List

Our IntList type was defined as

sealed trait IntList
case object End extends IntList
final case class Pair(head: Int, tail: IntList) extends IntList

Change the name to LinkedList and make it generic in the type of data stored in the list.

This is an application of the generic sum type pattern.

sealed trait LinkedList[A]
final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A]
final case class End[A]() extends LinkedList[A]

5.1.3.2 Working With Generic Types

There isn’t much we can do with our LinkedList type. Remember that types define the available operations, and with a generic type like A there isn’t a concrete type to define any available operations. (Generic types are made concrete when a class is instantiated, which is too late to make use of the information in the definition of the class.)

However, we can still do some useful things with our LinkedList! Implement length, returning the length of the LinkedList. Some test cases are below.

val example = Pair(1, Pair(2, Pair(3, End())))
assert(example.length == 3)
assert(example.tail.length == 2)
assert(End().length == 0)

This code is largely unchanged from the implementation of length on IntList.

sealed trait LinkedList[A] {
  def length: Int =
    this match {
      case Pair(hd, tl) => 1 + tl.length
      case End() => 0
    }
}
final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A]
final case class End[A]() extends LinkedList[A]

On the JVM we can compare all values for equality. Implement a method contains that determines whether or not a given item is in the list. Ensure your code works with the following test cases:

val example = Pair(1, Pair(2, Pair(3, End())))
assert(example.contains(3) == true)
assert(example.contains(4) == false)
assert(End().contains(0) == false)
// This should not compile
// example.contains("not an Int")

This is another example of the standard structural recursion pattern. The important point is contains takes a parameter of type A.

sealed trait LinkedList[A] {
  def contains(item: A): Boolean =
    this match {
      case Pair(hd, tl) =>
        if(hd == item)
          true
        else
          tl.contains(item)
      case End() => false
    }
}

final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A]
final case class End[A]() extends LinkedList[A]

Implement a method apply that returns the nth item in the list

Hint: If you need to signal an error in your code (there’s one situation in which you will need to do this), consider throwing an exception. Here is an example:

throw new Exception("Bad things happened")

Ensure your solution works with the following test cases:

val example = Pair(1, Pair(2, Pair(3, End())))
assert(example(0) == 1)
assert(example(1) == 2)
assert(example(2) == 3)
assert(try {
  example(3)
  false
} catch {
  case e: Exception => true
})

There are a few interesting things in this exercise. Possibly the easiest part is the use of the generic type as the return type of the apply method.

Next up is the End case, which the hint suggested you through an Exception for. Strictly speaking we should throw Java’s IndexOutOfBoundsException in this instance, but we will shortly see a way to remove exception handling from our code altogether.

Finally we get to the actual structural recursion, which is perhaps the trickiest part. The key insight is that if the index is zero, we’re selecting the current element, otherwise we subtract one from the index and recurse. We can recursively define the integers in terms of addition by one. For example, 3 = 2 + 1 = 1 + 1 + 1. Here we are performing structural recursion on the list and on the integers.

sealed trait LinkedList[A] {
  def apply(index: Int): A =
    this match {
      case Pair(hd, tl) =>
        if(index == 0)
          hd
        else
          tl(index - 1)
      case End() =>
        throw new Exception("Attempted to get element from an Empty list")
    }
}
final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A]
final case class End[A]() extends LinkedList[A]

Throwing an exception isn’t cool. Whenever we throw an exception we lose type safety as there is nothing in the type system that will remind us to deal with the error. It would be much better to return some kind of result that encodes we can succeed or failure. We introduced such a type in this very section.

sealed trait Result[A]
case class Success[A](result: A) extends Result[A]
case class Failure[A](reason: String) extends Result[A]

Change apply so it returns a Result, with a failure case indicating what went wrong. Here are some test cases to help you:

assert(example(0) == Success(1))
assert(example(1) == Success(2))
assert(example(2) == Success(3))
assert(example(3) == Failure("Index out of bounds"))
sealed trait Result[A]
case class Success[A](result: A) extends Result[A]
case class Failure[A](reason: String) extends Result[A]

sealed trait LinkedList[A] {
  def apply(index: Int): Result[A] =
    this match {
      case Pair(hd, tl) =>
        if(index == 0)
          Success(hd)
        else
          tl(index - 1)
      case End() =>
        Failure("Index out of bounds")
    }
}
final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A]
final case class End[A]() extends LinkedList[A]

5.2 Functions

Functions allow us to abstract over methods, turning methods into values that we can pass around and manipulate within our programs.

Let’s look at three methods we wrote that manipulate IntList.

sealed trait IntList {
  def length: Int =
    this match {
      case End => 0
      case Pair(hd, tl) => 1 + tl.length
    }
  def double: IntList =
    this match {
      case End => End
      case Pair(hd, tl) => Pair(hd * 2, tl.double)
    }
  def product: Int =
    this match {
      case End => 1
      case Pair(hd, tl) => hd * tl.product
    }
  def sum: Int =
    this match {
      case End => 0
      case Pair(hd, tl) => hd + tl.sum
    }
}

case object End extends IntList
case class Pair(hd: Int, tl: IntList) extends IntList

All of these methods have the same general pattern, which is not surprising as they all use structural recursion. It would be nice to be able to remove the duplication.

Let’s start by focusing on the methods that return an Int: length, product, and sum. We want to write a method like

def abstraction(end: Int, f: ???): Int =
  this match {
    case End => end
    case Pair(hd, tl) => f(hd, tl.abstraction(end, f))
  }

I’ve used f to denote some kind of object that does the combination of the head and recursive call for the Pair case. At the moment we don’t know how to write down the type of this value, or how to construct one. However, we can guess from the title of this section that what we want is a function!

A function is like a method: we can call it with parameters and it evaluates to a result. Unlike a method a function is value. We can pass a function to a method or to another function. We can return a function from a method, and so on.

Much earlier in this course we introduced the apply method, which lets us treat objects as functions in a syntactic sense:

object add1 {
  def apply(in: Int) = in + 1
}
add1(2)
// res0: Int = 3

This is a big step towards doing real functional programming in Scala but we’re missing one important component: types.

As we have seen, types allow us to abstract across values. We’ve seen special case functions like Adders, but what we really want is a generalised set of types that allow us to represent computations of any kind.

Enter Scala’s Function types.

5.2.1 Function Types

We write a function type like (A, B) => C where A and B are the types of the parameters and C is the result type. The same pattern generalises from functions of no arguments to an arbitrary number of arguments.

In our example above we want f to be a function that accepts two Ints as parameters and returns an Int. Thus we can write it as (Int, Int) => Int.

Function Type Declaration Syntax

To declare a function type, write

(A, B, ...) => C

where

  • A, B, ... are the types of the input parameters; and
  • C is the type of the result.

If a function only has one parameter the parentheses may be dropped:

A => B

5.2.2 Function literals

Scala also gives us a function literal syntax specifically for creating new functions. Here are some example function literals:

val sayHi = () => "Hi!"
// sayHi: () => String = <function0>

sayHi()
// res1: String = Hi!

val add1 = (x: Int) => x + 1
// add1: Int => Int = <function1>

add1(10)
// res2: Int = 11

val sum = (x: Int, y:Int) => x + y
// sum: (Int, Int) => Int = <function2>

sum(10, 20)
// res3: Int = 30

In code where we know the argument types, we can sometimes drop the type annotations and allow Scala to infer them10. There is no syntax for declaring the result type of a function and it is normally inferred, but if we find ourselves needing to do this we can put a type on the function’s body expression:

(x: Int) => (x + 1): Int

Function Literal Syntax

The syntax for declaring a function literal is

(parameter: type, ...) => expression

where - the optional parameters are the names given to the function parameters; - the types are the types of the function parameters; and - the expression determines the result of the function.

5.2.3 Exercises

5.2.3.1 A Better Abstraction

We started developing an abstraction over sum, length, and product which we sketched out as

def abstraction(end: Int, f: ???): Int =
  this match {
    case End => end
    case Pair(hd, tl) => f(hd, tl.abstraction(end, f))
  }

Rename this function to fold, which is the name it is usually known as, and finish the implementation.

Your fold method should look like this:

sealed trait IntList {
  def fold(end: Int, f: (Int, Int) => Int): Int =
    this match {
      case End => end
      case Pair(hd, tl) => f(hd, tl.fold(end, f))
    }

  // other methods...
}
case object End extends IntList
final case class Pair(head: Int, tail: IntList) extends IntList

Now reimplement sum, length, and product in terms of fold.

sealed trait IntList {
  def fold(end: Int, f: (Int, Int) => Int): Int =
    this match {
      case End => end
      case Pair(hd, tl) => f(hd, tl.fold(end, f))
    }
  def length: Int =
    fold(0, (_, tl) => 1 + tl)
  def product: Int =
    fold(1, (hd, tl) => hd * tl)
  def sum: Int =
    fold(0, (hd, tl) => hd + tl)
}
case object End extends IntList
final case class Pair(head: Int, tail: IntList) extends IntList

Is it more convenient to rewrite methods in terms of fold if they were implemented using pattern matching or polymorphic? What does this tell us about the best use of fold?

When using fold in polymorphic implementations we have a lot of duplication; the polymorphic implementations without fold were simpler to write. The pattern matching implementations benefitted from fold as we removed the duplication in the pattern matching.

In general fold makes a good interface for users outside the class, but not necessarily for use inside the class.

Why can’t we write our double method in terms of fold? Is it feasible we could if we made some change to fold?

The types tell us it won’t work. fold returns an Int and double returns an IntList. However the general structure of double is captured by fold. This is apparent if we look at them side-by-side:

def double: IntList =
  this match {
    case End => End
    case Pair(hd, tl) => Pair(hd * 2, tl.double)
  }

def fold(end: Int, f: (Int, Int) => Int): Int =
  this match {
    case End => end
    case Pair(hd, tl) => f(hd, tl.fold(end, f))
  }

If we could generalise the types of fold from Int to some general type then we could write double. And that, dear reader, is what we turn to next.

Implement a generalised version of fold and rewrite double in terms of it.

We want to generalise the return type of fold. Our starting point is

def fold(end: Int, f: (Int, Int) => Int): Int

Replacing the return type and tracing it back we arrive at

def fold[A](end: A, f: (Int, A) => A): A

where we’ve used a generic type on the method to capture the changing return type. With this we can implement double. When we try to do so we’ll see that type inference fails, so we have to give it a bit of help.

sealed trait IntList {
  def fold[A](end: A, f: (Int, A) => A): A =
    this match {
      case End => end
      case Pair(hd, tl) => f(hd, tl.fold(end, f))
    }
  def length: Int =
    fold[Int](0, (_, tl) => 1 + tl)
  def product: Int =
    fold[Int](1, (hd, tl) => hd * tl)
  def sum: Int =
    fold[Int](0, (hd, tl) => hd + tl)
  def double: IntList =
    fold[IntList](End, (hd, tl) => Pair(hd * 2, tl))
}
case object End extends IntList
final case class Pair(head: Int, tail: IntList) extends IntList

5.3 Generic Folds for Generic Data

We’ve seen that when we define a class with generic data, we cannot implement very many methods on that class. The user supplies the generic type, and thus we must ask the user to supply functions that work with that type. Nonetheless, there are some common patterns for using generic data, which is what we explore in this section. We have already seen fold in the context of our IntList. Here we will explore fold in more detail, and learn the pattern for implementing fold for any algebraic data type.

5.3.1 Fold

Last time we saw fold we were working with a list of integers. Let’s generalise to a list of a generic type. We’ve already seen all the tools we need. First our data definition, in this instance slightly modified to use the invariant sum type pattern.

sealed trait LinkedList[A]
final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A]
final case class End[A]() extends LinkedList[A]

The last version of fold that we saw on IntList was

def fold[A](end: A, f: (Int, A) => A): A =
  this match {
    case End => end
    case Pair(hd, tl) => f(hd, tl.fold(end, f))
  }

It’s reasonably straightforward to extend this to LinkedList[A]. We merely have to account for the head element of a Pair being of type A not Int.

sealed trait LinkedList[A] {
  def fold[B](end: B, f: (A, B) => B): B =
    this match {
      case End() => end
      case Pair(hd, tl) => f(hd, tl.fold(end, f))
    }
}
final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A]
final case class End[A]() extends LinkedList[A]

Fold is just an adaptation of structural recursion where we allow the user to pass in the functions we apply at each case. As structural recursion is the generic pattern for writing any function that transforms an algebraic datatype, fold is the concrete realisation of this generic pattern. That is, fold is the generic transformation or iteration method. Any function you care to write on an algebraic datatype can be written in terms of fold.

Fold Pattern

For an algebraic datatype A, fold converts it to a generic type B. Fold is a structural recursion with:

  • one function parameter for each case in A;
  • each function takes as parameters the fields for its associated class;
  • if A is recursive, any function parameters that refer to a recursive field take a parameter of type B.

The right-hand side of pattern matching cases, or the polymorphic methods as appropriate, consists of calls to the appropriate function.

Let’s apply the pattern to derive the fold method above. We start with our basic template:

sealed trait LinkedList[A] {
  def fold[B](???): B =
    this match {
      case End() => ???
      case Pair(hd, tl) => ???
    }
}
final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A]
final case class End[A]() extends LinkedList[A]

This is just the structural recursion template with the addition of a generic type parameter for the return type.

Now we add one function for each of the two classes in LinkedList.

def fold[B](end: ???, pair: ???): B =
  this match {
    case End() => ???
    case Pair(hd, tl) => ???
  }

From the rules for the function types:

Substituting in we get

def fold[B](end: B, pair: (A, B) => B): B =
  this match {
    case End() => end
    case Pair(hd, tl) => pair(hd, tl.fold(end, pair))
  }

5.3.2 Working With Functions

There are a few tricks in Scala for working with functions and methods that accept functions (known as higher-order methods). Here we are going to look at:

  1. a compact syntax for writing functions;
  2. converting methods to functions; and
  3. a way to write higher-order methods that assists type inference.

5.3.2.1 Placeholder syntax

In very simple situations we can write inline functions using an extreme shorthand called placeholder syntax. It looks like this:

((_: Int) * 2)
// res: Int => Int = <function1>

(_: Int) * 2 is expanded by the compiler to (a: Int) => a * 2. It is more idiomatic to use the placeholder syntax only in the cases where the compiler can infer the types. Here are a few more examples:

_ + _     // expands to `(a, b) => a + b`
foo(_)    // expands to `(a) => foo(a)`
foo(_, b) // expands to `(a) => foo(a, b)`
_(foo)    // expands to `(a) => a(foo)`
// and so on...

Placeholder syntax, while wonderfully terse, can be confusing for large expressions and should only be used for very small functions.

5.3.3 Converting methods to functions

Scala contains another feature that is directly relevant to this section—the ability to convert method calls to functions. This is closely related to placeholder syntax—simply follow a method with an underscore:

object Sum {
  def sum(x: Int, y: Int) = x + y
}
Sum.sum
// <console>:23: error: missing argument list for method sum in object Sum
// Unapplied methods are only converted to functions when a function type is expected.
// You can make this conversion explicit by writing `sum _` or `sum(_,_)` instead of `sum`.
//        Sum.sum
//            ^
(Sum.sum _)
// res1: (Int, Int) => Int = <function2>

In situations where Scala can infer that we need a function, we can even drop the underscore and simply write the method name—the compiler will promote the method to a function automatically:

object MathStuff {
  def add1(num: Int) = num + 1
}
Counter(2).adjust(MathStuff.add1)
// res2: Counter = Counter(3)

5.3.3.1 Multiple Parameter Lists

Methods in Scala can actually have multiple parameter lists. Such methods work just like normal methods, except we must bracket each parameter list separately.

def example(x: Int)(y: Int) = x + y
// example: (x: Int)(y: Int)Int

example(1)(2)
// res3: Int = 3

Multiple parameter lists have two relevant uses: they look nicer when defining functions inline and they assist with type inference.

The former is the ability to write functions that look like code blocks. For example, if we define fold as

def fold[B](end: B)(pair: (A, B) => B): B =
  this match {
    case End() => end
    case Pair(hd, tl) => pair(hd, tl.fold(end, pair))
  }

then we can call it as

fold(0){ (total, elt) => total + elt }

which is a bit easier to read than

fold(0, (total, elt) => total + elt)

More important is the use of multiple parameter lists to ease type inference. Scala’s type inference algorithm cannot use a type inferred for one parameter for another parameter in the same list. For example, given fold with a signature like

def fold[B](end: B, pair: (A, B) => B): B

if Scala infers B for end it cannot then use this inferred type for the B in pair, so we must often write a type declaration on pair. However, Scala can use types inferred for one parameter list in another parameter list. So if we write fold as

def fold[B](end: B)(pair: (A, B) => B): B

then inferring B from end (which is usually easy) allows B to be used when inferring the type pair. This means fewer type declarations and a smoother development process.

5.3.4 Exercises

5.3.4.1 Tree

A binary tree can be defined as follows:

A Tree of type A is a Node with a left and right Tree or a Leaf with an element of type A.

Implement this algebraic data type along with a fold method.

This is another recursive data type just like list. Follow the patterns and you should be ok.

sealed trait Tree[A] {
  def fold[B](node: (B, B) => B, leaf: A => B): B
}
final case class Node[A](left: Tree[A], right: Tree[A]) extends Tree[A] {
  def fold[B](node: (B, B) => B, leaf: A => B): B =
    node(left.fold(node, leaf), right.fold(node, leaf))
}
final case class Leaf[A](value: A) extends Tree[A] {
  def fold[B](node: (B, B) => B, leaf: A => B): B =
    leaf(value)
}

Using fold convert the following Tree to a String

val tree: Tree[String] =
  Node(Node(Leaf("To"), Leaf("iterate")),
       Node(Node(Leaf("is"), Leaf("human,")),
            Node(Leaf("to"), Node(Leaf("recurse"), Leaf("divine")))))

Remember you can append Strings using the + method.

Note it is necessary to instantiate the generic type variable for fold. Type inference fails in this case.

tree.fold[String]((a, b) => a + " " + b, str => str)

5.4 Modelling Data with Generic Types

In this section we’ll see the additional power the generic types give us when modelling data. We see that with generic types we can implement generic sum and product types, and also model some other useful abstractions such as optional values.

5.4.1 Generic Product Types

Let’s look at using generics to model a product type. Consider a method that returns two values—for example, an Int and a String, or a Boolean and a Double:

def intAndString: ??? = // ...

def booleanAndDouble: ??? = // ...

The question is what do we use as the return types? We could use a regular class without any type parameters, with our usual algebraic data type patterns, but then we would have to implement one version of the class for each combination of return types:

case class IntAndString(intValue: Int, stringValue: String)

def intAndString: IntAndString = // ...

case class BooleanAndDouble(booleanValue: Boolean, doubleValue: Double)

def booleanAndDouble: BooleanAndDouble = // ...

The answer is to use generics to create a product type—for example a Pair—that contains the relevant data for both return types:

def intAndString: Pair[Int, String] = // ...

def booleanAndDouble: Pair[Boolean, Double] = // ...

Generics provide a different approach to defining product types— one that relies on aggregation as opposed to inheritance.

5.4.1.1 Exercise: Pairs

Implement the Pair class from above. It should store two values—one and two—and be generic in both arguments. Example usage:

val pair = Pair[String, Int]("hi", 2)
// pair: Pair[String,Int] = Pair(hi,2)

pair.one
// res0: String = hi

pair.two
// res1: Int = 2

If one type parameter is good, two type parameters are better:

case class Pair[A, B](one: A, two: B)

This is just the product type pattern we have seen before, but we introduce generic types.

Note that we don’t always need to specify the type parameters when we construct Pairs. The compiler will attempt to infer the types as usual wherever it can:

val pair = Pair("hi", 2)
// pair: Pair[String,Int] = Pair(hi,2)

5.4.2 Tuples

A tuple is the generalisation of a pair to more terms. Scala includes built-in generic tuple types with up to 22 elements, along with special syntax for creating them. With these classes we can represent any kind of this and that relationship between almost any number of terms.

The classes are called Tuple1[A] through to Tuple22[A, B, C, ...] but they can also be written in the sugared11 form (A, B, C, ...). For example:

Tuple2("hi", 1) // unsugared syntax
// res2: (String, Int) = (hi,1)

("hi", 1) // sugared syntax
// res3: (String, Int) = (hi,1)

("hi", 1, true)
// res4: (String, Int, Boolean) = (hi,1,true)

We can define methods that accept tuples as parameters using the same syntax:

def tuplized[A, B](in: (A, B)) = in._1
// tuplized: [A, B](in: (A, B))A

tuplized(("a", 1))
// res5: String = a

We can also pattern match on tuples as follows:

(1, "a") match {
  case (a, b) => a + b
}
// res6: String = 1a

Although pattern matching is the natural way to deconstruct a tuple, each class also has a complement of fields named _1, _2 and so on:

val x = (1, "b", true)
// x: (Int, String, Boolean) = (1,b,true)

x._1
// res7: Int = 1

x._3
// res8: Boolean = true

5.4.3 Generic Sum Types

Now let’s look at using generics to model a sum type. Again, we have previously implemented this using our algebraic data type pattern, factoring out the common aspects into a supertype. Generics allow us to abstract over this pattern, providing a … well … generic implementation.

Consider a method that, depending on the value of its parameters, returns one of two types:

def intOrString(input: Boolean) =
  if(input == true) 123 else "abc"
// intOrString: (input: Boolean)Any

We can’t simply write this method as shown above because the compiler infers the result type as Any. Instead we have to introduce a new type to explicitly represent the disjunction:

def intOrString(input: Boolean): Sum[Int, String] =
  if(input == true) {
    Left[Int, String](123)
  } else {
    Right[Int, String]("abc")
  }
// intOrString: (input: Boolean)sum.Sum[Int,String]

How do we implement Sum? We just have to use the patterns we’ve already seen, with the addition of generic types.

5.4.3.1 Exercise: Generic Sum Type

Implement a trait Sum[A, B] with two subtypes Left and Right. Create type parameters so that Left and Right can wrap up values of two different types.

Hint: you will need to put both type parameters on all three types. Example usage:

Left[Int, String](1).value
// res9: Int = 1

Right[Int, String]("foo").value
// res10: String = foo

val sum: Sum[Int, String] = Right("foo")
// sum: sum.Sum[Int,String] = Right(foo)

sum match {
  case Left(x) => x.toString
  case Right(x) => x
}
// res11: String = foo

The code is an adaptation of our invariant generic sum type pattern, with another type parameter:

sealed trait Sum[A, B]
final case class Left[A, B](value: A) extends Sum[A, B]
final case class Right[A, B](value: B) extends Sum[A, B]

Scala’s standard library has the generic sum type Either for two cases, but it does not have types for more cases.

5.4.4 Generic Optional Values

Many expressions may sometimes produce a value and sometimes not. For example, when we look up an element in a hash table (associative array) by a key, there may not be a value there. If we’re talking to a web service, that service may be down and not reply to us. If we’re looking for a file, that file may have been deleted. There are a number of ways to model this situation of an optional value. We could throw an exception, or we could return null when a value is not available. The disadvantage of both these methods is they don’t encode any information in the type system.

We generally want to write robust programs, and in Scala we try to utilise the type system to encode properties we want our programs to maintain. One common property is “correctly handle errors”. If we can encode an optional value in the type system, the compiler will force us to consider the case where a value is not available, thus increasing the robustness of our code.

5.4.4.1 Exercise: Maybe that Was a Mistake

Create a generic trait called Maybe of a generic type A with two subtypes, Full containing an A, and Empty containing no value. Example usage:

val perhaps: Maybe[Int] = Empty[Int]

val perhaps: Maybe[Int] = Full(1)

We can apply our invariant generic sum type pattern and get

sealed trait Maybe[A]
final case class Full[A](value: A) extends Maybe[A]
final case class Empty[A]() extends Maybe[A]

5.4.5 Take Home Points

In this section we have used generics to model sum types, product types, and optional values using generics.

These abstractions are commonly used in Scala code and have implementations in the Scala standard library. The sum type is called Either, products are tuples, and optional values are modelled with Option.

5.4.6 Exercises

5.4.6.1 Generics versus Traits

Sum types and product types are general concepts that allow us to model almost any kind of data structure. We have seen two methods of writing these types—traits and generics. When should we consider using each?

Ultimately the decision is up to us. Different teams will adopt different programming styles. However, we look at the properties of each approach to inform our choices:

Inheritance-based approaches—traits and classes—allow us to create permanent data structures with specific types and names. We can name every field and method and implement use-case-specific code in each class. Inheritance is therefore better suited to modelling significant aspects of our programs that are re-used in many areas of our codebase.

Generic data structures—Tuples, Options, Eithers, and so on—are extremely broad and general purpose. There are a wide range of predefined classes in the Scala standard library that we can use to quickly model relationships between data in our code. These classes are therefore better suited to quick, one-off pieces of data manipulation where defining our own types would introduce unnecessary verbosity to our codebase.

5.4.6.2 Folding Maybe

In this section we implemented a sum type for modelling optional data:

sealed trait Maybe[A]
final case class Full[A](value: A) extends Maybe[A]
final case class Empty[A]() extends Maybe[A]

Implement fold for this type.

The code is very similar to the implementation for LinkedList. I choose pattern matching in the base trait for my solution.

sealed trait Maybe[A] {
  def fold[B](full: A => B, empty: B): B =
    this match {
      case Full(v) => full(v)
      case Empty() => empty
    }
}
final case class Full[A](value: A) extends Maybe[A]
final case class Empty[A]() extends Maybe[A]

5.4.6.3 Folding Sum

In this section we implemented a generic sum type:

sealed trait Sum[A, B]
final case class Left[A, B](value: A) extends Sum[A, B]
final case class Right[A, B](value: B) extends Sum[A, B]

Implement fold for Sum.

sealed trait Sum[A, B] {
  def fold[C](left: A => C, right: B => C): C =
    this match {
      case Left(a) => left(a)
      case Right(b) => right(b)
    }
}
final case class Left[A, B](value: A) extends Sum[A, B]
final case class Right[A, B](value: B) extends Sum[A, B]

5.5 Sequencing Computation

We have now mastered generic data and folding over algebraic data types. Now we will look as some other common patterns of computation that are 1) often more convenient to use than fold for algebraic data types and 2) can be implemented for certain types of data that do not support a fold. These methods are known as map and flatMap.

5.5.1 Map

Imagine we have a list of Int user IDs, and a function which, given a user ID, returns a User record. We want to get a list of user records for all the IDs in the list. Written as types we have List[Int] and a function Int => User, and we want to get a List[User].

Imagine we have an optional value representing a user record loaded from the database and a function that will load their most recent order. If we have a record we want to then lookup the user’s most recent order. That is, we have a Maybe[User] and a function User => Order, and we want a Maybe[Order].

Imagine we have a sum type representing an error message or a completed order. If we have a completed order we want to get the total value of the order. That is, we have a Sum[String, Order] and a function Order => Double, and we want Sum[String, Double].

What these all have in common is we have a type F[A] and a function A => B, and we want a result F[B]. The method that performs this operation is called map.

Let’s implement map for LinkedList. We start by outlining the types and adding the general structural recursion skeleton:

sealed trait LinkedList[A] {
  def map[B](fn: A => B): LinkedList[B] =
    this match {
      case Pair(hd, tl) => ???
      case End() => ???
    }
}
final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A]
final case class End[A]() extends LinkedList[A]

We know we can use the structural recursion pattern as we know that fold (which is just the structural recursion pattern abstracted) is the universal iterator for an algebraic data type. Thus:

case Pair(hd, tl) => {
  val newTail: LinkedList[B] = tail.map(fn)
  // Combine newTail and head to create LinkedList[B]
}

We can convert head to a B using fn, and then build a larger list from newTail and our B giving us the final solution

case Pair(hd, tl) => Pair(fn(hd), tl.map(fn))

Therefore the complete solution is

sealed trait LinkedList[A] {
  def map[B](fn: A => B): LinkedList[B] =
    this match {
      case Pair(hd, tl) => Pair(fn(hd), tl.map(fn))
      case End() => End[B]()
    }
}
case class Pair[A](hd: A, tl: LinkedList[A]) extends LinkedList[A]
case class End[A]() extends LinkedList[A]

Notice how using the types and patterns guided us to a solution.

5.5.2 FlatMap

Now imagine the following examples:

What these all have in common is we have a type F[A] and a function A => F[B], and we want a result F[B]. The method that performs this operation is called flatMap.

Let’s implement flatMap for Maybe (we need an append method to implement flatMap for LinkedList). We start by outlining the types:

sealed trait Maybe[A] {
  def flatMap[B](fn: A => Maybe[B]): Maybe[B] = ???
}
final case class Full[A](value: A) extends Maybe[A]
final case class Empty[A]() extends Maybe[A]

We use the same pattern as before: it’s a structural recursion and our types guide us in filling in the method bodies.

sealed trait Maybe[A] {
  def flatMap[B](fn: A => Maybe[B]): Maybe[B] =
    this match {
      case Full(v) => fn(v)
      case Empty() => Empty[B]()
    }
}
final case class Full[A](value: A) extends Maybe[A]
final case class Empty[A]() extends Maybe[A]

5.5.3 Functors and Monads

A type like F[A] with a map method is called a functor. If a functor also has a flatMap method it is called a monad12.

Although the most immediate applications of map and flatMap are in collection classes like lists, the bigger picture is sequencing computations. Imagine we have a number of computations that can fail. For instance

def mightFail1: Maybe[Int] =
  Full(1)

def mightFail2: Maybe[Int] =
  Full(2)

def mightFail3: Maybe[Int] =
  Empty() // This one failed

We want to run these computations one after another. If any one of them fails the whole computation fails. Otherwise we’ll add up all the numbers we get. We can do this with flatMap as follows.

mightFail1 flatMap { x =>
  mightFail2 flatMap { y =>
    mightFail3 flatMap { z =>
      Full(x + y + z)
    }
  }
}

The result of this is Empty. If we drop mightFail3, leaving just

mightFail1 flatMap { x =>
  mightFail2 flatMap { y =>
    Full(x + y)
  }
}

the computation succeeds and we get Full(3).

The general idea is a monad represents a value in some context. The context depends on the monad we’re using. We’ve seen examples where the context is:

We use map when we want to transform the value within the context to a new value, while keeping the context the same. We use flatMap when we want to transform the value and provide a new context.

5.5.4 Exercises

5.5.4.1 Mapping Lists

Given the following list

val list: LinkedList[Int] = Pair(1, Pair(2, Pair(3, End())))

These exercises just get you used to using map.

list.map(_ * 2)
list.map(_ + 1)
list.map(_ / 3)

5.5.4.2 Mapping Maybe

Implement map for Maybe.

sealed trait Maybe[A] {
  def flatMap[B](fn: A => Maybe[B]): Maybe[B] =
    this match {
      case Full(v) => fn(v)
      case Empty() => Empty[B]()
    }
  def map[B](fn: A => B): Maybe[B] =
    this match {
      case Full(v) => Full(fn(v))
      case Empty() => Empty[B]()
    }
}
final case class Full[A](value: A) extends Maybe[A]
final case class Empty[A]() extends Maybe[A]

For bonus points, implement map in terms of flatMap.

sealed trait Maybe[A] {
  def flatMap[B](fn: A => Maybe[B]): Maybe[B] =
    this match {
      case Full(v) => fn(v)
      case Empty() => Empty[B]()
    }
  def map[B](fn: A => B): Maybe[B] =
    flatMap[B](v => Full(fn(v)))
}
final case class Full[A](value: A) extends Maybe[A]
final case class Empty[A]() extends Maybe[A]

5.5.4.3 Sequencing Computations

We’re going to use Scala’s builtin List class for this exercise as it has a flatMap method.

Given this list

val list = List(1, 2, 3)

return a List[Int] containing both all the elements and their negation. Order is not important. Hint: Given an element create a list containing it and its negation.

list.flatMap(x => List(x, -x))

Given this list

val list: List[Maybe[Int]] = List(Full(3), Full(2), Full(1))

return a List[Maybe[Int]] containing None for the odd elements. Hint: If x % 2 == 0 then x is even.

list.map(maybe => maybe.flatMap[Int] { x => if (x % 2 == 0) Full(x) else Empty() })

5.5.4.4 Sum

Recall our Sum type.

sealed trait Sum[A, B] {
  def fold[C](left: A => C, right: B => C): C =
    this match {
      case Left(a) => left(a)
      case Right(b) => right(b)
    }
}
final case class Left[A, B](value: A) extends Sum[A, B]
final case class Right[A, B](value: B) extends Sum[A, B]

To prevent a name collision between the built-in Either, rename the Left and Right cases to Failure and Success respectively.

sealed trait Sum[A, B] {
  def fold[C](error: A => C, success: B => C): C =
    this match {
      case Failure(v) => error(v)
      case Success(v) => success(v)
    }
}
final case class Failure[A, B](value: A) extends Sum[A, B]
final case class Success[A, B](value: B) extends Sum[A, B]

Now things are going to get a bit trickier. We are going to implement map and flatMap, again using pattern matching in the Sum trait. Start with map. The general recipe for map is to start with a type like F[A] and apply a function A => B to get F[B]. Sum however has two generic type parameters. To make it fit the F[A] pattern we’re going to fix one of these parameters and allow map to alter the other one. The natural choice is to fix the type parameter associated with Failure and allow map to alter a Success. This corresponds to “fail-fast” behaviour. If our Sum has failed, any sequenced computations don’t get run.

In summary map should have type

def map[C](f: B => C): Sum[A, C]
sealed trait Sum[A, B] {
  def fold[C](error: A => C, success: B => C): C =
    this match {
      case Failure(v) => error(v)
      case Success(v) => success(v)
    }
  def map[C](f: B => C): Sum[A, C] =
    this match {
      case Failure(v) => Failure(v)
      case Success(v) => Success(f(v))
    }
}
final case class Failure[A, B](value: A) extends Sum[A, B]
final case class Success[A, B](value: B) extends Sum[A, B]

Now implement flatMap using the same logic as map.

sealed trait Sum[A, B] {
  def fold[C](error: A => C, success: B => C): C =
    this match {
      case Failure(v) => error(v)
      case Success(v) => success(v)
    }
  def map[C](f: B => C): Sum[A, C] =
    this match {
      case Failure(v) => Failure(v)
      case Success(v) => Success(f(v))
    }
  def flatMap[C](f: B => Sum[A, C]) =
    this match {
      case Failure(v) => Failure(v)
      case Success(v) => f(v)
    }
}
final case class Failure[A, B](value: A) extends Sum[A, B]
final case class Success[A, B](value: B) extends Sum[A, B]

5.6 Variance

In this section we cover variance annotations, which allow us to control subclass relationships between types with type parameters. To motivate this, let’s look again at our invariant generic sum type pattern.

Recall our Maybe type, which we defined as

sealed trait Maybe[A]
final case class Full[A](value: A) extends Maybe[A]
final case class Empty[A]() extends Maybe[A]

Ideally we would like to drop the unused type parameter on Empty and write something like

sealed trait Maybe[A]
final case class Full[A](value: A) extends Maybe[A]
case object Empty extends Maybe[???]

Objects can’t have type parameters. In order to make Empty an object we need to provide a concrete type in the extends Maybe part of the definition. But what type parameter should we use? In the absence of a preference for a particular data type, we could use something like Unit or Nothing. However this leads to type errors:

sealed trait Maybe[A]
// defined trait Maybe

final case class Full[A](value: A) extends Maybe[A]
// defined class Full

case object Empty extends Maybe[Nothing]
// defined object Empty
// warning: previously defined class Empty is not a companion to object Empty.
// Companions must be defined together; you may wish to use :paste mode for this.
val possible: Maybe[Int] = Empty
// <console>:14: error: type mismatch;
//  found   : Empty.type
//  required: Maybe[Int]
// Note: Nothing <: Int (and Empty.type <: Maybe[Nothing]), but trait Maybe is invariant in type A.
// You may wish to define A as +A instead. (SLS 4.5)
//        val possible: Maybe[Int] = Empty
//                                   ^

The problem here is that Empty is a Maybe[Nothing] and a Maybe[Nothing] is not a subtype of Maybe[Int]. To overcome this issue we need to introduce variance annotations.

5.6.1 Invariance, Covariance, and Contravariance

Variance is Hard

Variance is one of the trickier aspects of Scala’s type system. Although it is useful to be aware of its existence, we rarely have to use it in application code.

If we have some type Foo[A], and A is a subtype of B, is Foo[A] a subtype of Foo[B]? The answer depends on the variance of the type Foo. The variance of a generic type determines how its supertype/subtype relationships change with respect with its type parameters:

A type Foo[T] is invariant in terms of T, meaning that the types Foo[A] and Foo[B] are unrelated regardless of the relationship between A and B. This is the default variance of any generic type in Scala.

A type Foo[+T] is covariant in terms of T, meaning that Foo[A] is a supertype of Foo[B] if A is a supertype of B. Most Scala collection classes are covariant in terms of their contents. We’ll see these next chapter.

A type Foo[-T] is contravariant in terms of T, meaning that Foo[A] is a subtype of Foo[B] if A is a supertype of B. The only example of contravariance that I am aware of is function arguments.

5.6.2 Function Types

When we discussed function types we glossed over how exactly they are implemented. Scala has 23 built-in generic classes for functions of 0 to 22 arguments. Here’s what they look like:

trait Function0[+R] {
  def apply: R
}

trait Function1[-A, +B] {
  def apply(a: A): B
}

trait Function2[-A, -B, +C] {
  def apply(a: A, b: B): C
}

// and so on...

Functions are contravariant in terms of their arguments and covariant in terms of their return type. This seems counterintuitive but it makes sense if we look at it from the point of view of function arguments. Consider some code that expects a Function1[A, B]:

case class Box[A](value: A) {
  /** Apply `func` to `value`, returning a `Box` of the result. */
  def map[B](func: Function1[A, B]): Box[B] =
    Box(func(value))
}

To understand variance, consider what functions can we safely pass to this map method:

5.6.3 Covariant Sum Types

Now we know about variance annotations we can solve our problem with Maybe by making it covariant.

sealed trait Maybe[+A]
final case class Full[A](value: A) extends Maybe[A]
case object Empty extends Maybe[Nothing]

In use we get the behaviour we expect. Empty is a subtype of all Full values.

val perhaps: Maybe[Int] = Empty
// perhaps: Maybe[Int] = Empty

This pattern is the most commonly used one with generic sum types. We should only use covariant types where the container type is immutable. If the container allows mutation we should only use invariant types.

Covariant Generic Sum Type Pattern

If A of type T is a B or C, and C is not generic, write

sealed trait A[+T]
final case class B[T](t: T) extends A[T]
case object C extends A[Nothing]

This pattern extends to more than one type parameter. If a type parameter is not needed for a specific case of a sum type, we can substitute Nothing for that parameter.

5.6.4 Contravariant Position

There is another pattern we need to learn for covariant sum types, which involves the interaction of covariant type parameters and contravariant method and function parameters. To illustrate this issue let’s develop a covariant Sum.

5.6.4.1 Exercise: Covariant Sum

Implement a covariant Sum using the covariant generic sum type pattern.

sealed trait Sum[+A, +B]
final case class Failure[A](value: A) extends Sum[A, Nothing]
final case class Success[B](value: B) extends Sum[Nothing, B]

Now let’s see what happens when we implement flatMap on Sum.

5.6.4.2 Exercise: Some sort of flatMap

Implement flatMap and verify you receive an error like

error: covariant type A occurs in contravariant position in type B => Sum[A,C] of value f
  def flatMap[C](f: B => Sum[A, C]): Sum[A, C] =
                 ^
sealed trait Sum[+A, +B] {
  def flatMap[C](f: B => Sum[A, C]): Sum[A, C] =
    this match {
      case Failure(v) => Failure(v)
      case Success(v) => f(v)
    }
}
final case class Failure[A](value: A) extends Sum[A, Nothing]
final case class Success[B](value: B) extends Sum[Nothing, B]

What is going on here? Let’s momentarily switch to a simpler example that illustrates the problem.

case class Box[+A](value: A) {
  def set(a: A): Box[A] = Box(a)
}

which causes the error

error: covariant type A occurs in contravariant position in type A of value a
  def set(a: A): Box[A] = Box(a)
          ^

Remember that functions, and hence methods, which are just like functions, are contravariant in their input parameters. In this case we have specified that A is covariant but in set we have a parameter of type A and the type rules requires A to be contravariant here. This is what the compiler means by a “contravariant position”.

The solution is introduce a new type that is a supertype of A. We can do this with the notation [AA >: A] like so:

case class Box[+A](value: A) {
  def set[AA >: A](a: AA): Box[AA] = Box(a)
}

This successfully compiles.

Back to flatMap, the function f is a parameter, and thus in a contravariant position. This means we accept supertypes of f. It is declared with type B => Sum[A, C] and thus a supertype is covariant in B and contravariant in A and C. B is declared as covariant, so that is fine. C is invariant, so that is fine as well. A on the other hand is covariant but in a contravariant position. Thus we have to apply the same solution we did for Box above.

sealed trait Sum[+A, +B] {
  def flatMap[AA >: A, C](f: B => Sum[AA, C]): Sum[AA, C] =
    this match {
      case Failure(v) => Failure(v)
      case Success(v) => f(v)
    }
}
final case class Failure[A](value: A) extends Sum[A, Nothing]
final case class Success[B](value: B) extends Sum[Nothing, B]

Contravariant Position Pattern

If A of a covariant type T and a method f of A complains that T is used in a contravariant position, introduce a type TT >: T in f.

case class A[+T]() {
  def f[TT >: T](t: TT): A[TT] = ???
}

5.6.5 Type Bounds

We have seen some type bounds above, in the contravariant position pattern. Type bounds extend to specify subtypes as well as supertypes. The syntax is A <: Type to declare A must be a subtype of Type and A >: Type to declare a supertype.

For example, the following type allows us to store a Visitor or any subtype:

case class WebAnalytics[A <: Visitor](
  visitor: A,
  pageViews: Int,
  searchTerms: List[String],
  isOrganic: Boolean
)

5.6.6 Exercises

5.6.6.1 Covariance and Contravariance

Using the notation A <: B to indicate A is a subtype of B and assuming:

if I have a method

def groom(groomer: Cat => CatSound): CatSound = {
  val oswald = Cat("Black", "Cat food")
  groomer(oswald)
}

which of the following can I pass to groom?

The only function that will work is the the function of type Animal => Purr. The Siamese => Purr function will not work because the Oswald is a not a Siamese cat. The Animal => Sound function will not work because we require the return type to be a CatSound.

5.6.6.2 Calculator Again

We’re going to return to the interpreter example we saw at the end of the last chapter. This time we’re going to use the general abstractions we’ve created in this chapter, and our new knowledge of map, flatMap, and fold.

We’re going to represent calculations as Sum[String, Double], where the String is an error message. Extend Sum to have map and fold method.

sealed trait Sum[+A, +B] {
  def fold[C](error: A => C, success: B => C): C =
    this match {
      case Failure(v) => error(v)
      case Success(v) => success(v)
    }
  def map[C](f: B => C): Sum[A, C] =
    this match {
      case Failure(v) => Failure(v)
      case Success(v) => Success(f(v))
    }
  def flatMap[AA >: A, C](f: B => Sum[AA, C]): Sum[AA, C] =
    this match {
      case Failure(v) => Failure(v)
      case Success(v) => f(v)
    }
}
final case class Failure[A](value: A) extends Sum[A, Nothing]
final case class Success[B](value: B) extends Sum[Nothing, B]

Now we’re going to reimplement the calculator from last time. We have an abstract syntax tree defined via the following algebraic data type:

sealed trait Expression
final case class Addition(left: Expression, right: Expression) extends Expression
final case class Subtraction(left: Expression, right: Expression) extends Expression
final case class Division(left: Expression, right: Expression) extends Expression
final case class SquareRoot(value: Expression) extends Expression
final case class Number(value: Double) extends Expression

Now implement a method eval: Sum[String, Double] on Expression. Use flatMap and map on Sum and introduce any utility methods you see fit to make the code more compact. Here are some test cases:

assert(Addition(Number(1), Number(2)).eval == Success(3))
assert(SquareRoot(Number(-1)).eval == Failure("Square root of negative number"))
assert(Division(Number(4), Number(0)).eval == Failure("Division by zero"))
assert(Division(Addition(Subtraction(Number(8), Number(6)), Number(2)), Number(2)).eval == Success(2.0))

Here’s my solution. I used a helper method lift2 to “lift” a function into the result of two expressions. I hope you’ll agree the code is both more compact and easier to read than our previous solution!

sealed trait Expression {
  def eval: Sum[String, Double] =
    this match {
      case Addition(l, r) => lift2(l, r, (left, right) => Success(left + right))
      case Subtraction(l, r) => lift2(l, r, (left, right) => Success(left - right))
      case Division(l, r) => lift2(l, r, (left, right) =>
        if(right == 0)
          Failure("Division by zero")
        else
          Success(left / right)
      )
      case SquareRoot(v) =>
        v.eval flatMap { value =>
          if(value < 0)
            Failure("Square root of negative number")
          else
            Success(Math.sqrt(value))
        }
      case Number(v) => Success(v)
    }

  def lift2(l: Expression, r: Expression, f: (Double, Double) => Sum[String, Double]) =
    l.eval.flatMap { left =>
      r.eval.flatMap { right =>
        f(left, right)
      }
    }
}
final case class Addition(left: Expression, right: Expression) extends Expression
final case class Subtraction(left: Expression, right: Expression) extends Expression
final case class Division(left: Expression, right: Expression) extends Expression
final case class SquareRoot(value: Expression) extends Expression
final case class Number(value: Int) extends Expression

5.7 Conclusions

In this section we have explored generic types and functions, which allow us to abstract over types and methods respectively.

We have seen new patterns for generic algebraic types, and generic structural recursion. Using these building blocks we have seen some common patterns for working with generic types, namely fold, map, and flatMap.

In the next section we will explore these topics further by working with the collections classes in Scala.

6 Collections

We hardly need to state how important collection classes are. The Collections API was one of the most significant additions to Java, and Scala’s collections framework, completely revised and updated in 2.8, is an equally important addition to Scala.

In this section we’re going to look at three key datastructures in Scala’s collection library: sequences, options, and maps.

We will start with sequences. We begin with basic operations on sequences, and then briefly examine the distinction Scala makes between interface and implementation, and mutable and immutable sequences. We then explore in depth the methods Scala provides to transform sequences.

After covering the main collection types we turn to for comprehensions, a syntax that allows convenient specification of operations on collections.

With for comprehensions under our belt we will move onto options, which are used frequently in the APIs for sequences and maps. Options provide a means to sequence computations and are an essential companion to for comprehensions.

We’ll then look at monads, which we have introduced before, and see how they work with for comprehensions.

Next we will cover the other main collection classes: maps and sets. We will discover that they share a great deal in common with sequences, so most of our knowledge transfers directly.

We finish with discussion of ranges, which can represent large sequences of integers without storing every intermediate value in memory.

In the previous two chapters we have been focused on Scala concepts. The focus in this chapter is not on fundamental concepts, but on gaining practice with an important API and reinforcing concepts we have previously seen.

6.1 Sequences

A sequence is a collection of items with a defined and stable order. Sequences are one of the most common data structures. In this section we’re going to look at the basics of sequences: creating them, key methods on sequences, and the distinction between mutable and immutable sequences.

Here’s how you create a sequence in Scala:

val sequence = Seq(1, 2, 3)
// sequence: Seq[Int] = List(1, 2, 3)

This immediately shows off a key feature of Scala’s collections, the separation between interface and implementation. In the above, the value has type Seq[Int] but is implemented by a List.

6.1.1 Basic operations

Sequences implement many methods. Let’s look at some of the most common.

6.1.1.1 Accessing elements

We can access the elements of a sequence using its apply method, which accepts an Int index as a parameter. Indices start from 0.

sequence.apply(0)
// res0: Int = 1

sequence(0) // sugared syntax
// res1: Int = 1

An exception is raised if we use an index that is out of bounds:

sequence(3)
// java.lang.IndexOutOfBoundsException: 3
//        at ...

We can also access the head and tail of the sequence:

sequence.head
// res5: Int = 1

sequence.tail
// res6: Seq[Int] = List(2, 3)

sequence.tail.head
// res7: Int = 2

Again, trying to access an element that doesn’t exist throws an exception:

Seq().head
// java.util.NoSuchElementException: head of empty list
//   at scala.collection.immutable.Nil$.head(List.scala:337)
//   ...

Seq().tail
// java.lang.UnsupportedOperationException: tail of empty list
//   at scala.collection.immutable.Nil$.tail(List.scala:339)
//   ...

If we want to safely get the head without risking an exception, we can use headOption:

sequence.headOption
// res17: Option[Int] = Some(1)

Seq().headOption
// res18: Option[Nothing] = None

The Option class here is Scala’s built-in equivalent of our Maybe class from earlier. It has two subtypes—Some and None—representing the presence and absence of a value respectively.

6.1.2 Sequence length

Finding the length of a sequence is straightforward:

sequence.length
// res19: Int = 3

6.1.3 Searching for elements

There are a few ways of searching for elements. The contains method tells us whether a sequence contains an element (using == for comparison):

sequence.contains(2)
// res20: Boolean = true

The find method is like a generalised version of contains - we provide a test function and the sequence returns the first item for which the test returns true:

sequence.find(_ == 3)
// res21: Option[Int] = Some(3)

sequence.find(_ > 4)
// res22: Option[Int] = None

The filter method is a variant of find that returns all the matching elements in the sequence:

sequence.filter(_ > 1)
// res23: Seq[Int] = List(2, 3)

6.1.4 Sorting elements

We can use the sortWith method to sort a list using a binary function. The function takes two list items as parameters and returns true if they are in the correct order and false if they are the wrong way around. For example, to sort a list of Ints in descending order:

sequence.sortWith(_ > _)
// res24: Seq[Int] = List(3, 2, 1)

6.1.5 Appending/prepending elements

There are many ways to add elements to a sequence. We can append an element with the :+ method:

sequence.:+(4)
// res25: Seq[Int] = List(1, 2, 3, 4)

It is more idiomatic to call :+ as an infix operator:

sequence :+ 4
// res26: Seq[Int] = List(1, 2, 3, 4)

We can similarly prepend an element using the +: method:

sequence.+:(0)
// res27: Seq[Int] = List(0, 1, 2, 3)

Again, it is more idiomatic to call +: as an infix operator. Here the trailing colon makes it right associative, so we write the operator-style expression the other way around:

0 +: sequence
// res28: Seq[Int] = List(0, 1, 2, 3)

This is another of Scala’s general syntax rules—any method ending with a : character becomes right associative when written as an infix operator. This rule is designed to replicate Haskell-style operators for things like list prepend (::) and list concatenation (:::). We’ll look at this in more detail in a moment.

Finally we can concatenate entire sequences using the ++ method.

sequence ++ Seq(4, 5, 6)
// res29: Seq[Int] = List(1, 2, 3, 4, 5, 6)

6.1.6 Lists

The default implementation of Seq is a List, which is a classic linked list data structure similar to the one we developed in an earlier exercise. Some Scala libraries work specifically with Lists rather than using more generic types like Seq. For this reason we should familiarize ourselves with a couple of list-specific methods.

We can write an empty list using the singleton object Nil:

Nil
// res31: scala.collection.immutable.Nil.type = List()

Longer lists can be created by prepending elements in classic linked-list style using the :: method, which is equivalent to +::

val list = 1 :: 2 :: 3 :: Nil
// list: List[Int] = List(1, 2, 3)

4 :: 5 :: list
// res32: List[Int] = List(4, 5, 1, 2, 3)

We can also use the List.apply method for a more conventional constructor notation:

List(1, 2, 3)
// res33: List[Int] = List(1, 2, 3)

Finally, the ::: method is a right-associative List-specific version of ++:

List(1, 2, 3) ::: List(4, 5, 6)
// res34: List[Int] = List(1, 2, 3, 4, 5, 6)

:: and ::: are specific to lists whereas +:, :+ and ++ work on any type of sequence.

Lists have well known performance characteristics—constant-time in prepend, head and tail operations and linear-time in append, apply and update operations. Other immutable sequences are available in Scala with different performance characteristics to match all situations. So it is up to us as developers to decide whether we want to tie our code to a specific sequence type like List or prefer the sequence type Seq which is normally used to simplify swapping implementations.

6.1.7 Importing Collections and Other Libraries

The Seq and List types are so ubiquitous in Scala that they are made automatically available at all times. Other collections like Stack and Queue have to be brought into scope manually.

The main collections package is called scala.collection.immutable. We can import specific collections from this package as follows:

import scala.collection.immutable.Vector
Vector(1, 2, 3)
// res35: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)

We can also use wildcard imports to import everything in a package:

import scala.collection.immutable._
Queue(1, 2, 3)
// res36: scala.collection.immutable.Queue[Int] = Queue(1, 2, 3)

We can also use import to bring methods and fields into scope from a singleton:

import scala.collection.immutable.Vector.apply
apply(1, 2, 3)
// res37: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)

We can write import statements anywhere in our code—imported identifiers are lexically scoped to the block where we use them:

// `empty` is unbound here

def someMethod = {
  import scala.collection.immutable.Vector.empty

  // `empty` is bound to `Vector.empty` here
  empty[Int]
}

// `empty` is unbound here again

Import Statements

Import statements in Scala are very flexible. The main points are nicely described in the Scala Wikibook.

6.1.8 Take Home Points

Seq is Scala’s general sequence datatype. It has a number of general subtypes such as List, Stack, Vector, Queue, and Array, and specific subtypes such as String.

The default sequences in Scala are immutable. We also have access to mutable sequences, which are covered separately in the Collections Redux chapter.

We have covered a variety of methods that operate on sequences. Here is a type table of everything we have seen so far:

Method We have We provide We get

Seq(...)

[A], …

Seq[A]

apply

Seq[A]

Int

A

head

Seq[A]

A

tail

Seq[A]

Seq[A]

length

Seq[A]

Int

contains

Seq[A]

A

Boolean

find

Seq[A]

A => Boolean

Option[A]

filter

Seq[A]

A => Boolean

Seq[A]

sortWith

Seq[A]

(A, A) => Boolean

Seq[A]

:+, +:

Seq[A]

A

Seq[A]

++

Seq[A]

Seq[A]

Seq[A]

::

List[A]

A

List[A]

:::

List[A]

List[A]

List[A]

We can always use Seq and List in our code. Other collections can be brought into scope using the import statement as we have seen.

6.1.9 Exercises

6.1.9.1 Documentation

Discovering Scala’s collection classes is all about knowing how to read the API documentation. Look up the Seq and List types now and answer the following:

Tip: There is a link to the Scala API documentation at http://scala-lang.org.

The synonym for length is size.

The methods for retrieving the first element in a list are: - head —returns A, throwing an exception if the list is empty - headOption—returns Option[A], returning None if the list is empty

The mkString method allows us to quickly display a Seq as a String:

Seq(1, 2, 3).mkString(",")               // returns "1,2,3"
Seq(1, 2, 3).mkString("[ ", ", ", " ]") // returns "[ 1, 2, 3 ]"

Options contain two methods, isDefined and isEmpty, that we can use as a quick test:

Some(123).isDefined // returns true
Some(123).isEmpty   // returns false
None.isDefined      // returns false
None.isEmpty        // returns true

6.1.9.2 Animals

Create a Seq containing the Strings "cat", "dog", and "penguin". Bind it to the name animals.

val animals = Seq("cat", "dog", "penguin")
// animals: scala.collection.immutable.Seq[String] = List(cat, dog, penguin)

Append the element "tyrannosaurus" to animals and prepend the element "mouse".

"mouse" +: animals :+ "tyrannosaurus"
// res48: scala.collection.immutable.Seq[String] = List(mouse, cat, dog, penguin, tyrannosaurus)

What happens if you prepend the Int 2 to animals? Why? Try it out… were you correct?

The returned sequence has type Seq[Any]. It is perfectly valid to return a supertype (in this case Seq[Any]) from a non-destructive operation.

2 +: animals

You might expect a type error here, but Scala is capable of determining the least upper bound of String and Int and setting the type of the returned sequence accordingly.

In most real code appending an Int to a Seq[String] would be an error. In practice, the type annotations we place on methods and fields protect against this kind of type error, but be aware of this behaviour just in case.

6.1.9.3 Intranet Movie Database

Let’s revisit our films and directors example from the Classes chapter.

The code below is a partial rewrite of the previous sample code in which Films is stored as a field of Director instead of the other way around. Copy and paste this into a new Scala worksheet and continue with the exercises below:

case class Film(
  name: String,
  yearOfRelease: Int,
  imdbRating: Double)

case class Director(
  firstName: String,
  lastName: String,
  yearOfBirth: Int,
  films: Seq[Film])

val memento           = new Film("Memento", 2000, 8.5)
val darkKnight        = new Film("Dark Knight", 2008, 9.0)
val inception         = new Film("Inception", 2010, 8.8)

val highPlainsDrifter = new Film("High Plains Drifter", 1973, 7.7)
val outlawJoseyWales  = new Film("The Outlaw Josey Wales", 1976, 7.9)
val unforgiven        = new Film("Unforgiven", 1992, 8.3)
val granTorino        = new Film("Gran Torino", 2008, 8.2)
val invictus          = new Film("Invictus", 2009, 7.4)

val predator          = new Film("Predator", 1987, 7.9)
val dieHard           = new Film("Die Hard", 1988, 8.3)
val huntForRedOctober = new Film("The Hunt for Red October", 1990, 7.6)
val thomasCrownAffair = new Film("The Thomas Crown Affair", 1999, 6.8)

val eastwood = new Director("Clint", "Eastwood", 1930,
  Seq(highPlainsDrifter, outlawJoseyWales, unforgiven, granTorino, invictus))

val mcTiernan = new Director("John", "McTiernan", 1951,
  Seq(predator, dieHard, huntForRedOctober, thomasCrownAffair))

val nolan = new Director("Christopher", "Nolan", 1970,
  Seq(memento, darkKnight, inception))

val someGuy = new Director("Just", "Some Guy", 1990,
  Seq())

val directors = Seq(eastwood, mcTiernan, nolan, someGuy)

// TODO: Write your code here!

Using this sample code, write implementations of the following methods:

We use `filter` because we are expecting more than one result:
def directorsWithBackCatalogOfSize(numberOfFilms: Int): Seq[Director] =
 directors.filter(_.films.length > numberOfFilms)

We use find because we are expecting at most one result. This solution will return the first director found who matches the criteria of the search:

def directorBornBefore(year: Int): Option[Director] =
 directors.find(_.yearOfBirth < year)
The Option type is discussed in more detail later this chapter.

This solution performs each part of the query separately and uses filter and contains to calculate the intersection of the results:

def directorBornBeforeWithBackCatalogOfSize(year: Int, numberOfFilms: Int): Seq[Director] = {
 val byAge   = directors.filter(_.yearOfBirth < year)
 val byFilms = directors.filter(_.films.length > numberOfFilms)
 byAge.filter(byFilms.contains)
}

Here is one solution. Note that sorting by ascending age is the same as sorting by descending year of birth:

def directorsSortedByAge(ascending: Boolean = true) =
  if(ascending) {
    directors.sortWith((a, b) => a.yearOfBirth < b.yearOfBirth)
  } else {
    directors.sortWith((a, b) => a.yearOfBirth > b.yearOfBirth)
  }

Because Scala is a functional language, we can also factor our code as follows:

def directorsSortedByAge(ascending: Boolean = true) = {
  val comparator: (Director, Director) => Boolean =
    if(ascending) {
      (a, b) => a.yearOfBirth < b.yearOfBirth
    } else {
      (a, b) => a.yearOfBirth > b.yearOfBirth
    }

  directors.sortWith(comparator)
}

Here is a final refactoring that is slightly less efficient because it rechecks the value of ascending multiple times.

def directorsSortedByAge(ascending: Boolean = true) =
  directors.sortWith { (a, b) =>
    if(ascending) {
      a.yearOfBirth < b.yearOfBirth
    } else {
      a.yearOfBirth > b.yearOfBirth
    }
  }
Note the use of braces instead of parentheses on the call to sortWith in the last example. We can use this syntax on any method call of one argument to give it a control-structure-like look and feel.

6.2 Working with Sequences

In the previous section we looked at the basic operations on sequences. Now we’re going to look at practical aspects of working with sequences—how functional programming allows us to process sequences in a terse and declarative style.

6.2.1 Bulk Processing of Elements

When working with sequences we often want to deal with the collection as a whole, rather than accessing and manipulating individual elements. Scala gives us a number of powerful options that allow us to solve many problems more directly.

6.2.2 Map

Let’s start with something simple—suppose we want to double every element of a sequence. You might wish to express this as a loop. However, this requires writing several lines of looping machinery for only one line of actual doubling functionality.

In Scala we can use the map method defined on any sequence. Map takes a function and applies it to every element, creating a sequence of the results. To double every element we can write:

val sequence = Seq(1, 2, 3)
// sequence: Seq[Int] = List(1, 2, 3)

sequence.map(elt => elt * 2)
// res0: Seq[Int] = List(2, 4, 6)

If we use placeholder syntax we can write this even more compactly:

sequence.map(_ * 2)
// res1: Seq[Int] = List(2, 4, 6)

Given a sequence with type Seq[A], the function we pass to map must have type A => B and we get a Seq[B] as a result. This isn’t right for every situation. For example, suppose we have a sequence of strings, and we want to generate a sequence of all the permutations of those strings. We can call the permutations method on a string to get all permutations of it:

"dog".permutations
// res2: Iterator[String] = non-empty iterator

This returns an Iterable, which is a bit like a Java Iterator. We’re going to look at iterables in more detail later. For now all we need to know is that we can call the toList method to convert an Iterable to a List.

"dog".permutations.toList
// res3: List[String] = List(dog, dgo, odg, ogd, gdo, god)

Thus we could write

Seq("a", "wet", "dog").map(_.permutations.toList)
// res4: Seq[List[String]] = List(List(a), List(wet, wte, ewt, etw, twe, tew), List(dog, dgo, odg, ogd, gdo, god))

but we end up with a sequence of sequences. Let’s look at the types in more detail to see what’s gone wrong:

Method We have We provide We get

map

Seq[A]

A => B

Seq[B]

map

Seq[String]

String => List[String]

Seq[List[String]]

???

Seq[A]

A => Seq[B]

Seq[B]

What is the method ??? that we can use to collect a single flat sequence?

6.2.3 FlatMap

Our mystery method above is called flatMap. If we simply replace map with flatMap we get the answer we want:

Seq("a", "wet", "dog").flatMap(_.permutations.toList)
// res5: Seq[String] = List(a, wet, wte, ewt, etw, twe, tew, dog, dgo, odg, ogd, gdo, god)

flatMap is similar to map except that it expects our function to return a sequence. The sequences for each input element are appended together. For example:

Seq(1, 2, 3).flatMap(num => Seq(num, num * 10))
// res6: Seq[Int] = List(1, 10, 2, 20, 3, 30)

The end result is (nearly) always the same type as the original sequence: aList.flatMap(...) returns another List, aVector.flatMap(...) returns another Vector, and so on:

import scala.collection.immutable.Vector
Vector(1, 2, 3).flatMap(num => Seq(num, num * 10))
// res7: scala.collection.immutable.Vector[Int] = Vector(1, 10, 2, 20, 3, 30)

6.2.4 Folds

Now let’s look at another kind of operation. Say we have a Seq[Int] and we want to add all the numbers together. map and flatMap don’t apply here for two reasons:

  1. they expect a unary function, whereas + is a binary operation;
  2. they both return sequences of items, whereas we want to return a single Int.

There are also two further wrinkles to consider.

  1. What result do we expect if the sequence is empty? If we’re adding items together then 0 seems like a natural result, but what is the answer in general?
  2. Although + is commutative (i.e. a+b == b+a), in general we may need to specify an order in which to pass arguments to our binary function.

Let’s make another type table to see what we’re looking for:

Method We have We provide We get

???

Seq[Int]

0 and (Int, Int) => Int

Int

The methods that fit the bill are called folds, with two common cases foldLeft and foldRight corresponding to the order the fold is applied. The job of these methods is to traverse a sequence and accumulate a result. The types are as follows:

Method We have We provide We get

foldLeft

Seq[A]

B and (B, A) => B

B

foldRight

Seq[A]

B and (A, B) => B

B

Given the sequence Seq(1, 2, 3), 0, and + the methods calculate the following:

Method Operations Notes

Seq(1, 2, 3).foldLeft(0)(_ + _)

(((0 + 1) + 2) + 3)

Evaluation starts on the left

Seq(1, 2, 3).foldRight(0)(_ + _)

(1 + (2 + (3 + 0)))

Evaluation starts on the right

As we know from studying algebraic data types, the fold methods are very flexible. We can write any transformation on a sequence in terms of fold.

6.2.5 Foreach

There is one more traversal method that is commonly used: foreach. Unlike map, flatMap and the folds, foreach does not return a useful result—we use it purely for its side-effects. The type table is:

Method We have We provide We get

foreach

Seq[A]

A => Unit

Unit

A common example of using foreach is printing the elements of a sequence:

List(1, 2, 3).foreach(num => println("And a " + num + "..."))
// And a 1...
// And a 2...
// And a 3...

6.2.6 Algebra of Transformations

We’ve seen the four major traversal functions, map, flatMap, fold, and foreach. It can be difficult to know which to use, but it turns out there is a simple way to decide: look at the types! The type table below gives the types for all the operations we’ve seen so far. To use it, start with the data you have (always a Seq[A] in the table below) and then look at the functions you have available and the result you want to obtain. The final column will tell you which method to use.

We have We provide We want Method

Seq[A]

A => Unit

Unit

foreach

Seq[A]

A => B

Seq[B]

map

Seq[A]

A => Seq[B]

Seq[B]

flatMap

Seq[A]

B and (B, A) => B

B

foldLeft

Seq[A]

B and (A, B) => B

B

foldRight

This type of analysis may seem foreign at first, but you will quickly get used to it. Your two steps in solving any problem with sequences should be: think about the types, and experiment on the REPL!

6.2.7 Exercises

The goals of this exercise are for you to learn your way around the collections API, but more importantly to learn to use types to drive implementation. When approaching each exercise you should answer:

  1. What is the type of the data we have available?
  2. What is the type of the result we want?
  3. What is the type of the operations we will use?

When you have answered these questions look at the type table above to find the correct method to use. Done in this way the actual programming should be straightforward.

6.2.7.1 Heroes of the Silver Screen

These exercises re-use the example code from the Intranet Movie Database exercise from the previous section:

Nolan Films

Starting with the definition of nolan, create a list containing the names of the films directed by Christopher Nolan.

nolan.films.map(_.name)

Cinephile

Starting with the definition of directors, create a list containing the names of all films by all directors.

directors.flatMap(director => director.films.map(film => film.name))

Vintage McTiernan

Starting with mcTiernan, find the date of the earliest McTiernan film.

Tip: you can concisely find the minimum of two numbers a and b using math.min(a, b).

There are a number of ways to do this. We can sort the list of films and then retrieve the smallest element.

mcTiernan.films.sortWith { (a, b) =>
  a.yearOfRelease < b.yearOfRelease
}.headOption

We can also do this by using a fold.

mcTiernan.films.foldLeft(Int.MaxValue) { (current, film) =>
  math.min(current, film.yearOfRelease)
}

A quick aside:

There’s a far simpler solution to this problem using a convenient method on sequences called min. This method finds the smallest item in a list of naturally comparable elements. We don’t even need to sort them:

mcTiernan.films.map(_.yearOfRelease).min

We didn’t introduce min in this section because our focus is on working with general-purpose methods like map and flatMap. However, you may come across min in the documentation for the Scala standard library, and you may wonder how it is implemented.

Not all data types have a natural sort order. We might naturally wonder how min would work on a list of values of an unsortable data type. A quick experiment shows that the call doesn’t even compile:

mcTiernan.films.min
// <console>:19: error: No implicit Ordering defined for Film.
//        mcTiernan.films.min
//                        ^

The min method is a strange beast—it only compiles when it is called on a list of sortable values. This is an example of something called the type class pattern. We don’t know enough Scala to implement type classes yet—we’ll learn all about how they work in Chapter [@sec:type-classes].

High Score Table

Starting with directors, find all films sorted by descending IMDB rating:

directors.
  flatMap(director => director.films).
  sortWith((a, b) => a.imdbRating > b.imdbRating)

Starting with directors again, find the average score across all films:

We cache the list of films in a variable because we use it twice—once to calculate the sum of the ratings and once to fetch the number of films:

val films = directors.flatMap(director => director.films)

films.foldLeft(0.0)((sum, film) => sum + film.imdbRating) / films.length

Tonight’s Listings

Starting with directors, print the following for every film: "Tonight only! FILM NAME by DIRECTOR!"

Println is used for its side-effects so we don’t need to accumulate a result—we use println as a simple iterator:

directors.foreach { director =>
  director.films.foreach { film =>
    println(s"Tonight! ${film.name} by ${director.firstName} ${director.lastName}!")
  }
}

From the Archives

Finally, starting with directors again, find the earliest film by any director:

Here’s the solution written using sortWith:

directors.
  flatMap(director => director.films).
  sortWith((a, b) => a.yearOfRelease < b.yearOfRelease).
  headOption

We have to be careful in this solution to handle situations where there are no films. We can’t use the head method, or even the min method we saw in the solution to Vintage McTiernan, because these methods throw exceptions if the sequence is empty:

someBody.films.map(_.yearOfRelease).min
// java.lang.UnsupportedOperationException: empty.min
//   at scala.collection.TraversableOnce$class.min(TraversableOnce.scala:222)
//   at scala.collection.AbstractTraversable.min(Traversable.scala:104)
//   ... 1022 elided

6.2.7.2 Do-It-Yourself

Now we know the essential methods of Seq, we can write our own versions of some other library methods.

Minimum

Write a method to find the smallest element of a Seq[Int].

This is another fold. We have a Seq[Int], the minimum operation is (Int, Int) => Int, and we want an Int. The challenge is to find the zero value.

What is the identity for min so that min(x, identity) = x. It is positive infinity, which in Scala we can write as Int.MaxValue (see, fixed width numbers do have benefits).

Thus the solution is:

def smallest(seq: Seq[Int]): Int =
  seq.foldLeft(Int.MaxValue)(math.min)

Unique

Given Seq(1, 1, 2, 4, 3, 4) create the sequence containing each number only once. Order is not important, so Seq(1, 2, 4, 3) or Seq(4, 3, 2, 1) are equally valid answers. Hint: Use contains to check if a sequence contains a value.

Once again we follow the same pattern. The types are:

  1. We have a Seq[Int]
  2. We want a Seq[Int]
  3. Constructing the operation we want to use requires a bit more thought. The hint is to use contains. We can keep a sequence of the unique elements we’ve seen so far, and use contains to test if the sequence contains the current element. If we have seen the element we don’t add it, otherwise we do. In code
def insert(seq: Seq[Int], elt: Int): Seq[Int] = {
 if(seq.contains(elt))
   seq
 else
   elt +: seq
}

With these three pieces we can solve the problem. Looking at the type table we see we want a fold. Once again we must find the identity element. In this case the empty sequence is what we want. Why so? Think about what the answer should be if we try to find the unique elements of the empty sequence.

Thus the solution is

def insert(seq: Seq[Int], elt: Int): Seq[Int] = {
  if(seq.contains(elt))
    seq
  else
    elt +: seq
}

def unique(seq: Seq[Int]): Seq[Int] = {
  seq.foldLeft(Seq.empty[Int]){ insert _ }
}

unique(Seq(1, 1, 2, 4, 3, 4))

Note how I created the empty sequence. I could have written Seq[Int]() but in both cases I need to supply a type (Int) to help the type inference along.

Reverse

Write a function that reverses the elements of a sequence. Your output does not have to use the same concrete implementation as the input. Hint: use foldLeft.

In this exercise, and the ones that follow, using the types are particularly important. Start by writing down the type of reverse.

def reverse[A, B](seq: Seq[A], f: A => B): Seq[B] = {
  ???
}

The hint says to use foldLeft, so let’s go ahead and fill in the body as far as we can.

def reverse[A](seq: Seq[A]): Seq[A] = {
  seq.foldLeft(???){ ??? }
}

We need to work out the function to provide to foldLeft and the zero or identity element. For the function, the type of foldLeft is required to be of type (Seq[A], A) => Seq[A]. If we flip the types around the +: method on Seq has the right types.

For the zero element we know that it must have the same type as the return type of reverse (because the result of the fold is the result of reverse). Thus it’s a Seq[A]. Which sequence? There are a few ways to answer this:

  • The only Seq[A] we can create in this method, before we know what A is, is the empty sequence Seq.empty[A].
  • The identity element is one such that x +: zero = Seq(x). Again this must be the empty sequence.

So we now we can fill in the answer.

def reverse[A](seq: Seq[A]): Seq[A] = {
  seq.foldLeft(Seq.empty[A]){ (seq, elt) => elt +: seq }
}

Map

Write map in terms of foldRight.

Follow the same process as before: write out the type of the method we need to create, and fill in what we know. We start with map and foldRight.

def map[A, B](seq: Seq[A], f: A => B): Seq[B] = {
  seq.foldRight(???){ ??? }
}

As usual we need to fill in the zero element and the function. The zero element must have type Seq[B], and the function has type (A, Seq[B]) => Seq[B]). The zero element is straightforward: Seq.empty[B] is the only sequence we can construct of type Seq[B]. For the function, we clearly have to convert that A to a B somehow. There is only one way to do that, which is with the function supplied to map. We then need to add that B to our Seq[B], for which we can use the +: method. This gives us our final result.

def map[A, B](seq: Seq[A], f: A => B): Seq[B] = {
  seq.foldRight(Seq.empty[B]){ (elt, seq) => f(elt) +: seq }
}

Fold Left

Write your own implementation of foldLeft that uses foreach and mutable state. Remember you can create a mutable variable using the var keyword, and assign a new value using =. For example

var mutable = 1
// mutable: Int = 1

mutable = 2
// mutable: Int = 2

Once again, write out the skeleton and then fill in the details using the types. We start with

def foldLeft[A, B](seq: Seq[A], zero: B, f: (B, A) => B): B = {
  seq.foreach { ??? }
}

Let’s look at what we have need to fill in. foreach returns Unit but we need to return a B. foreach takes a function of type A => Unit but we only have a (B, A) => B available. The A can come from foreach and by now we know that the B is the intermediate result. We have the hint to use mutable state and we know that we need to keep a B around and return it, so let’s fill that in.

def foldLeft[A, B](seq: Seq[A], zero: B, f: (B, A) => B): B = {
  var result: B = ???
  seq.foreach { (elt: A) => ??? }
  result
}

At this point we can just follow the types. result must be initially assigned to the value of zero as that is the only B we have. The body of the function we pass to foreach must call f with result and elt. This returns a B which we must store somewhere—the only place we have to store it is in result. So the final answer becomes

def foldLeft[A, B](seq: Seq[A], zero: B, f: (B, A) => B): B = {
  var result = zero
  seq.foreach { elt => result = f(result, elt) }
  result
}

There are many other methods on sequences. Consult the API documentation for the Seq trait for more information.

6.3 For Comprehensions

We’ve discussed the main collection transformation functions—map, flatMap, foldLeft, foldRight, and foreach—and seen that they provide a powerful way of working with collections. They can become unwieldy to work with when dealing with many collections or many nested transformations. Fortunately Scala has special syntax for working with collections (in fact any class that implements map and flatMap) that makes complicated operations simpler to write. This syntax is known as a for comprehension.

Not Your Father’s For Loops

for comprehensions in Scala are very different to the C-style for loops in Java. There is no direct equivalent of either language’s syntax in the other.

Let’s start with a simple example. Say we have the sequence Seq(1, 2, 3) and we wish to create a sequence with every element doubled. We know we can write

Seq(1, 2, 3).map(_ * 2)
// res0: Seq[Int] = List(2, 4, 6)

The equivalent program written with a for comprehension is:

for {
  x <- Seq(1, 2, 3)
} yield x * 2
// res1: Seq[Int] = List(2, 4, 6)

We call the expression containing the <- a generator, with a pattern on the left hand side and a generator expression on the right. A for comprehension iterates over the elements in the generator, binding each element to the pattern and calling the yield expression. It combines the yielded results into a sequence of the same type as the original generator.

In simple examples like this one we don’t really see the power of for comprehensions—direct use of map and flatMap are often more compact in the simplest case. Let’s try a more complicated example. Say we want to double all the numbers in Seq(Seq(1), Seq(2, 3), Seq(4, 5, 6)) and return a flattened sequence of the results. To do this with map and flatMap we must nest calls:

val data = Seq(Seq(1), Seq(2, 3), Seq(4, 5, 6))
// data: Seq[Seq[Int]] = List(List(1), List(2, 3), List(4, 5, 6))

data.flatMap(_.map(_ * 2))
// res2: Seq[Int] = List(2, 4, 6, 8, 10, 12)

This is getting complicated. The equivalent for comprehension is much more … comprehensible.

for {
  subseq  <- data
  element <- subseq
} yield element * 2
// res3: Seq[Int] = List(2, 4, 6, 8, 10, 12)

This gives us an idea of what the for comprehensions does. A general for comprehension:

for {
  x <- a
  y <- b
  z <- c
} yield e

translates to:

a.flatMap(x => b.flatMap(y => c.map(z => e)))

The intuitive understanding of the code is to iterate through all of the sequences in the generators, mapping the yield expression over every element therein, and accumulating a result of the same type as sequence fed into the first generator.

Note that if we omit the yield keyword before the final expression, the overall type of the for comprehension becomes Unit. This version of the for comprehension is executed purely for its side-effects, and any result is ignored. Revisiting the doubling example from earlier, we can print the results instead of returning them:

for {
  seq <- Seq(Seq(1), Seq(2, 3))
  elt <- seq
} println(elt * 2) // Note: no 'yield' keyword
// 2
// 4
// 6

The equivalent method calls use flatMap as usual and foreach in place of the final map:

a.flatMap(x => b.flatMap(y => c.foreach(z => e)))

We can use parentheses instead of braces to delimit the generators in a for loop. However, we must use semicolons to separate the generators if we do. Thus:

for (
  x <- a;
  y <- b;
  z <- c
) yield e

is equivalent to:

for {
  x <- a
  y <- b
  z <- c
} yield e

Some developers prefer to use parentheses when there is only one generator and braces otherwise:

for(x <- Seq(1, 2, 3)) yield {
  x * 2
}

We can also use braces to wrap the yield expression and convert it to a block as usual:

for {
  // ...
} yield {
  // ...
}

6.3.1 Exercises

(More) Heroes of the Silver Screen

Repeat the following exercises from the previous section without using map or flatMap:

Nolan Films

List the names of the films directed by Christopher Nolan.

for {
  film <- nolan.films
} yield film.name

Cinephile

List the names of all films by all directors.

for {
  director <- directors
  film     <- director.films
} yield film.name

High Score Table

Find all films sorted by descending IMDB rating:

This one’s a little trickier. We have to calculate the complete list of films first before sorting them with sortWith. Precedence rules require us to wrap the whole for / yield expression in parentheses to achieve this in one expression:

(for {
  director <- directors
  film     <- director.films
} yield film).sortWith((a, b) => a.imdbRating > b.imdbRating)

Many developers prefer to use a temporary variable to make this code tidier:

val films = for {
  director <- directors
  film     <- director.films
} yield film

films sortWith { (a, b) =>
  a.imdbRating > b.imdbRating
}

Tonight’s Listings

Print the following for every film: "Tonight only! FILM NAME by DIRECTOR!"

We can drop the yield keyword from the for expression to achieve foreach-like semantics:

for {
  director <- directors
  film     <- director.films
} println(s"Tonight! ${film.name} by ${director.name}!")

6.4 Options

We have seen Options in passing a number of times already—they represent values that may or may not be present in our code. Options are an alternative to using null that provide us with a means of chaining computations together without risking NullPointerExceptions. We have previously produced code in the spirit of Option with our DivisionResult and Maybe types in previous chapters.

Let’s look into Scala’s built-in Option type in more detail.

6.4.1 Option, Some, and None

Option is a generic sealed trait with two subtypes—Some and None. Here is an abbreviated version of the code—we will fill in more methods as we go on:

sealed trait Option[+A] {
  def getOrElse[B >: A](default: B): B

  def isEmpty: Boolean
  def isDefined: Boolean = !isEmpty

  // other methods...
}

final case class Some[A](x: A) extends Option[A] {
  def getOrElse[B >: A](default: B) = x

  def isEmpty: Boolean = false

  // other methods...
}

case object None extends Option[Nothing] {
  def getOrElse[B >: Nothing](default: B) = default

  def isEmpty: Boolean = true

  // other methods...
}

Here is a typical example of code for generating an option—reading an integer from the user:

def readInt(str: String): Option[Int] =
  if(str matches "-?\\d+") Some(str.toInt) else None

The toInt method of String throws a NumberFormatException if the string isn’t a valid series of digits, so we guard its use with a regular expression. If the number is correctly formatted we return Some of the Int result. Otherwise we return None. Example usage:

readInt("123")
// res2: Option[Int] = Some(123)

readInt("abc")
// res3: Option[Int] = None

6.4.2 Extracting Values from Options

There are several ways to safely extract the value in an option without the risk of throwing any exceptions.

Alternative 1: the getOrElse method—useful if we want to fall back to a default value:

readInt("abc").getOrElse(0)
// res4: Int = 0

Alternative 2: pattern matchingSome and None both have associated patterns that we can use in a match expression:

readInt("123") match {
  case Some(number) => number + 1
  case None         => 0
}
// res5: Int = 124

Alternative 3: map and flatMapOption supports both of these methods, enabling us to chain off of the value within producing a new Option. This bears a more thorough explanation—let’s look at it in a little more detail.

6.4.3 Options as Sequences

One way of thinking about an Option is as a sequence of 0 or 1 elements. In fact, Option supports many of the sequence operations we have seen so far:

sealed trait Option[+A] {
  def getOrElse[B >: A](default: B): B

  def isEmpty: Boolean
  def isDefined: Boolean = !isEmpty

  def filter(func: A => Boolean): Option[A]
  def find(func: A => Boolean): Option[A]

  def map[B](func: A => B): Option[B]
  def flatMap[B](func: A => Option[B]): Option[B]
  def foreach(func: A => Unit): Unit

  def foldLeft[B](initial: B)(func: (B, A) => B): B
  def foldRight[B](initial: B)(func: (A, B) => B): B
}

Because of the limited size of 0 or 1, there is a bit of redundancy here: filter and find effectively do the same thing, and foldLeft and foldRight only differ in the order of their arguments. However, these methods give us a lot flexibility for manipulating optional values. For example, we can use map and flatMap to define optional versions of common operations:

def sum(optionA: Option[Int], optionB: Option[Int]): Option[Int] =
  optionA.flatMap(a => optionB.map(b => a + b))
sum(readInt("1"), readInt("2"))
// res2: Option[Int] = Some(3)

sum(readInt("1"), readInt("b"))
// res3: Option[Int] = None

sum(readInt("a"), readInt("2"))
// res4: Option[Int] = None

The implementation of sum looks complicated at first, so let’s break it down:

Although map and flatMap don’t allow us to extract values from our Options, they allow us to compose computations together in a safe manner. If all arguments to the computation are Some, the result is a Some. If any of the arguments are None, the result is None.

We can use map and flatMap in combination with pattern matching or getOrElse to combine several Options and yield a single non-optional result:

sum(readInt("1"), readInt("b")).getOrElse(0)
// res5: Int = 0

It’s worth noting that Option and Seq are also compatible in some sense. We can turn a Seq[Option[A]] into a Seq[A] using flatMap:

Seq(readInt("1"), readInt("b"), readInt("3")).flatMap(x => x)
// res6: Seq[Int] = List(1, 3)

6.5 Options as Flow Control

Because Option supports map and flatMap, it also works with for comprehensions. This gives us a nice syntax for combining values without resorting to building custom methods like sum to keep our code clean:

val optionA = readInt("123")
val optionB = readInt("234")

for {
  a <- optionA
  b <- optionB
} yield a + b

In this code snippet a and b are both Ints—we can add them together directly using + in the yield block.

Let’s stop to think about this block of code for a moment. There are three ways of looking at it:

  1. We can expand the block into calls to map and flatMap. You will be unsurprised to see that the resulting code is identical to our implementation of sum above:

    scala>     optionA.flatMap(a => optionB.map(b => a + b))
    res9: Option[Int] = Some(357)
  2. We can think of optionA and optionB as sequences of zero or one elements, in which case the result is going to be a flattened sequence of length optionA.size * optionB.size. If either optionA or optionB is None then the result is of length 0.

  3. We can think of each clause in the for comprehension as an expression that says: if this clause results in a Some, extract the value and continue… if it results in a None, exit the for comprehension and return None.

Once we get past the initial foreignness of using for comprehensions to “iterate through” options, we find a useful control structure that frees us from excessive use of map and flatMap.

6.5.1 Exercises

6.5.1.1 Adding Things

Write a method addOptions that accepts two parameters of type Option[Int] and adds them together. Use a for comprehension to structure your code.

We can reuse code from the text above for this:

def addOptions(opt1: Option[Int], opt2: Option[Int]) =
  for {
    a <- opt1
    b <- opt2
  } yield a + b

Write a second version of your code using map and flatMap instead of a for comprehension.

The pattern is to use flatMap for all clauses except the innermost, which becomes a map:

def addOptions2(opt1: Option[Int], opt2: Option[Int]) =
  opt1 flatMap { a =>
    opt2 map { b =>
      a + b
    }
  }

6.5.1.2 Adding All of the Things

Overload addOptions with another implementation that accepts three Option[Int] parameters and adds them all together.

For comprehensions can have as many clauses as we want so all we need to do is add an extra line to the previous solution:

def addOptions(opt1: Option[Int], opt2: Option[Int], opt3: Option[Int]) =
  for {
    a <- opt1
    b <- opt2
    c <- opt3
  } yield a + b + c

Write a second version of your code using map and flatMap instead of a for comprehension.

Here we can start to see the simplicity of for comprehensions:

def addOptions2(opt1: Option[Int], opt2: Option[Int], opt3: Option[Int]) =
  opt1 flatMap { a =>
    opt2 flatMap { b =>
      opt3 map { c =>
        a + b + c
      }
    }
  }

6.5.1.3 A(nother) Short Division Exercise

Write a method divide that accepts two Int parameters and divides one by the other. Use Option to avoid exceptions when the denominator is 0.

We saw this code in the Traits chapter when we wrote the DivisionResult class. The implementation is much simpler now we can use Option to do the heavy lifting:

def divide(numerator: Int, denominator: Int) =
  if(denominator == 0) None else Some(numerator / denominator)

Using your divide method and a for comprehension, write a method called divideOptions that accepts two parameters of type Option[Int] and divides one by the other:

In this example the divide operation returns an Option[Int] instead of an Int. In order to process the result we need to move the calculation from the yield block to a for-clause:

def divideOptions(numerator: Option[Int], denominator: Option[Int]) =
  for {
    a <- numerator
    b <- denominator
    c <- divide(a, b)
  } yield c

6.5.1.4 A Simple Calculator

A final, longer exercise. Write a method called calculator that accepts three string parameters:

def calculator(operand1: String, operator: String, operand2: String): Unit = ???

and behaves as follows:

  1. Convert the operands to Ints;

  2. Perform the desired mathematical operator on the two operands:

    • provide support for at least four operations: +, -, * and /;
    • use Option to guard against errors (invalid inputs or division by zero).
  3. Finally print the result or a generic error message.

Tip: Start by supporting just one operator before extending your method to other cases.

The trick to this one is realising that each clause in the for comprehension can contain an entire block of Scala code:

def calculator(operand1: String, operator: String, operand2: String): Unit = {
  val result = for {
    a   <- readInt(operand1)
    b   <- readInt(operand2)
    ans <- operator match {
             case "+" => Some(a + b)
             case "-" => Some(a - b)
             case "*" => Some(a * b)
             case "/" => divide(a, b)
             case _   => None
           }
  } yield ans

  result match {
    case Some(number) => println(s"The answer is $number!")
    case None         => println(s"Error calculating $operand1 $operator $operand2")
  }
}

Another approach involves factoring the calculation part out into its own private function:

def calculator(operand1: String, operator: String, operand2: String): Unit = {
  def calcInternal(a: Int, b: Int) =
    operator match {
      case "+" => Some(a + b)
      case "-" => Some(a - b)
      case "*" => Some(a * b)
      case "/" => divide(a, b)
      case _   => None
    }

  val result = for {
    a   <- readInt(operand1)
    b   <- readInt(operand2)
    ans <- calcInternal(a, b)
  } yield ans

  result match {
    case Some(number) => println(s"The answer is $number!")
    case None         => println(s"Error calculating $operand1 $operator $operand2")
  }
}

For the enthusiastic only, write a second version of your code using flatMap and map.

This version of the code is much clearer if we factor out the calculation part into its own function. Without this it would be very hard to read:

def calculator(operand1: String, operator: String, operand2: String): Unit = {
  def calcInternal(a: Int, b: Int) =
    operator match {
      case "+" => Some(a + b)
      case "-" => Some(a - b)
      case "*" => Some(a * b)
      case "/" => divide(a, b)
      case _   => None
    }

  val result =
    readInt(operand1) flatMap { a =>
      readInt(operand2) flatMap { b =>
        calcInternal(a, b) map { result =>
          result
        }
      }
    }

  result match {
    case Some(number) => println(s"The answer is $number!")
    case None         => println(s"Error calculating $operand1 $operator $operand2")
  }
}

6.6 Monads

We’ve seen that by implementing a few methods (map, flatMap, and optionally filter and foreach), we can use any class with a for comprehension. In the previous chapter we learned that such a class is called a monad. Here we are going to look in a bit more depth at monads.

6.6.1 What’s in a Monad?

The concept of a monad is notoriously difficult to explain because it is so general. We can get a good intuitive understanding by comparing some of the types of monad that we will deal with on a regular basis.

Broadly speaking, a monad is a generic type that allows us to sequence computations while abstracting away some technicality. We do the sequencing using for comprehensions, worrying only about the programming logic we care about. The code hidden in the monad’s map and flatMap methods does all of the plumbing for us. For example:

To demonstrate the generality of this principle, here are some examples. This first example calculates the sum of two numbers that may or may not be there:

for {
  a <- getFirstNumber  // getFirstNumber  returns Option[Int]
  b <- getSecondNumber // getSecondNumber returns Option[Int]
} yield a + b

// The final result is an Option[Int]---the result of
// applying `+` to `a` and `b` if both values are present

This second example calculate the sums of all possible pairs of numbers from two sequences:

for {
  a <- getFirstNumbers  // getFirstNumbers  returns Seq[Int]
  b <- getSecondNumbers // getSecondNumbers returns Seq[Int]
} yield a + b

// The final result is a Seq[Int]---the results of
// applying `+` to all combinations of `a` and `b`

This third example asynchronously calculates the sum of two numbers that can only be obtained asynchronously (all without blocking):

for {
  a <- getFirstNumber   // getFirstNumber  returns Future[Int]
  b <- getSecondNumber  // getSecondNumber returns Future[Int]
} yield a + b

// The final result is a Future[Int]---a data structure
// that will eventually allow us to access the result of
// applying `+` to `a` and `b`

The important point here is that, if we ignore the comments, these three examples look identical. Monads allow us to forget about one part of the problem at hand—optional values, multiple values, or asynchronously available values—and focus on just the part we care about—adding two numbers together.

There are many other monads that can be used to simplify problems in different circumstances. You may come across some of them in your future use of Scala. In this course we will concentrate entirely on Seq and Option.

6.6.2 Exercises

6.6.2.1 Adding All the Things ++

We’ve already seen how we can use a for comprehension to neatly add together three optional values. Let’s extend this to other monads. Use the following definitions:

import scala.util.Try

val opt1 = Some(1)
val opt2 = Some(2)
val opt3 = Some(3)

val seq1 = Seq(1)
val seq2 = Seq(2)
val seq3 = Seq(3)

val try1 = Try(1)
val try2 = Try(2)
val try3 = Try(3)

Add together all the options to create a new option. Add together all the sequences to create a new sequence. Add together all the trys to create a new try. Use a for comprehension for each. It shouldn’t take you long!

for {
  x <- opt1
  y <- opt2
  z <- opt3
} yield x + y + z

for {
  x <- seq1
  y <- seq2
  z <- seq3
} yield x + y + z

for {
  x <- try1
  y <- try2
  z <- try3
} yield x + y + z

How’s that for a cut-and-paste job?

6.7 For Comprehensions Redux

Earlier we looked at the fundamentals of for comprehensions. In this section we’re going to looking at some handy additional features they offer, and at idiomatic solutions to common problems.

6.7.1 Filtering

It’s quite common to only process selected elements. We can do this with comprehensions by adding an if clause after the generator expression. So to process only the positive elements of sequence we could write

for(x <- Seq(-2, -1, 0, 1, 2) if x > 0) yield x
// res0: Seq[Int] = List(1, 2)

The code is converted to a withFilter call, or if that doesn’t exist to filter.

Note that, unlike the normal if expression, an if clause in a generator does not have parentheses around the condition. So we write if x > 0 not if(x > 0) in a for comprehension.

6.7.2 Parallel Iteration

Another common problem is to iterate over two or more collections in parallel. For example, say we have the sequences Seq(1, 2, 3) and Seq(4, 5, 6) and we want to add together elements with the same index yielding Seq(5, 7 , 9). If we write

for {
  x <- Seq(1, 2, 3)
  y <- Seq(4, 5, 6)
} yield x + y
// res1: Seq[Int] = List(5, 6, 7, 6, 7, 8, 7, 8, 9)

we see that iterations are nested. We traverse the first element from the first sequence and then all the elements of the second sequence, then the second element from the first sequence and so on.

The solution is to zip together the two sequences, giving a sequence containing pairs of corresponding elements

Seq(1, 2, 3).zip(Seq(4, 5, 6))
// res2: Seq[(Int, Int)] = List((1,4), (2,5), (3,6))

With this we can easily compute the result we wanted

for(x <- Seq(1, 2, 3).zip(Seq(4, 5, 6))) yield { val (a, b) = x; a + b }
// res3: Seq[Int] = List(5, 7, 9)

Sometimes you want to iterate over the values in a sequence and their indices. For this case the zipWithIndex method is provided.

for(x <- Seq(1, 2, 3).zipWithIndex) yield x
// res4: Seq[(Int, Int)] = List((1,0), (2,1), (3,2))

Finally note that zip and zipWithIndex are available on all collection classes, including Map and Set.

6.7.3 Pattern Matching

The pattern on the left hand side of a generator is not named accidentally. We can include any pattern there and only process results matching the pattern. This provides another way of filtering results. So instead of:

for(x <- Seq(1, 2, 3).zip(Seq(4, 5, 6))) yield { val (a, b) = x; a + b }
// res5: Seq[Int] = List(5, 7, 9)

we can write:

for((a, b) <- Seq(1, 2, 3).zip(Seq(4, 5, 6))) yield a + b
// res6: Seq[Int] = List(5, 7, 9)

6.7.4 Intermediate Results

It is often useful to create an intermediate result within a sequence of generators. We can do this by inserting an assignment expression like so:

for {
  x     <- Seq(1, 2, 3)
  square = x * x
  y     <- Seq(4, 5, 6)
} yield square * y
// res7: Seq[Int] = List(4, 5, 6, 16, 20, 24, 36, 45, 54)

6.8 Maps and Sets

Up to now we’ve spent all of our time working with sequences. In this section we’ll go through the two other most common collection types: Maps and Sets.

6.8.1 Maps

A Map is very much like its counterpart in Java - it is a collection that maps keys to values. The keys must form a set and in most cases are unordered. Here is how to create a basic map:

val example = Map("a" -> 1, "b" -> 2, "c" -> 3)
// example: scala.collection.immutable.Map[String,Int] = Map(a -> 1, b -> 2, c -> 3)

The type of the resulting map is Map[String,Int], meaning all the keys are type String and all the values are of type Int.

A quick aside on ->. The constructor function for Map actually accepts an arbitrary number of Tuple2 arguments. -> is actually a function that generates a Tuple2.

"a" -> 1
// res0: (String, Int) = (a,1)

Let’s look at the most common operations on a map.

6.8.1.1 Accessing values using keys

The raison d’etre of a map is to convert keys to values. There are two main methods for doing this: apply and get.

example("a") // The same as example.apply("a")
// res1: Int = 1

example.get("a")
// res2: Option[Int] = Some(1)

apply attempts to look up a key and throws an exception if it is not found. By contrast, get returns an Option, forcing you to handle the not found case in your code.

example("d")
// java.util.NoSuchElementException: key not found: d
//   at scala.collection.MapLike$class.default(MapLike.scala:228)
//   at scala.collection.AbstractMap.default(Map.scala:59)
//   at scala.collection.MapLike$class.apply(MapLike.scala:141)
//   at scala.collection.AbstractMap.apply(Map.scala:59)
//   ... 266 elided

java.util.NoSuchElementException: key not found: d
// <console>:2: error: ';' expected but ':' found.
// java.util.NoSuchElementException: key not found: d
//                                                ^
example.get("d")
// res4: Option[Int] = None

Finally, the getOrElse method accepts a default value to return if the key is not found.

example.getOrElse("d", -1)
// res5: Int = -1

6.8.1.2 Determining membership

The contains method determines whether a map contains a key.

example.contains("a")
// res6: Boolean = true

6.8.1.3 Determining size

Finding the size of a map is just as easy as finding the size of a sequence.

example.size
// res7: Int = 3

6.8.1.4 Adding and removing elements

As with Seq, the default implementation of Map is immutable. We add and remove elements by creating new maps as opposed to mutating existing ones.

We can add new elements using the + method. Note that, as with Java’s HashMap, keys are overwritten and order is non-deterministic.

example.+("c" -> 10, "d" -> 11, "e" -> 12)
// res8: scala.collection.immutable.Map[String,Int] = Map(e -> 12, a -> 1, b -> 2, c -> 10, d -> 11)

We can remove keys using the - method:

example.-("b", "c")
// res9: scala.collection.immutable.Map[String,Int] = Map(a -> 1)

If we are only specifying a single argument, we can write + and - as infix operators.

example + ("d" -> 4) - "c"
// res10: scala.collection.immutable.Map[String,Int] = Map(a -> 1, b -> 2, d -> 4)

Note that we still have to write the pair "d" -> 4 in parentheses because + and -> have the same precedence.

There are many other methods for manipulating immutable maps. For example, the ++ and -- methods return the union and intersection of their arguments. See the Scaladoc for Map for more information.

6.8.1.5 Mutable maps

The scala.collection.mutable package contains several mutable implementations of Map:

val example2 = scala.collection.mutable.Map("x" -> 10, "y" -> 11, "z" -> 12)
// example2: scala.collection.mutable.Map[String,Int] = Map(z -> 12, y -> 11, x -> 10)

The in-place mutation equivalents of + and - are += and -= respectively.

example2 += ("x" -> 20)
// res11: example2.type = Map(z -> 12, y -> 11, x -> 20)

example2 -= ("y", "z")
// res12: example2.type = Map(x -> 20)

Note that, like their immutable cousins, += and -= both return a result of type Map. In this case, however, the return value is the same object that we called the method on. The return value is useful for chaining method calls together, but we can discard it if we see fit.

We can also use the update method, or its assignment-style syntactic-sugar, to update elements in the map:

example2("w") = 30
example2
// res14: scala.collection.mutable.Map[String,Int] = Map(w -> 30, x -> 20)

Note that, as with mutable sequences, a(b) = c is shorthand for a.update(b, c). The update method does not return a value, but the map is mutated as a side-effect.

There are many other methods for manipulating mutable maps. See the Scaladoc for scala.collection.mutable.Map for more information.

6.8.1.6 Sorted maps

The maps we have seen so far do not guarantee an ordering over their keys. For example, note that in this example, the order of keys in the resulting map is different from the order of addition operations.

Map("a" -> 1) + ("b" -> 2) + ("c" -> 3) + ("d" -> 4) + ("e" -> 5)
// res15: scala.collection.immutable.Map[String,Int] = Map(e -> 5, a -> 1, b -> 2, c -> 3, d -> 4)

Scala also provides ordered immutable and mutable versions of a ListMap class that preserves the order in which keys are added:

scala.collection.immutable.ListMap("a" -> 1) + ("b" -> 2) + ("c" -> 3) + ("d" -> 4) + ("e" -> 5)
// res16: scala.collection.immutable.ListMap[String,Int] = Map(a -> 1, b -> 2, c -> 3, d -> 4, e -> 5)

Scala’s separation of interface and implementation means that the methods on ordered and unordered maps are almost identical, although their performance may vary. See this useful page for more information on the performance characteristics of the various types of collection.

6.8.1.7 map and flatMap

Maps, like sequences, extend the Traversable trait, which means they inherit the standard map and flatMap methods. In fact, a Map[A,B] is a Traversable[Tuple2[A,B]], which means that map and flatMap operate on instances of Tuple2.

Here is an example of map:

example.map(pair => pair._1 -> pair._2 * 2)
// res17: scala.collection.immutable.Map[String,Int] = Map(a -> 2, b -> 4, c -> 6)

Note that the resulting object is also a Map as you might expect. However, what happens when the function we supply doesn’t return a pair? What does map return then? Is it a compile error? Let’s try it.

example.map(pair => pair._1 + " = " + pair._2)
// res18: scala.collection.immutable.Iterable[String] = List(a = 1, b = 2, c = 3)

It turns out the code does work, but we get back an Iterable result (look at the type, not the value)—a far more general data type.

Scala’s collections framework is built in a clever (and complicated) way that always ensures you get something sensible back out of one of the standard operations like map and flatMap. We won’t go into the details here (it’s practically a training course in its own right). Suffice to say that you can normally guess using common sense (and judicious use of the REPL) the type of collection you will get back from any operation.

Here is a more complicated example using flatMap:

example.flatMap {
         case (str, num) =>
           (1 to 3).map(x => (str + x) -> (num * x))
       }
// res19: scala.collection.immutable.Map[String,Int] = Map(c3 -> 9, b2 -> 4, b3 -> 6, c2 -> 6, b1 -> 2, c1 -> 3, a3 -> 3, a1 -> 1, a2 -> 2)

and the same example written using for syntax:

for{
         (str, num) <- example
          x         <- 1 to 3
       } yield (str + x) -> (num * x)
// res20: scala.collection.immutable.Map[String,Int] = Map(c3 -> 9, b2 -> 4, b3 -> 6, c2 -> 6, b1 -> 2, c1 -> 3, a3 -> 3, a1 -> 1, a2 -> 2)

Note that the result is a Map again. The argument to flatMap returns a sequence of pairs, so in the end we are able to make a new Map from them. If our function returns a sequence of non-pairs, we get back a more generic data type.

for{
         (str, num) <- example
          x         <- 1 to 3
       } yield (x + str) + "=" + (x * num)
// res21: scala.collection.immutable.Iterable[String] = List(1a=1, 2a=2, 3a=3, 1b=2, 2b=4, 3b=6, 1c=3, 2c=6, 3c=9)

6.8.1.8 In summary

Here is a type table of all the methods we have seen so far:

Method We have We provide We get

Map(...)

Tuple2[A,B], …

Map[A,B]

apply

Map[A,B]

A

B

get

Map[A,B]

A

Option[B]

+

Map[A,B]

Tuple2[A,B], …

Map[A,B]

-

Map[A,B]

Tuple2[A,B], …

Map[A,B]

++

Map[A,B]

Map[A,B]

Map[A,B]

--

Map[A,B]

Map[A,B]

Map[A,B]

contains

Map[A,B]

A

Boolean

size

Map[A,B]

Int

map

Map[A,B]

Tuple2[A,B] => Tuple2[C,D]

Map[C,D]

map

Map[A,B]

Tuple2[A,B] => E

Iterable[E]

flatMap

Map[A,B]

Tuple2[A,B] => Traversable[Tuple2[C,D]]

Map[C,D]

flatMap

Map[A,B]

Tuple2[A,B] => Traversable[E]

Iterable[E]

Here are the extras for mutable Maps:

Method We have We provide We get

+=

Map[A,B]

A

Map[A,B]

-=

Map[A,B]

A

Map[A,B]

update

Map[A,B]

A, B

Unit

6.8.2 Sets

Sets are unordered collections that contain no duplicate elements. You can think of them as sequences without an order, or maps with keys and no values. Here is a type table of the most important methods:

Method We have We provide We get

+

Set[A]

A

Set[A]

-

Set[A]

A

Set[A]

++

Set[A]

Set[A]

Set[A]

--

Set[A]

Set[A]

Set[A]

contains

Set[A]

A

Boolean

apply

Set[A]

A

Boolean

size

Set[A]

Int

map

Set[A]

A => B

Set[B]

flatMap

Set[A]

A => Traversable[B]

Set[B]

and the extras for mutable Sets:

Method We have We provide We get

+=

Set[A]

A

Set[A]

-=

Set[A]

A

Set[A]

6.8.3 Exercises

6.8.3.1 Favorites

Copy and paste the following code into an editor:

val people = Set(
  "Alice",
  "Bob",
  "Charlie",
  "Derek",
  "Edith",
  "Fred")

val ages = Map(
  "Alice"   -> 20,
  "Bob"     -> 30,
  "Charlie" -> 50,
  "Derek"   -> 40,
  "Edith"   -> 10,
  "Fred"    -> 60)

val favoriteColors = Map(
  "Bob"     -> "green",
  "Derek"   -> "magenta",
  "Fred"    -> "yellow")

val favoriteLolcats = Map(
  "Alice"   -> "Long Cat",
  "Charlie" -> "Ceiling Cat",
  "Edith"   -> "Cloud Cat")

Use the code as test data for the following exercises:

Write a method favoriteColor that accepts a person’s name as a parameter and returns their favorite colour.

The person may or may not be a key in the favoriteColors map so the function should return an Option result:

def favoriteColor(person: String): Option[String] =
  favoriteColors.get(person)

Update favoriteColor to return a person’s favorite color or beige as a default.

Now we have a default value we can return a String instead of an Option[String]:

def favoriteColor(person: String): String =
  favoriteColors.get(person).getOrElse("beige")

Write a method printColors that prints everyone’s favorite color!

We can write this one using foreach or a for comprehension:

def printColors() = for {
  person <- people
} println(s"${person}'s favorite color is ${favoriteColor(person)}!")

or:

def printColors() = people foreach { person =>
  println(s"${person}'s favorite color is ${favoriteColor(person)}!")
}

Write a method lookup that accepts a name and one of the maps and returns the relevant value from the map. Ensure that the return type of the method matches the value type of the map.

Here we write a generic method using a type parameter:

def lookup[A](name: String, values: Map[String, A]) =
  values get name

Calculate the color of the oldest person:

First we find the oldest person, then we look up the answer:

val oldest: Option[String] =
  people.foldLeft(Option.empty[String]) { (older, person) =>
    if(ages.getOrElse(person, 0) > older.flatMap(ages.get).getOrElse(0)) {
      Some(person)
    } else {
      older
    }
  }

val favorite: Option[String] =
  for {
    oldest <- oldest
    color  <- favoriteColors.get(oldest)
  } yield color

6.8.4 Do-It-Yourself Part 2

Now we have some practice with maps and sets let’s see if we can implement some useful library functions for ourselves.

6.8.4.1 Union of Sets

Write a method that takes two sets and returns a set containing the union of the elements. Use iteration, like map or foldLeft, not the built-in union method to do so!

As always, start by writing out the types and then follow the types to fill-in the details.

def union[A](set1: Set[A], set2: Set[A]): Set[A] = {
  ???
}

We need to think of an algorithm for computing the union. We can start with one of the sets and add the elements from the other set to it. The result will be the union. What types does this result in? Our result has type Set[A] and we need to add every A from the two sets to our result, which is an operation with type (Set[A], A) => Set[A]. This means we need a fold. Since order is not important any fold will do.

def union[A](set1: Set[A], set2: Set[A]): Set[A] = {
  set1.foldLeft(set2){ (set, elt) => (set + elt) }
}

6.8.4.2 Union of Maps

Now let’s write union for maps. Assume we have two Map[A, Int] and add corresponding elements in the two maps. So the union of Map('a' -> 1, 'b' -> 2) and Map('a' -> 2, 'b' -> 4) should be Map('a' -> 3, 'b' -> 6).

The solution follows the same pattern as the union for sets, but here we have to handle adding the values as well.

def union[A](map1: Map[A, Int], map2: Map[A, Int]): Map[A, Int] = {
  map1.foldLeft(map2){ (map, elt) =>
    val (key, value1) = elt
    val value2 = map.get(key)
    val total = value1 + value2.getOrElse(0)
    map + (key -> total)
  }
}

6.8.4.3 Generic Union

There are many things that can be added, such as strings (string concatenation), sets (union), and of course numbers. It would be nice if we could generalise our union method on maps to handle anything for which a sensible add operation can be defined. How can we go about doing this?

With the tools we’ve seen far, we could add another function parameter like so:

def union[A, B](map1: Map[A, B], map2: Map[A, B], add: (B, B) => B): Map[A, B] = {
  map1.foldLeft(map2){ (map, elt) =>
    val (k, v) = elt
    val newV = map.get(k).map(v2 => add(v, v2)).getOrElse(v)
    map + (k -> newV)
  }
}

Later we’ll see a nicer way to do this using type classes.

6.9 Ranges

So far we’ve seen lots of ways to iterate over sequences but not much in the way of iterating over numbers. In Java and other languages it is common to write code like

for(i = 0; i < array.length; i++) {
  doSomething(array[i])
}

We’ve seen that for comprehensions provide a succinct way of implementing these programs. But what about classics like this?

for(i = 99; i > 0; i--) {
  System.out.println(i + "bottles of beer on the wall!")
  // Full text omitted for the sake of brevity
}

Scala provides the Range class for these occasions. A Range represents a sequence of integers from some starting value to less than the end value with a non-zero step. We can construct a Range using the until method on Int.

1 until 10
// res0: scala.collection.immutable.Range = Range(1, 2, 3, 4, 5, 6, 7, 8, 9)

By default the step size is 1, so trying to go from high to low gives us an empty Range.

10 until 1
// res1: scala.collection.immutable.Range = Range()

We can rectify this by specifying a different step, using the by method on Range.

10 until 1 by -1
// res2: scala.collection.immutable.Range = Range(10, 9, 8, 7, 6, 5, 4, 3, 2)

Now we can write the Scala equivalent of our Java program.

for(i <- 99 until 0 by -1) println(i + " bottles of beer on the wall!")
// 99 bottles of beer on the wall!
// 98 bottles of beer on the wall!
// 97 bottles of beer on the wall!

This gives us a hint of the power of ranges. Since they are sequences we can combine them with other sequences in interesting ways. For example, to create a range with a gap in the middle we can concatenate two ranges:

(1 until 10) ++ (20 until 30)
// res7: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29)

Note that the result is a Vector not a Range but this doesn’t matter. As they are both sequences we can use both them in a for comprehension without any code change!

6.10 Generating Random Data

In this section we have an extended case study of generating random data. The ideas here have many applications. For example, in generating data for testing, as used in property based testing, in probabilistic programming, a new area of machine learning, and, if you’re going through the extended case study, in generative art.

6.10.1 Random Words

We’ll start by generating text. Imagine we wanted to generate (somewhat) realistic text, perhaps to use as a placeholder to fill in parts of a website design. If we took a large amount of real text we could analyse to work out for each word what the most common words following it are. Such a model is known as a Markov chain.

To keep this example to a reasonable size we’re going to deal with a really simplified version of the problem, where all sentences have the form subject-verb-object. For example, “Noel wrote code”.

Write a program to generate all possible sentences given the following model:

The following code will compute all possible sentences. The equivalent with explicit flatMap and map would also work.

Note that flatMap has more power than we need for this example. We could use the subject to alter how we choose the verb, for example. We’ll use this ability in the next exercise.

val subjects = List("Noel", "The cat", "The dog")
val verbs = List("wrote", "chased", "slept on")
val objects = List("the book", "the ball", "the bed")

def allSentences: List[(String, String, String)] =
  for {
    subject <- subjects
    verb <- verbs
    obj <- objects
  } yield (subject, verb, obj)

This model creates some clearly nonsensical sentences. We can do better by making the choice of verb dependend on the subject, and the object depend on the verb.

Let’s use the following model:

Implement this.

We’re now using the full power of flatMap and map (via our for comprehension) to make decisions in our code that are dependent on what has happened before.

def verbsFor(subject: String): List[String] =
  subject match {
    case "Noel" => List("wrote", "chased", "slept on")
    case "The cat" => List("meowed at", "chased", "slept on")
    case "The dog" => List("barked at", "chased", "slept on")
  }

def objectsFor(verb: String): List[String] =
  verb match {
    case "wrote" => List("the book", "the letter", "the code")
    case "chased" => List("the ball", "the dog", "the cat")
    case "slept on" => List("the bed", "the mat", "the train")
    case "meowed at" => List("Noel", "the door", "the food cupboard")
    case "barked at" => List("the postman", "the car", "the cat")
  }

def allSentencesConditional: List[(String, String, String)] =
  for {
    subject <- subjects
    verb <- verbsFor(subject)
    obj <- objectsFor(verb)
  } yield (subject, verb, obj)

This model has all the features we need for our full random generation model. In particular we have conditional distributions, meaning the choice of, say, verb is dependent or conditional on what has come before.

6.10.2 Probabilities

We now have a model that we can imagine making arbitrarily complex to generate more and more realistic data, but we’re missing the element of probability that would allow us to weight the data generation towards more common outcomes.

Let’s extend our model to work on List[(A, Double)], where A is the type of data we are generating and the Double is a probability. We’re still enumerating all possibilities but we’re now associating a probability with each possible outcome.

Start by defining a class Distribution that will wrap a List[(A, Double)]. (Why?)

There are no subtypes involved here, so a simple final case class will do. We wrap the List[(A, Double)] within a class so we can encapsulate manipulating the probabilities—external code can view the probabilities but probably shouldn’t be directly working with them.

final case class Distribution[A](events: List[(A, Double)])

We should create some convenience constructors for Distribution. A useful one is uniform which will accept a List[A] and create a Distribution[A] where each element has equal probability. Make it so.

The convenience constructor looks like this:

def uniform[A](atoms: List[A]): Distribution[A] = {
  val p = 1.0 / atoms.length
  Distribution(atoms.map(a => a -> p))
}
// uniform: [A](atoms: List[A])Distribution[A]

According to Scala convention, convenience constructors should normally live on the companion object.

What are the other methods we must add to implement the models we’ve seen so far? What are their signatures?

We need flatMap and map. The signatures follow the patterns that flatMap and map always have:

def flatMap[B](f: A => Distribution[B]): Distribution[B]

and

def map[B](f: A => B): Distribution[B]

Now implement these methods. Start with map, which is simpler. We might end up with elements appearing multiple times in the list of events after calling map. That’s absolutely ok.

Implementing map merely requires we follow the types.

final case class Distribution[A](events: List[(A, Double)]) {
  def map[B](f: A => B): Distribution[B] =
    Distribution(events map { case (a, p) => f(a) -> p })
}

Now implement flatMap. To do so you’ll need to combine the probability of an event with the probability of the event it depends on. The correct way to do so is to multiply the probabilities together. This may lead to unnormalised probabilities—probabilities that do not sum up to 1. You might find the following two utilities useful, though you don’t need to normalise probabilities or ensure that elements are unique for the model to work.

final case class Distribution[A](events: List[(A, Double)]) {
  def normalize: Distribution[A] = {
    val totalWeight = (events map { case (a, p) => p }).sum
    Distribution(events map { case (a,p) => a -> (p / totalWeight) })
  }

  def compact: Distribution[A] = {
    val distinct = (events map { case (a, p) => a }).distinct
    def prob(a: A): Double =
      (events filter { case (x, p) => x == a } map { case (a, p) => p }).sum

    Distribution(distinct map { a => a -> prob(a) })
  }
}

Once we know how to combine probabilities we just have to follow the types. I’ve decided to normalise the probabilities after flatMap as it helps avoid numeric underflow, which can occur in complex models. An alternative is to use log-probabilities, replacing multiplication with addition.

final case class Distribution[A](events: List[(A, Double)]) {
  def map[B](f: A => B): Distribution[B] =
    Distribution(events map { case (a, p) => f(a) -> p })

  def flatMap[B](f: A => Distribution[B]): Distribution[B] =
    Distribution(events flatMap { case (a, p1) =>
                   f(a).events map { case (b, p2) => b -> (p1 * p2) }
                 }).compact.normalize

  def normalize: Distribution[A] = {
    val totalWeight = (events map { case (a, p) => p }).sum
    Distribution(events map { case (a,p) => a -> (p / totalWeight) })
  }

  def compact: Distribution[A] = {
    val distinct = (events map { case (a, p) => a }).distinct
    def prob(a: A): Double =
      (events filter { case (x, p) => x == a } map { case (a, p) => p }).sum

    Distribution(distinct map { a => a -> prob(a) })
  }
}

object Distribution {
  def uniform[A](atoms: List[A]): Distribution[A] = {
    val p = 1.0 / atoms.length
    Distribution(atoms.map(a => a -> p))
  }
}

6.10.3 Examples

With Distribution we can now define some interesting model. We could do some classic problems, such as working out the probability that a coin flip gives three heads in a row.

sealed trait Coin
case object Heads extends Coin
case object Tails extends Coin
val fairCoin: Distribution[Coin] = Distribution.uniform(List(Heads, Tails))
val threeFlips =
  for {
    c1 <- fairCoin
    c2 <- fairCoin
    c3 <- fairCoin
  } yield (c1, c2, c3)
// threeFlips: Distribution[(Coin, Coin, Coin)] = Distribution(List(((Heads,Heads,Heads),0.125), ((Heads,Heads,Tails),0.125), ((Heads,Tails,Heads),0.125), ((Heads,Tails,Tails),0.125), ((Tails,Heads,Heads),0.125), ((Tails,Heads,Tails),0.125), ((Tails,Tails,Heads),0.125), ((Tails,Tails,Tails),0.125)))

From this we can read of the probability of three heads being 0.125, as we’d expect.

Let’s create a more complex model. Imagine the following situation:

I put my food into the oven and after some time it ready to eat and produces delicious smell with probability 0.3 and otherwise it is still raw and produces no smell with probability 0.7. If there are delicious smells the cat comes to harass me with probability 0.8, and otherwise it stays asleep. If there is no smell the cat harasses me for the hell of it with probability 0.4 and otherwise stays asleep.

Implement this model and answer the question: if the cat comes to harass me what is the probability my food is producing delicious smells (and therefore is ready to eat.)

I found it useful to add this constructor to the companion object of Distribution:

def discrete[A](events: List[(A,Double)]): Distribution[A] =
  Distribution(events).compact.normalize

First I constructed the model

// We assume cooked food makes delicious smells with probability 1.0, and raw
// food makes no smell with probability 0.0.
sealed trait Food
case object Raw extends Food
case object Cooked extends Food

val food: Distribution[Food] =
  Distribution.discrete(List(Cooked -> 0.3, Raw -> 0.7))

sealed trait Cat
case object Asleep extends Cat
case object Harassing extends Cat

def cat(food: Food): Distribution[Cat] =
  food match {
    case Cooked => Distribution.discrete(List(Harassing -> 0.8, Asleep -> 0.2))
    case Raw => Distribution.discrete(List(Harassing -> 0.4, Asleep -> 0.6))
  }

val foodModel: Distribution[(Food, Cat)] =
  for {
    f <- food
    c <- cat(f)
  } yield (f, c)

From foodModel we could read off the probabilities of interest, but it’s more fun to write some code to do this for us. Here’s what I did.

// Probability the cat is harassing me
val pHarassing: Double =
  foodModel.events.filter {
    case ((_, Harassing), _) => true
    case ((_, Asleep), _) => false
  }.map { case (a, p) => p }.sum

// Probability the food is cooked given the cat is harassing me
val pCookedGivenHarassing: Option[Double] =
  foodModel.events collectFirst[Double] {
    case ((Cooked, Harassing), p) => p
  } map (_ / pHarassing)

From this we can see the probability my food is cooked given the cat is harassing me is probably 0.46. I should probably check the oven even though it’s more likely the food isn’t cooked because leaving my food in and it getting burned is a far worse outcome than checking my food while it is still raw.

This example also shows us that to use this library for real we’d probably want to define a lot of utility functions, such as filter, directly on distribution. We also need to keep probabilities unnormalised after certain operations, such as filtering, so we can compute conditional probabilities correctly.

6.10.4 Next Steps

The current library is limited to working with discrete events. If we wanted to work with continuous domains, such as coordinates in the plane, we would need a different representation as we clearly can’t represent all possible outcomes. Also, we can easily run into issues when working with complex discrete models, as the number of events increases exponentially with each flatMap.

Instead of representing all events we can sample from the distributions of interest and maintain a set of samples. Varying the size of the set allows us to tradeoff accuracy with computational resources.

We could use the same style of implementation with a sampling representation, but this would require us to fix the number of samples in advance. It’s more useful to be able to repeatedly sample from the same model, so that the user can ask for more samples if they decide they need higher accuracy. Doing so requires that we separate defining the structure of the model from the process of sampling, hence reifying the model. We’re not going to go further into this implementation here, but if you’re going through the case study you’ll pick up the techniques needed to implement it.

7 Type Classes

Type classes are a powerful feature of Scala that allow us to extend existing libraries with new functionality, without using inheritance and without having access to the original library source code. In this chapter we will learn how to use and implement type classes, using a Scala feature called implicits.

In the section on traits we compared object oriented and functional style in terms of extensibility, using this table.

Add new method Add new data

OO

Change existing code

Existing code unchanged

FP

Existing code unchanged

Change existing code

Type classes give us a third implementation technique which is more flexible than either. A type class is like a trait, defining an interface. However, with type classes we can:

This means we can add new methods or new data without changing any existing code.

It’s difficult to understand these concepts without an example. We’ll start this section by exploring how we can use type classes. We’ll then turn to implementing them ourselves. We’ll finish with a discussion of best practices.

7.1 Type Class Instances

Type classes in Scala involve the interaction of a number of components. To simplify the presentation we are going to start by looking at using type classes before we look at how to build them ourselves.

7.1.1 Ordering

A simple example of a type class is the Ordering trait. For a type A, an Ordering[A] defines a comparison method compare that compares two instances of A by some ordering. To construct an Ordering we can use the convenience method fromLessThan defined on the companion object.

Imagine we want to sort a List of Ints. There are many different ways to sort such a list. For example, we could sort from highest to lowest, or we could sort from lowest to highest. There is a method sorted on List that will sort a list, but to use it we must pass in an Ordering to give the particular ordering we want.

Let’s define some Orderings and see them in action.

import scala.math.Ordering
val minOrdering = Ordering.fromLessThan[Int](_ < _)
// minOrdering: scala.math.Ordering[Int] = scala.math.Ordering$$anon$9@7fa1c525

val maxOrdering = Ordering.fromLessThan[Int](_ > _)
// maxOrdering: scala.math.Ordering[Int] = scala.math.Ordering$$anon$9@41f99fa2

List(3, 4, 2).sorted(minOrdering)
// res0: List[Int] = List(2, 3, 4)

List(3, 4, 2).sorted(maxOrdering)
// res1: List[Int] = List(4, 3, 2)

Here we define two orderings: minOrdering, which sorts from lowest to highest, and maxOrdering, which sorts from highest to lowest. When we call sorted we pass the Ordering we want to use. These implementations of a type class are called type class instances.

The type class pattern separates the implementation of functionality (the type class instance, an Ordering[A] in our example) from the type the functionality is provided for (the A in an Ordering[A]). This is the basic pattern for type classes. Everything else we will see just provides extra convenience.

7.1.2 Implicit Values

It can be inconvenient to continually pass the type class instance to a method when we want to repeatedly use the same instance. Scala provides a convenience, called an implicit value, that allows us to get the compiler to pass the type class instance for us. Here’s an example of use:

implicit val ordering = Ordering.fromLessThan[Int](_ < _)
List(2, 4, 3).sorted
// res2: List[Int] = List(2, 3, 4)

List(1, 7 ,5).sorted
// res3: List[Int] = List(1, 5, 7)

Note we didn’t supply an ordering to sorted. Instead, the compiler provides it for us.

We have to tell the compiler which values it is allowed pass to methods for us. We do this by annotating a value with implicit, as in the declaration implicit val ordering = .... The method must also indicate that it accepts implicit values. If you look at the documentation for the sorted method on List you see that the single parameter is declared implicit. We’ll talk more about implicit parameter lists in a bit. For now we just need to know that we can get the compiler to supply implicit values to parameters that are themselves marked implicit.

7.1.3 Declaring Implicit Values

We can tag any val, var, object or zero-argument def with the implicit keyword, making it a potential candidate for an implicit parameter.

implicit val exampleOne = ...
implicit var exampleTwo = ...
implicit object exampleThree = ...
implicit def exampleFour = ...

An implicit value must be declared within a surrounding object, class, or trait.

7.1.4 Implicit Value Ambiguity

What happens when multiple implicit values are in scope? Let’s ask the console.

implicit val minOrdering = Ordering.fromLessThan[Int](_ < _)

implicit val maxOrdering = Ordering.fromLessThan[Int](_ > _)
List(3,4,5).sorted
// <console>:17: error: ambiguous implicit values:
//  both value ordering of type => scala.math.Ordering[Int]
//  and value minOrdering of type => scala.math.Ordering[Int]
//  match expected type scala.math.Ordering[Int]
//        List(3,4,5).sorted
//                    ^

//  <console>:12: error: ambiguous implicit values:
//  both value ordering of type => scala.math.Ordering[Int]
//  and value minOrdering of type => scala.math.Ordering[Int]
//  match expected type scala.math.Ordering[Int]
//                 List(3,4,5).sorted
//                             ^

The rule is simple: the compiler will signal an error if there is any ambiguity in which implicit value should be used.

7.1.5 Take Home Points

In this section we’ve seen the basics for using type classes. In Scala, a type class is just a trait. To use a type class we:

Marking values as implicit tells the compiler it can supply them as a parameter to a method call if none is explicitly given. For the compiler to supply a value:

  1. the parameter must be marked implicit in the method declaration;
  2. there must be an implicit value available of the same type as the parameter; and
  3. there must be only one such implicit value available.

7.1.6 Exercises

7.1.6.1 More Orderings

Define an Ordering that orders Ints from lowest to highest by absolute value. The following test cases should pass.

assert(List(-4, -1, 0, 2, 3).sorted(absOrdering) == List(0, -1, 2, 3, -4))
assert(List(-4, -3, -2, -1).sorted(absOrdering) == List(-1, -2, -3, -4))
val absOrdering = Ordering.fromLessThan[Int]{ (x, y) =>
  Math.abs(x) < Math.abs(y)
}

Now make your ordering an implicit value, so the following test cases work.

assert(List(-4, -1, 0, 2, 3).sorted == List(0, -1, 2, 3, -4))
assert(List(-4, -3, -2, -1).sorted == List(-1, -2, -3, -4))

Simply mark the value as implicit (and make sure it is in scope)

implicit val absOrdering = Ordering.fromLessThan[Int]{ (x, y) =>
  Math.abs(x) < Math.abs(y)
}

7.1.6.2 Rational Orderings

Scala doesn’t have a class to represent rational numbers, but we can easily implement one ourselves.

final case class Rational(numerator: Int, denominator: Int)

Implement an Ordering for Rational to order rationals from smallest to largest. The following test case should pass.

assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==
       List(Rational(1, 3), Rational(1, 2), Rational(3, 4)))
implicit val ordering = Ordering.fromLessThan[Rational]((x, y) =>
  (x.numerator.toDouble / x.denominator.toDouble) <
  (y.numerator.toDouble / y.denominator.toDouble)
)

7.2 Organising Type Class Instances

In section we’ll learn about the places the compiler searches for type class instances (implicit values), known as the implicit scope, and we’ll discuss how to organise type class instances to make their use more convenient.

7.2.1 Implicit Scope

The compiler searches the implicit scope when it tries to find an implicit value to supply as an implicit parameter. The implicit scope is composed of several parts, and there are rules that prioritise some parts over others.

The first part of the implicit scope is the normal scope where other identifiers are found. This includes identifiers declared in the local scope, within any enclosing class, object, or trait, or imported from elsewhere. An eligible implicit value must be a single identifier (i.e. a, not a.b). This is referred to as the local scope.

The implicit scope also includes the companion objects of types involved in the method call with the implicit parameter. Let’s look at sorted for example. The signature for sorted, defined on List[A], is

def sorted[B >: A](implicit ord: math.Ordering[B]): List[A]

The compiler will look in the following places for Ordering instances:

The practical upshot is we can define type class instances in the companion object of our types (the type A in this example) and they will be found by the compiler without the user having to import them explicitly.

In the previous section we defined an Ordering for a Rational type we created. Let’s see how we can use the companion object to make this Ordering easier to use.

First let’s define the ordering in the local scope.

final case class Rational(numerator: Int, denominator: Int)

object Example {
  def example() = {
    implicit val ordering = Ordering.fromLessThan[Rational]((x, y) =>
      (x.numerator.toDouble / x.denominator.toDouble) <
      (y.numerator.toDouble / y.denominator.toDouble)
    )
    assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==
           List(Rational(1, 3), Rational(1, 2), Rational(3, 4)))
  }
}

This works as we expect.

Now let’s shift the type class instance out of the local scope and see that it doesn’t compile.

final case class Rational(numerator: Int, denominator: Int)

object Instance {
  implicit val ordering = Ordering.fromLessThan[Rational]((x, y) =>
    (x.numerator.toDouble / x.denominator.toDouble) <
    (y.numerator.toDouble / y.denominator.toDouble)
  )
}
object Example {
  def example =
    assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==
           List(Rational(1, 3), Rational(1, 2), Rational(3, 4)))
}
// <console>:16: error: No implicit Ordering defined for Rational.
//            assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==
//                                                                        ^

Here I get an error at compilation time

No implicit Ordering defined for Rational.
assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==
                                                            ^

Finally let’s move the type class instance into the companion object of Rational and see that the code compiles again.

final case class Rational(numerator: Int, denominator: Int)

object Rational {
  implicit val ordering = Ordering.fromLessThan[Rational]((x, y) =>
    (x.numerator.toDouble / x.denominator.toDouble) <
    (y.numerator.toDouble / y.denominator.toDouble)
  )
}
object Example {
  def example() =
    assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==
           List(Rational(1, 3), Rational(1, 2), Rational(3, 4)))
}

This leads us to our first pattern for packaging type class instances.

Type Class Instance Packaging: Companion Objects

When defining a type class instance, if

  1. there is a single instance for the type; and
  2. you can edit the code for the type that you are defining the instance for

then define the type class instance in the companion object of the type.

7.2.2 Implicit Priority

If we look in the companion object for Ordering we see some type class instances are already defined. In particular there is an instance for Int, yet we could define our own instances for Ordering[Int] (which we did in the previous section) and not have an issue with ambiguity.

To understand this we need to learn about the priority rules for selecting implicits. An ambiguity error is only raised if there are multiple type class instances with the same priority. Otherwise the highest priority implicit is selected.

The full priority rules are rather complex, but that complexity has little impact in most cases. The practical implication is that the local scope takes precedence over instances found in companion objects. This means that implicits that the programmer explicitly pulls into scope, by importing or defining them in the local scope, will be used in preference.

Let’s see this in practice, by defining an Ordering for Rational within the local scope.

final case class Rational(numerator: Int, denominator: Int)

object Rational {
  implicit val ordering = Ordering.fromLessThan[Rational]((x, y) =>
    (x.numerator.toDouble / x.denominator.toDouble) <
    (y.numerator.toDouble / y.denominator.toDouble)
  )
}
object Example {
  implicit val higherPriorityImplicit = Ordering.fromLessThan[Rational]((x, y) =>
      (x.numerator.toDouble / x.denominator.toDouble) >
      (y.numerator.toDouble / y.denominator.toDouble)
  )

  def example() =
    assert(List(Rational(1, 2), Rational(3, 4), Rational(1, 3)).sorted ==
           List(Rational(3, 4), Rational(1, 2), Rational(1, 3)))
}

Notice that higherPriorityImplicit defines a different ordering to the one defined in the companion object for Rational. We’ve also changed the expected ordering in example to match this new ordering. This code both compiles and runs correctly, illustrating the effect of the priority rules.

Type Class Instance Packaging: Companion Objects Part 2

When defining a type class instance, if

  1. there is a single good default instance for the type; and
  2. you can edit the code for the type that you are defining the instance for

then define the type class instance in the companion object of the type. This allows users to override the instance by defining one in the local scope whilst still providing sensible default behaviour.

7.2.3 Packaging Implicit Values Without Companion Objects

If there is no good default instance for a type class instance, or if there are several good defaults, we should not place type class instances in the companion object but instead require the user to explicitly import an instance into the local scope.

In this case, one simple way to package instances is to place each in its own object that the user can import into the local scope. For instance, we might define orderings for Rational as follows:

final case class Rational(numerator: Int, denominator: Int)

object RationalLessThanOrdering {
  implicit val ordering = Ordering.fromLessThan[Rational]((x, y) =>
    (x.numerator.toDouble / x.denominator.toDouble) <
    (y.numerator.toDouble / y.denominator.toDouble)
  )
}

object RationalGreaterThanOrdering {
  implicit val ordering = Ordering.fromLessThan[Rational]((x, y) =>
    (x.numerator.toDouble / x.denominator.toDouble) >
    (y.numerator.toDouble / y.denominator.toDouble)
  )
}

In use the user would import RationalLessThanOrdering._ or import RationalGreaterThanOrdering._ as appropriate.

7.2.4 Take Home Points

The compiler looks for type class instances (implicit values) in two places:

  1. the local scope; and
  2. the companion objects of types involved in the method call.

Implicits found in the local scope take precedence over those found in companion objects.

When packaging type class instances, if there is a single instance or a single good default we should put it in the companion object if possible. Otherwise, one way to package implicits is to place each one in an object and require the user to explicitly import them.

7.2.5 Exercises

7.2.5.1 Ordering Orders

Here is a case class to store orders of some arbitrary item.

final case class Order(units: Int, unitPrice: Double) {
  val totalPrice: Double = units * unitPrice
}

We have a requirement to order Orders in three different ways:

  1. by totalPrice;
  2. by number of units; and
  3. by unitPrice.

Implement and package implicits to provide these orderings, and justify your packaging.

My implementation is below. I decided that ordering by totalPrice is likely to be the most common choice, and therefore should be the default. Thus I placed it in the companion object for Order. The other two orderings I placed in objects so the user could explicitly import them.

final case class Order(units: Int, unitPrice: Double) {
  val totalPrice: Double = units * unitPrice
}

object Order {
  implicit val lessThanOrdering = Ordering.fromLessThan[Order]{ (x, y) =>
    x.totalPrice < y.totalPrice
  }
}

object OrderUnitPriceOrdering {
  implicit val unitPriceOrdering = Ordering.fromLessThan[Order]{ (x, y) =>
    x.unitPrice < y.unitPrice
  }
}

object OrderUnitsOrdering {
  implicit val unitsOrdering = Ordering.fromLessThan[Order]{ (x, y) =>
    x.units < y.units
  }
}

7.3 Creating Type Classes

In the previous sections we saw how to create and use type class instances. Now we’re going to explore creating our own type classes.

7.3.1 Elements of Type Classes

There are four components of the type class pattern:

We have already seen type class instances and talked briefly about implicit parameters. Here we will look at defining our own type class, and in the following section we will look at the two styles of interface.

7.3.2 Creating a Type Class

Let’s start with an example—converting data to HTML. This is a fundamental operation in any web application, and it would be great to be able to provide a toHtml method across the board in our application.

One implementation strategy is to create a trait we extend wherever we want this functionality:

trait HtmlWriteable {
  def toHtml: String
}

final case class Person(name: String, email: String) extends HtmlWriteable {
  def toHtml = s"<span>$name &lt;$email&gt;</span>"
}
Person("John", "john@example.com").toHtml
// res1: String = <span>John &lt;john@example.com&gt;</span>

This solution has a number of drawbacks. First, we are restricted to having just one way of rendering a Person. If we want to list people on our company homepage, for example, it is unlikely we will want to list everybody’s email addresses without obfuscation. For logged in users, however, we probably want the convenience of direct email links. Second, this pattern can only be applied to classes that we have written ourselves. If we want to render a java.util.Date to HTML, for example, we will have to write some other form of library function.

Polymorphism has failed us, so perhaps we should try pattern matching instead? We could write something like

object HtmlWriter {
  def write(in: Any): String =
    in match {
      case Person(name, email) => ???
      case d: Date => ???
      case _ => throw new Exception(s"Can't render ${in} to HTML")
    }
}

This implementation has its own issues. We have lost type safety because there is no useful supertype that covers just the elements we want to render and no more. We can’t have more than one implementation of rendering for a given type. We also have to modify this code whenever we want to render a new type.

We can overcome all of these problems by moving our HTML rendering to an adapter class:

trait HtmlWriter[A] {
  def write(in: A): String
}

object PersonWriter extends HtmlWriter[Person] {
  def write(person: Person) = s"<span>${person.name} &lt;${person.email}&gt;</span>"
}
PersonWriter.write(Person("John", "john@example.com"))
// res3: String = <span>John &lt;john@example.com&gt;</span>

This is better. We can now define HtmlWriter functionality for other types, including types we have not written ourselves:

import java.util.Date

object DateWriter extends HtmlWriter[Date] {
  def write(in: Date) = s"<span>${in.toString}</span>"
}
DateWriter.write(new Date)
// res5: String = <span>Thu Mar 05 16:17:38 UTC 2020</span>

We can also write another HtmlWriter for writing People on our homepage:

object ObfuscatedPersonWriter extends HtmlWriter[Person] {
  def write(person: Person) =
    s"<span>${person.name} (${person.email.replaceAll("@", " at ")})</span>"
}
ObfuscatedPersonWriter.write(Person("John", "john@example.com"))
// res6: String = <span>John (john at example.com)</span>

Much safer—it’ll take a spam bot more than a few microseconds to decypher that!

You might recognise PersonWriter, DateWriter, and ObfuscatedPersonWriter as following the type class instance pattern (though we haven’t made them implicit values at this point). The HtmlWriter trait, which the instances implement, is the type class itself.

Type Class Pattern

A type class is a trait with at least one type variable. The type variables specify the concrete types the type class instances are defined for. Methods in the trait usually use the type variables.

trait ExampleTypeClass[A] {
  def doSomething(in: A): Foo
}

The next step is to introduce implicit parameters, so we can use type classes with less boilerplate.

7.3.3 Take Home Points

We have seen the basic pattern for implementing type classes.

trait HtmlWriter[A] {
  def toHtml(in: A): String
}
object PersonWriter extends HtmlWriter[Person] {
  def toHtml(person: Person) =
    s"${person.name} (${person.email})"
}

object ObfuscatedPersonWriter extends HtmlWriter[Person] {
  def toHtml(person: Person) =
    s"${person.name} (${person.email.replaceAll("@", " at ")})"
}

7.3.4 Exercises

7.3.4.1 Equality

Scala provides two equality predicates: by value (==) and by reference (eq). Nonetheless, we sometimes need additional predicates. For instance, we could compare people by just email address if we were validating new user accounts in some web application.

Implement a trait Equal of some type A, with a method equal that compares two values of type A and returns a Boolean. Equal is a type class.

trait Equal[A] {
  def equal(v1: A, v2: A): Boolean
}

Our Person class is

case class Person(name: String, email: String)

Implement instances of Equal that compare for equality by email address only, and by name and email.

object EmailEqual extends Equal[Person] {
  def equal(v1: Person, v2: Person): Boolean =
    v1.email == v2.email
}

object NameEmailEqual extends Equal[Person] {
  def equal(v1: Person, v2: Person): Boolean =
    v1.email == v2.email && v1.name == v2.name
}

7.4 Implicit Parameter and Interfaces

We’ve seen the basics of the type class pattern. Now let’s look at how we can make it easier to use. Recall our starting point is a trait HtmlWriter which allows us to implement HTML rendering for classes without requiring access to their source code, and allows us to render the same class in different ways.

trait HtmlWriter[A] {
  def write(in: A): String
}

object PersonWriter extends HtmlWriter[Person] {
  def write(person: Person) = s"<span>${person.name} &lt;${person.email}&gt;</span>"
}

This issue with this code is that we need manage a lot of HtmlWriter instances when we render any complex data. We have already seen that we can manage this complexity using implicit values and have mentioned implicit parameters in passing. In this section we go in depth on implicit parameters.

7.4.1 Implicit Parameter Lists

Here is an example of an implicit parameter list:

object HtmlUtil {
  def htmlify[A](data: A)(implicit writer: HtmlWriter[A]): String = {
    writer.write(data)
  }
}

The htmlify method accepts two arguments: some data to convert to HTML and a writer to do the conversion. The writer is an implicit parameter.

The implicit keyword applies to the whole parameter list, not just an individual parameter. This makes the parameter list optional—when we call HtmlUtil.htmlify we can either specify the list as normal

HtmlUtil.htmlify(Person("John", "john@example.com"))(PersonWriter)
// res1: String = <span>John &lt;john@example.com&gt;</span>

or we can omit the implicit parameters. If we omit the implicit parameters, the compiler searches for implicit values of the correct type it can use to fill in the missing arguments. We have already learned about implicit values, but let’s see a quick example to refresh our memory. First we define an implicit value.

implicit object ApproximationWriter extends HtmlWriter[Int] {
  def write(in: Int): String =
    s"It's definitely less than ${((in / 10) + 1) * 10}"
}

When we use HtmlUtil we don’t have to specify the implicit parameter if an implicit value can be found.

HtmlUtil.htmlify(2)

7.4.2 Interfaces Using Implicit Parameters

A complete use of the type class pattern requires an interface using implicit parameters, along with implicit type class instances. We’ve seen two examples already: the sorted method using Ordering, and the htmlify method above. The best interface depends on the problem being solved, but there is a pattern that occurs frequently enough that it is worth explaining here.

In many case the interface defined by the type class is the same interface we want to use. This is the case for HtmlWriter – the only method of interest is write. We could write something like

object HtmlWriter {
  def write[A](in: A)(implicit writer: HtmlWriter[A]): String =
    writer.write(in)
}

We can avoid this indirection (which becomes more painful to write as our interfaces become larger) with the following construction:

object HtmlWriter {
  def apply[A](implicit writer: HtmlWriter[A]): HtmlWriter[A] =
    writer
}

In use it looks like

HtmlWriter[Person].write(Person("Noel", "noel@example.org"))

The idea is to simply select a type class instance by type (done by the no-argument apply method) and then directly call the methods defined on that instance.

Type Class Interface Pattern

If the desired interface to a type class TypeClass is exactly the methods defined on the type class trait, define an interface on the companion object using a no-argument apply method like

object TypeClass {
  def apply[A](implicit instance: TypeClass[A]): TypeClass[A] =
    instance
}

7.4.3 Take Home Points

Implicit parameters make type classes more convenient to use. We can make an entire parameter list with the implicit keyword to make it an implicit parameter list.

def method[A](normalParam1: NormalType, ...)(implicit implicitParam1: ImplicitType[A], ...)

If we call a method and do not explicitly supply its implicit parameter list, the compiler will search for implicit values of the correct types to complete the parameter list for us.

Using implicit parameters we can make more convenient interfaces using type class instances. If the desired interface to a type class is exactly the methods defined on the type class we can create a convenient interface using the pattern

object TypeClass {
  def apply[A](implicit instance: TypeClass[A]): TypeClass[A] =
    instance
}

7.4.4 Exercises

7.4.4.1 Equality Again

In the previous section we defined a trait Equal along with some implementations for Person.

case class Person(name: String, email: String)

trait Equal[A] {
  def equal(v1: A, v2: A): Boolean
}

object EmailEqual extends Equal[Person] {
  def equal(v1: Person, v2: Person): Boolean =
    v1.email == v2.email
}

object NameEmailEqual extends Equal[Person] {
  def equal(v1: Person, v2: Person): Boolean =
    v1.email == v2.email && v1.name == v2.name
}

Implement an object called Eq with an apply method. This method should accept two explicit parameters of type A and an implicit Equal[A]. It should perform the equality checking using the provided Equal. With appropriate implicits in scope, the following code should work

Eq(Person("Noel", "noel@example.com"), Person("Noel", "noel@example.com"))
object Eq {
  def apply[A](v1: A, v2: A)(implicit equal: Equal[A]): Boolean =
    equal.equal(v1, v2)
}

Package up the different Equal implementations as implicit values in their own objects, and show you can control the implicit selection by changing which object is imported.

object NameAndEmailImplicit {
  implicit object NameEmailEqual extends Equal[Person] {
    def equal(v1: Person, v2: Person): Boolean =
      v1.email == v2.email && v1.name == v2.name
  }
}

object EmailImplicit {
  implicit object EmailEqual extends Equal[Person] {
    def equal(v1: Person, v2: Person): Boolean =
      v1.email == v2.email
  }
}

object Examples {
  def byNameAndEmail = {
    import NameAndEmailImplicit._
    Eq(Person("Noel", "noel@example.com"), Person("Noel", "noel@example.com"))
  }

  def byEmail = {
    import EmailImplicit._
    Eq(Person("Noel", "noel@example.com"), Person("Dave", "noel@example.com"))
  }
}

Now implement an interface on the companion object for Equal using the no-argument apply method pattern. The following code should work.

import NameAndEmailImplicit._
Equal[Person].equal(Person("Noel", "noel@example.com"), Person("Noel", "noel@example.com"))

Which interface style do you prefer?

The following code is what we’re looking for:

object Equal {
  def apply[A](implicit instance: Equal[A]): Equal[A] =
    instance
}

In this case the Eq interface is slightly easier to use, as it requires less typing. For most complicated interfaces, with more than a single method, the companion object pattern would be preferred. In the next section we’ll see how we can make interfaces that appear to be methods defined on the objects of interest.

7.5 Enriched Interfaces

A second type of type class interface, called type enrichment13 allow us to create interfaces that act as if they were methods defined on the classes of interest. For example, suppose we have a method called numberOfVowels:

def numberOfVowels(str: String) =
  str.filter(Seq('a', 'e', 'i', 'o', 'u').contains(_)).length
numberOfVowels("the quick brown fox")
// res0: Int = 5

This is a method that we use all the time. It would be great if numberOfVowels was a built-in method of String so we could write "a string".numberOfVowels, but of course we can’t change the source code for String. Scala has a feature called implicit classes that allow us to add new functionality to an existing class without editing its source code. This is a similar concept to categories in Objective C or extension methods in C#, but the implementation is different in each case.

7.5.1 Implicit Classes

Let’s build up implicit classes piece by piece. We can wrap String in a class that adds our numberOfVowels:

class ExtraStringMethods(str: String) {
  val vowels = Seq('a', 'e', 'i', 'o', 'u')

  def numberOfVowels =
    str.toList.filter(vowels contains _).length
}

We can use this to wrap up our String and gain access to our new method:

new ExtraStringMethods("the quick brown fox").numberOfVowels

Writing new ExtraStringMethods every time we want to use numberOfVowels is unwieldy. However, if we tag our class with the implicit keyword, we give Scala the ability to insert the constructor call automatically into our code:

implicit class ExtraStringMethods(str: String) { /* ... */ }
"the quick brown fox".numberOfVowels
// res2: Int = 5

When the compiler processes our call to numberOfVowels, it interprets it as a type error because there is no such method in String. Rather than give up, the compiler attempts to fix the error by searching for an implicit class that provides the method and can be constructed from a String. It finds ExtraStringMethods. The compiler then inserts an invisible constructor call, and our code type checks correctly.

Implicit classes follow the same scoping rules as implicit values. Like implicit values, they must be defined within an enclosing object, class, or trait (except when writing Scala at the console).

There is one additional restriction for implicit classes: only a single implicit class will be used to resolve a type error. The compiler will not look to construct a chain of implicit classes to access the desired method.

7.6 Combining Type Classes and Type Enrichment

Implicit classes can be used on their own but we most often combine them with type classes to create a more natural style of interface. We keep the type class (HtmlWriter) and adapters (PersonWriter, DateWriter and so on) from our type class example, and add an implicit class with methods that themselves take implicit parameters. For example:

implicit class HtmlOps[T](data: T) {
  def toHtml(implicit writer: HtmlWriter[T]) =
    writer.toHtml(data)
}

This allows us to invoke our type-class pattern on any type for which we have an adapter as if it were a built-in feature of the class:

Person("John", "john@example.com").toHtml
// res3: String = John (john@example.com)

This gives us many benefits. We can extend existing types to give them new functionality, use simple syntax to invoke the functionality, and choose our preferred implementation by controlling which implicits we have in scope.

7.6.1 Take Home Points

Implicit classes are a Scala language feature that allows us to define extra functionality on existing data types without using conventional inheritance. This is a programming pattern called type enrichment.

The Scala compiler uses implicit classes to fix type errors in our code. When it encounters us accessing a method or field that doesn’t exist, it looks through the available implicits to find some code it can insert to fix the error.

The rules for implicit classes are the same as for implicit values, with the additional restriction that only a single implicit class will be used to fix a type error.

7.6.2 Exercises

7.6.2.1 Drinking the Kool Aid

Use your newfound powers to add a method yeah to Int, which prints Oh yeah! as many times as the Int on which it is called if the Int is positive, and is silent otherwise. Here’s an example of usage:

2.yeah()

3.yeah()

-1.yeah()

When you have written your implicit class, package it in an IntImplicits object.

implicit class IntOps(n: Int) {
  def yeah() = for{ _ <- 0 until n } println("Oh yeah!")
}

2.yeah()
// Oh yeah!
// Oh yeah!

The solution uses a for comprehension and a range to iterate through the correct number of iterations. Remember that the range 0 until n is the same as 0 to n-1—it contains all numbers from 0 inclusive to n exclusive.

The names IntImplicits and IntOps are quite vague—we would probably name them something more specific in a production codebase. However, for this exercise they will suffice.

7.6.2.2 Times

Extend your previous example to give Int an extra method called times that accepts a function of type Int => Unit as an argument and executes it n times. Example usage:

3.times(i => println(s"Look - it's the number $i!"))

For bonus points, re-implement yeah in terms of times.

object IntImplicits {
  implicit class IntOps(n: Int) {
    def yeah() =
      times(_ => println("Oh yeah!"))

    def times(func: Int => Unit) =
      for(i <- 0 until n) func(i)
  }
}

7.6.3 Easy Equality

Recall our Equal type class from a previous section.

trait Equal[A] {
  def equal(v1: A, v2: A): Boolean
}

Implement an enrichment so we can use this type class via a triple equal (===) method. For example, if the correct implicits are in scope the following should work.

"abcd".===("ABCD") // Assumes case-insensitive equality implicit

We just need to define an implicit class, which I have here placed in the companion object of Equal.

trait Equal[A] {
  def equal(v1: A, v2: A): Boolean
}
object Equal {
  def apply[A](implicit instance: Equal[A]): Equal[A] =
    instance

  implicit class ToEqual[A](in: A) {
    def ===(other: A)(implicit equal: Equal[A]): Boolean =
      equal.equal(in, other)
  }
}

Here is an example of use.

implicit val caseInsensitiveEquals = new Equal[String] {
  def equal(s1: String, s2: String) =
    s1.toLowerCase == s2.toLowerCase
}

import Equal._

"foo".===("FOO")

7.7 Using Type Classes

We have seen how to define type classes. In this section we’ll see some conveniences for using them: context bounds and the implicitly method.

7.7.1 Context Bounds

When we use type classes we often end up requiring implicit parameters that we pass onward to a type class interface. For example, using our HtmlWriter example we might want to define some kind of page template that accepts content rendered by a writer.

def pageTemplate[A](body: A)(implicit writer: HtmlWriter[A]): String = {
  val renderedBody = body.toHtml

  s"<html><head>...</head><body>${renderedBody}</body></html>"
}

We don’t explicitly use the implicit writer in our code, but we need it in scope so the compiler can insert it for the toHtml enrichment.

Context bounds allow us to write this more compactly, with a notation that is reminiscent of a type bound.

def pageTemplate[A : HtmlWriter](body: A): String = {
  val renderedBody = body.toHtml

  s"<html><head>...</head><body>${renderedBody}</body></html>"
}

The context bound is the notation [A : HtmlWriter] and it expands into the equivalent implicit parameter list in the prior example.

Context Bound Syntax

A context bound is an annotation on a generic type variable like so:

[A : Context]

It expands into a generic type parameter [A] along with an implicit parameter for a Context[A].

7.7.2 Implicitly

Context bounds give us a short-hand syntax for declaring implicit parameters, but since we don’t have an explicit name for the parameter we cannot use it in our methods. Normally we use context bounds when we don’t need explicit access to the implicit parameter, but rather just implicitly pass it on to some other method. However if we do need access for some reason we can use the implicitly method.

case class Example(name: String)
implicit val implicitExample = Example("implicit")
implicitly[Example]
// res0: Example = Example(implicit)

implicitly[Example] == implicitExample
// res1: Boolean = true

The implicitly method takes no parameters but has a generic type parameters. It returns the implicit matching the given type, assuming there is no ambiguity.

7.8 Implicit Conversions

So far we have seen two programming patterns using implicits: type enrichment, which we implement using implicit classes, and type classes, which we implement using implicit values and parameter lists.

Scala has a third implicit mechanism called implicit conversions that we will cover here for completeness. Implicit conversions can be seen as a more general form of implicit classes, and can be used in a wider variety of contexts.

The Dangers of Implicit Conversions

As we shall see later in this section, undisciplined use of implicit conversions can cause as many problems as it fixes for the beginning programmer. Scala even requires us to write a special import statement to silence compiler warnings resulting from the use of implicit conversions:

import scala.language.implicitConversions

We recommend using implicit classes and implicit values/parameters over implicit conversions wherever possible. By sticking to the type enrichment and type class design patterns you should find very little cause to use implicit conversions in your code.

You have been warned!

7.8.1 Implicit conversions

Implicit conversions are a more general form of implicit classes. We can tag any single-argument method with the implicit keyword to allow the compiler to implicitly use the method to perform automated conversions from one type to another:

class B {
  def bar = "This is the best method ever!"
}

class A

implicit def aToB(in: A): B = new B()
new A().bar
// res2: String = This is the best method ever!

Implicit classes are actually just syntactic sugar for the combination of a regular class and an implicit conversion. With an implicit class we have to define a new type as a target for the conversion; with an implicit method we can convert from any type to any other type as long as an implicit is available in scope.

7.8.2 Designing with Implicit Conversions

The power of implicit conversions tends to cause problems for newer Scala developers. We can easily define very general type conversions that play strange games with the semantics of our programs:

implicit def intToBoolean(int: Int) = int == 0
if(1) "yes" else "no"
// res3: String = no

if(0) "yes" else "no"
// res4: String = yes

This example is ridiculous, but it demonstrates the potential problems implicits can cause. intToBoolean could be defined in a library in a completely different part of our codebase, so how would we debug the bizarre behaviour of the if expressions above?

Here are some tips for designing using implicits that will prevent situations like the one above:

7.8.3 Exercises

7.8.3.1 Implicit Class Conversion

Any implicit class can be reimplemented as a class paired with an implicit method. Re-implement the IntOps class from the type enrichment section in this way. Verify that the class still works the same way as it did before.

Here is the solution. The methods yeah and times are exactly as we implemented them previously. The only differences are the removal of the implicit keyword on the class and the addition of the implicit def to do the job of the implicit constructor:

object IntImplicits {
  class IntOps(n: Int) {
    def yeah() =
      times(_ => println("Oh yeah!"))

    def times(func: Int => Unit) =
      for(i <- 0 until n) func(i)
  }

  implicit def intToIntOps(value: Int) =
    new IntOps(value)
}

The code still works the same way it did previously. The implicit conversion is not available until we bring it into scope:

5.yeah()
// <console>:18: error: value yeah is not a member of Int
//        5.yeah()
//          ^

Once the conversion has been brought into scope, we can use yeah and times as usual:

import IntImplicits._

5.yeah()
// Oh yeah!
// Oh yeah!
// Oh yeah!
// Oh yeah!
// Oh yeah!

7.9 JSON Serialisation

In this section we have an extended example involving serializing Scala data to JSON, which is one of the classic use cases for type classes. The typical process for converting data to JSON in Scala involves two steps. First we convert our data types to an intermediate case class representation, then we serialize the intermediate representation to a string.

Here is a suitable case class representation of a subset of the JSON language. We have a sealed trait JsValue that defines a stringify method, and a set of subtypes for two of the main JSON data types—objects and strings:

sealed trait JsValue {
  def stringify: String
}

final case class JsObject(values: Map[String, JsValue]) extends JsValue {
  def stringify = values
    .map { case (name, value) => "\"" + name + "\":" + value.stringify }
    .mkString("{", ",", "}")
}

final case class JsString(value: String) extends JsValue {
  def stringify = "\"" + value.replaceAll("\\|\"", "\\\\$1") + "\""
}

You should recognise this as the algebraic data type pattern.

We can construct JSON objects and serialize them as follows:

val obj = JsObject(Map("foo" -> JsString("a"), "bar" -> JsString("b"), "baz" -> JsString("c")))
// obj: JsObject = JsObject(Map(foo -> JsString(a), bar -> JsString(b), baz -> JsString(c)))

obj.stringify
// res2: String = {"foo":"a","bar":"b","baz":"c"}

7.9.1 Convert X to JSON

Let’s create a type class for converting Scala data to JSON. Implement a JsWriter trait containing a single abstract method write that converts a value to a JsValue.

The type class is generic in a type A. The write method converts a value of type A to some kind of JsValue.

trait JsWriter[A] {
  def write(value: A): JsValue
}

Now let’s create the dispatch part of our type class. Write a JsUtil object containing a single method toJson. The method should accept a value of an arbitrary type A and convert it to JSON.

Tip: your method will have to accept an implicit JsWriter to do the actual conversion.

object JsUtil {
  def toJson[A](value: A)(implicit writer: JsWriter[A]) =
    writer write value
}

Now, let’s revisit our data types from the web site visitors example in the Sealed traits section:

import java.util.Date

sealed trait Visitor {
  def id: String
  def createdAt: Date
  def age: Long = new Date().getTime() - createdAt.getTime()
}

final case class Anonymous(
  id: String,
  createdAt: Date = new Date()
) extends Visitor

final case class User(
  id: String,
  email: String,
  createdAt: Date = new Date()
) extends Visitor

Write JsWriter instances for Anonymous and User.

implicit object AnonymousWriter extends JsWriter[Anonymous] {
  def write(value: Anonymous) = JsObject(Map(
    "id"           -> JsString(value.id),
    "createdAt"    -> JsString(value.createdAt.toString)
  ))
}

implicit object UserWriter extends JsWriter[User] {
  def write(value: User) = JsObject(Map(
    "id"           -> JsString(value.id),
    "email"        -> JsString(value.email),
    "createdAt"    -> JsString(value.createdAt.toString)
  ))
}

Given these two definitions we can implement a JsWriter for Visitor as follows. This uses a new type of pattern – a: B – which matches any value of type B and binds it to a variable a:

implicit object VisitorWriter extends JsWriter[Visitor] {
  def write(value: Visitor) = value match {
    case anon: Anonymous => JsUtil.toJson(anon)
    case user: User      => JsUtil.toJson(user)
  }
}

Finally, verify that your code works by converting the following list of users to JSON:

val visitors: Seq[Visitor] = Seq(Anonymous("001", new Date), User("003", "dave@xample.com", new Date))
visitors.map(visitor => JsUtil.toJson(visitor))

7.9.2 Prettier Conversion Syntax

Let’s improve our JSON syntax by combining type classes and type enrichment. Convert JsUtil to an implicit class with a toJson method. Sample usage:

Anonymous("001", new Date).toJson
implicit class JsUtil[A](value: A) {
  def toJson(implicit writer: JsWriter[A]) =
    writer write value
}

In the previous exercise we only defined JsWriters for our main case classes. With this convenient syntax, it makes sense for us to have an complete set of JsWriters for all the serializable types in our codebase, including Strings and Dates:

implicit object StringWriter extends JsWriter[String] {
  def write(value: String) = JsString(value)
}

implicit object DateWriter extends JsWriter[Date] {
  def write(value: Date) = JsString(value.toString)
}

With these definitions we can simplify our existing JsWriters for Anonymous, User, and Visitor:

implicit object AnonymousWriter extends JsWriter[Anonymous] {
  def write(value: Anonymous) = JsObject(Map(
    "id"        -> value.id.toJson,
    "createdAt" -> value.createdAt.toJson
  ))
}

implicit object UserWriter extends JsWriter[User] {
  def write(value: User) = JsObject(Map(
    "id"        -> value.id.toJson,
    "email"     -> value.email.toJson,
    "createdAt" -> value.createdAt.toJson
  ))
}

implicit object VisitorWriter extends JsWriter[Visitor] {
  def write(value: Visitor) = value match {
    case anon: Anonymous => anon.toJson
    case user: User      => user.toJson
  }
}

8 Conclusions

This completes Essential Scala. To recap our journey, we have learned Scala via the major patterns of usage:

These are the patterns we use daily in our Scala coding, which we have found work well across many Scala projects, and they make up by the far the majority of our Scala code. They will serve you well.

We have tried to emphasise that if you can model the problem correctly the code follows in an almost mechanical way. Learning how to think in the Scala way (or, more broadly, in a functional way) is by far the most important lesson of this book.

We have introduced language features as they support the patterns. In the appendices you will find additional material covering some inessential functionality we have skipped over in the main text. Scala has a few other features, such as self types, that we have found so little use for in our years of programming Scala that we have omitted them entirely in this introductory text.

8.1 What Now?

The journey to mastering Scala has not finished with this book. You will benefit greatly from active participation in the Scala community. We have setup an online chat room for discussion of all Scala related matters. Any and all Scala related questions are welcome there. There are many other forums, conferences, and user groups where you can find an enthusiastic and welcoming community of fellow programmers.

If you have enjoyed Essential Scala we hope you’ll consider our followup book Advanced Scala. As the name suggests, it covers more advanced concepts with an emphasis on patterns for larger programs.

Finally, we would love hear your thoughts on Essential Scala. Any feedback—good or bad—helps to improve the book. We can be reached at hello@underscore.io. Any improvements we make to Essential Scala will of course be made available to every reader as part of our policy of free lifetime updates.

Thank you for reading Essential Scala, and we hope you future coding in Scala is productive and fun.

9 Pattern Matching

We have seen the duality between algebraic data types and pattern matching. Armed with this information, we are in a good position to return to pattern matching and see some of its more powerful features.

As we discussed earlier, patterns are written in their own DSL that only superficially resembles regular Scala code. Patterns serve as tests that match a specific set of Scala values. The match expression compares a value to each pattern in turn, finds the first pattern that matches, and executes the corresponding block of Scala code.

Some patterns bind values to variables that can be used on the right hand side of the corresponding => symbol, and some patterns contain other patterns, allowing us to build complex tests that simultaneously examine many parts of a value. Finally, we can create our own custom patterns, implemented in Scala code, to match any cross-section of values we see fit.

We have already seen case class patterns and certain types of sequence patterns. Each of the remaining types of pattern is described below together with an example of its use.

9.1 Standard patterns

9.1.1 Literal patterns

Literal patterns match a particular value. Any Scala literals work except function literals: primitive values, Strings, nulls, and ():

(1 + 1) match {
  case 1 => "It's one!"
  case 2 => "It's two!"
  case 3 => "It's three!"
}
// res0: String = It's two!

Person("Dave", "Gurnell") match {
  case Person("Noel", "Welsh") => "It's Noel!"
  case Person("Dave", "Gurnell") => "It's Dave!"
}
// res1: String = It's Dave!

println("Hi!") match {
  case () => "It's unit!"
}
// Hi!
// res2: String = It's unit!

9.1.2 Constant patterns

Identifiers starting with an uppercase letter are constants that match a single predefined constant value:

val X = "Foo"
// X: String = Foo

val Y = "Bar"
// Y: String = Bar

val Z = "Baz"
// Z: String = Baz

"Bar" match {
  case X => "It's foo!"
  case Y => "It's bar!"
  case Z => "It's baz!"
}
// res3: String = It's bar!

9.1.3 Alternative patterns

Vertical bars can be used to specify alternatives:

"Bar" match {
  case X | Y => "It's foo or bar!"
  case Z     => "It's baz!"
}
// res4: String = It's foo or bar!

9.1.4 Variable capture

Identifiers starting with lowercase letters bind values to variables. The variables can be used in the code to the right of the =>:

Person("Dave", "Gurnell") match {
  case Person(f, n) => f + " " + n
}
// res5: String = Dave Gurnell

The @ operator, written x @ y, allows us to capture a value in a variable x while also matching it against a pattern y. x must be a variable pattern and y can be any type of pattern. For example:

Person("Dave", "Gurnell") match {
  case p @ Person(_, s) => s"The person $p has the surname $s"
}
// res6: String = The person Person(Dave,Gurnell) has the surname Gurnell

9.1.5 Wildcard patterns

The _ symbol is a pattern that matches any value and simply ignores it. This is useful in two situations: when nested inside other patterns, and when used on its own to provide an “else” clause at the end of a match expression:

Person("Dave", "Gurnell") match {
  case Person("Noel", _) => "It's Noel!"
  case Person("Dave", _) => "It's Dave!"
}
// res7: String = It's Dave!

Person("Dave", "Gurnell") match {
  case Person(name, _) => s"It's $name!"
}
// res8: String = It's Dave!

Person("John", "Doe") match {
  case Person("Noel", _) => "It's Noel!"
  case Person("Dave", _) => "It's Dave!"
  case _ => "It's someone else!"
}
// res9: String = It's someone else!

9.1.6 Type patterns

A type pattern takes the form x: Y where Y is a type and x is a wildcard pattern or a variable pattern. The pattern matches any value of type Y and binds it to x:

val shape: Shape = Rectangle(1, 2)
// shape: Shape = Rectangle(1.0,2.0)

shape match {
  case c : Circle    => s"It's a circle: $c!"
  case r : Rectangle => s"It's a rectangle: $r!"
  case s : Square    => s"It's a square: $s!"
}
// res10: String = It's a rectangle: Rectangle(1.0,2.0)!

9.1.7 Tuple patterns

Tuples of any arity can be matched with parenthesised expressions as follows:

(1, 2) match {
  case (a, b) => a + b
}
// res11: Int = 3

9.1.8 Guard expressions

This isn’t so much a pattern as a feature of the overall match syntax. We can add an extra condition to any case clause by suffixing the pattern with the keyword if and a regular Scala expression. For example:

123 match {
  case a if a % 2 == 0 => "even"
  case _ => "odd"
}
// res12: String = odd

To reiterate, the code between the if and => keywords is a regular Scala expression, not a pattern.

9.2 Custom Patterns

In the last section we took an in-depth look at all of the types of pattern that are embedded into the pattern matching language. However, in that list we didn’t see some of the patterns that we’ve been using in the course so far—case class and sequence patterns were nowhere to be seen!

There is a final aspect of pattern matching that we haven’t covered that truly makes it a universal tool—we can define our own custom extractor patterns using regular Scala code and use them along-side the built-in patterns in our match expressions.

9.2.1 Extractors

An extractor pattern looks like a function call of zero or more arguments: foo(a, b, c), where each argument is itself an arbitrary pattern.

Extractor patterns are defined by creating objects with a method called unapply or unapplySeq. We’ll dive into the guts of these methods in a minute. For now let’s look at some of the predefined extractor patterns from the Scala library.

9.2.1.1 Case class extractors

The companion object of every case class is equipped with an extractor that creates a pattern of the same arity as the constructor. This makes it easy to capture fields in variables:

Person("Dave", "Gurnell") match {
  case Person(f, l) => List(f, l)
}
// res0: List[String] = List(Dave, Gurnell)

9.2.1.2 Regular expressions

Scala’s regular expression objects are outfitted with a pattern that binds each of the captured groups:

import scala.util.matching.Regex
val r = new Regex("""(\d+)\.(\d+)\.(\d+)\.(\d+)""")
// r: scala.util.matching.Regex = (\d+)\.(\d+)\.(\d+)\.(\d+)

"192.168.0.1" match {
  case r(a, b, c, d) => List(a, b, c, d)
}
// res1: List[String] = List(192, 168, 0, 1)

9.2.1.3 Lists and Sequences

Lists and sequences can be captured in several ways:

The List and Seq companion objects act as patterns that match fixed-length sequences.

List(1, 2, 3) match {
   case List(a, b, c) => a + b + c
}
// res2: Int = 6
Nil match {
  case List(a) => "length 1"
  case Nil => "length 0"
}
// res3: String = length 0

There is also a singleton object :: that matches the head and tail of a list.

List(1, 2, 3) match {
  case ::(head, tail) => s"head $head tail $tail"
  case Nil => "empty"
}
// res4: String = head 1 tail List(2, 3)

This perhaps makes more sense when you realise that binary extractor patterns can also be written infix.

List(1, 2, 3) match {
  case head :: tail => s"head $head tail $tail"
  case Nil => "empty"
}
// res5: String = head 1 tail List(2, 3)

Combined use of ::, Nil, and _ allow us to match the first elements of any length of list.

List(1, 2, 3) match {
  case Nil => "length 0"
  case a :: Nil => s"length 1 starting $a"
  case a :: b :: Nil => s"length 2 starting $a $b"
  case a :: b :: c :: _ => s"length 3+ starting $a $b $c"
}
// res6: String = length 3+ starting 1 2 3

9.2.1.4 Creating custom fixed-length extractors

You can use any object as a fixed-length extractor pattern by giving it a method called unapply with a particular type signature:

def unapply(value: A): Boolean           // pattern with 0 parameters
def unapply(value: A): Option[B]                      // 1 parameter
def unapply(value: A): Option[(B1, B2)]               // 2 parameters
                                                      // etc...

Each pattern matches values of type A and captures arguments of type B, B1, and so on. Case class patterns and :: are examples of fixed-length extractors.

For example, the extractor below matches email addresses and splits them into their user and domain parts:

object Email {
  def unapply(str: String): Option[(String, String)] = {
    val parts = str.split("@")
    if (parts.length == 2) Some((parts(0), parts(1))) else None
  }
}
"dave@underscore.io" match {
  case Email(user, domain) => List(user, domain)
}
// res7: List[String] = List(dave, underscore.io)

"dave" match {
  case Email(user, domain) => List(user, domain)
  case _ => Nil
}
// res8: List[String] = List()

This simpler pattern matches any string and uppercases it:

object Uppercase {
  def unapply(str: String): Option[String] =
    Some(str.toUpperCase)
}
Person("Dave", "Gurnell") match {
  case Person(f, Uppercase(l)) => s"$f $l"
}
// res9: String = Dave GURNELL

9.2.1.5 Creating custom variable-length extractors

We can also create extractors that match arbitrary numbers of arguments by defining an unapplySeq method of the following form:

def unapplySeq(value: A): Option[Seq[B]]

Variable-length extractors match a value only if the pattern in the case clause is the same length as the Seq returned by unapplySeq. Regex and List are examples of variable-length extractors.

The extractor below splits a string into its component words:

object Words {
  def unapplySeq(str: String) = Some(str.split(" ").toSeq)
}
"the quick brown fox" match {
  case Words(a, b, c)    => s"3 words: $a $b $c"
  case Words(a, b, c, d) => s"4 words: $a $b $c $d"
}
// res10: String = 4 words: the quick brown fox

9.2.1.6 Wildcard sequence patterns

There is one final type of pattern that can only be used with variable-length extractors. The wildcard sequence pattern, written _*, matches zero or more arguments from a variable-length pattern and discards their values. For example:

List(1, 2, 3, 4, 5) match {
  case List(a, b, _*) => a + b
}
// res11: Int = 3

"the quick brown fox" match {
  case Words(a, b, _*) => a + b
}
// res12: String = thequick

We can combine wildcard patterns with the @ operator to capture the remaining elements in the sequence.

"the quick brown fox" match {
  case Words(a, b, rest @ _*) => rest
}
// res13: Seq[String] = WrappedArray(brown, fox)

9.2.2 Exercises

9.2.2.1 Positive Matches

Custom extractors allow us to abstract away complicated conditionals. In this example we will build a very simple extractor, which we probably wouldn’t use in real code, but which is representative of this idea.

Create an extractor Positive that matches any positive integer. Some test cases:

assert(
  "No" ==
    (0 match {
       case Positive(_) => "Yes"
       case _ => "No"
     })
)

assert(
  "Yes" ==
    (42 match {
       case Positive(_) => "Yes"
       case _ => "No"
     })
)

To implement this extractor we define an unapply method on an object Postiive:

object Positive {
  def unapply(in: Int): Option[Int] =
    if(in > 0)
      Some(in)
    else
      None
}

9.2.2.2 Titlecase extractor

Extractors can also transform their input. In this exercise we’ll write an extractor that converts any string to titlecase by uppercasing the first letter of every word. A test case:

assert(
  "Sir Lord Doctor David Gurnell" ==
    ("sir lord doctor david gurnell" match {
       case Titlecase(str) => str
     })
)

Tips:

This extractor isn’t particularly useful, and in general defining your own extractors is not common in Scala. However it can be a useful tool in certain circumstances.

The model solution splits the string into a list of words and maps over the list, manipulating each word before re-combining the words into a string.

object Titlecase {
  def unapply(str: String) =
    Some(str.split(" ").toList.map {
      case "" => ""
      case word => word.substring(0, 1).toUpperCase + word.substring(1)
    }.mkString(" "))
}

10 Collections Redux

This optional section covers some more details of the collections framework that typically aren’t used in day-to-day programming. This includes the different sequence implementations available, details of collections operations on arrays and strings, some of the core traits in the framework, and details of Java interoperation.

10.1 Sequence Implementations

We’ve seen that the Scala collections separate interface from implementation. This means we can work with all collections in a generic manner. However different concrete implementations have different performance characteristics, so we must be aware of the available implementations so we can choose appropriately. Here we look at the mostly frequently used implementations of Seq. For full details on all the available implementation see the docs.

10.1.1 Peformance Characteristics

The collections framework distinguishes at the type level two general classes of sequences. Sequences implementing IndexedSeq have efficient apply, length, and (if mutable) update operations, while LinearSeqs have efficient head and tail operations. Neither have any additional operations over Seq.

10.1.2 Immutable Implementations

The main immutable Seq implementations are List, and Stream, and Vector.

10.1.2.1 List

A List is a singly linked list. It has constant time access to the first element and remainder of the list (head, and tail) and is thus a LinearSeq. It also has constant time prepending to the front of the list, but linear time appending to the end. List is the default Seq implementation.

10.1.2.2 Stream

A Stream is like a list except its elements are computed on demand, and thus it can have infinite size. Like other collections we can create streams by calling the apply method on the companion object.

Stream(1, 2, 3)
// res0: scala.collection.immutable.Stream[Int] = Stream(1, ?)

Note that only the first element is printed. The others will be computed when we try to access them.

We can also use the #:: method to construct a stream from individual elements, starting from Stream.empty.

Stream.empty.#::(3).#::(2).#::(1)
// res1: scala.collection.immutable.Stream[Int] = Stream(1, ?)

We can also use the more natural operator syntax.

1 #:: 2 #:: 3 #:: Stream.empty
// res2: scala.collection.immutable.Stream[Int] = Stream(1, ?)

This method allows us to create a infinite stream. Here’s an infinite stream of 1s:

def streamOnes: Stream[Int] = 1 #:: streamOnes
streamOnes
// res3: Stream[Int] = Stream(1, ?)

Because elements are only evaluated as requested, calling streamOnes doesn’t lead to infinte recursion. When we take the first five elements (and convert them to a List, so they’ll all print out) we see we have what we want.

streamOnes.take(5).toList
// res4: List[Int] = List(1, 1, 1, 1, 1)

10.1.2.3 Vector

Vector is the final immutable sequence we’ll consider. Unlike Stream and List it is an IndexedSeq, and thus offers fast random access and updates. It is the default immutable IndexedSeq, which we can see if we create one.

scala.collection.immutable.IndexedSeq(1, 2, 3)
// res5: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3)

Vectors are a good choice if you want both random access and immutability.

10.1.3 Mutable Implementations

The mutable collections are probably more familiar. In addition to linked lists and arrays (which we discuss in more detail later) there are buffers, which allow for efficient construction of certain data structures.

10.1.3.1 Buffers

Buffers are used when you want to efficiently create a data structure an item at a time. An ArrayBuffer is an IndexedSeq which also has constant time appends. A ListBuffer is like a List with constant time prepend and append (though note it is mutable, unlike List).

Buffers’ add methods to support destructive prepends and appends. For example, the += is destructive append.

val buffer = new scala.collection.mutable.ArrayBuffer[Int]()
// buffer: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer()

buffer += 1
// res6: buffer.type = ArrayBuffer(1)

buffer
// res7: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(1)

10.1.3.2 StringBuilder

A StringBuilder is essentially a buffer for building strings. It is mostly the same as Java’s StringBuilder except that it implements standard Scala collections method where there is a conflict. So, for example, the reverse method creates a new StringBuilder unlike in Java.

10.1.3.3 LinkedLists

Mutable singly LinkedLists and DoubleLinkedLists work for the most part just like List. A DoubleLikeList maintains both a prev and next pointer and so allows for efficient removal of an element.

10.2 Arrays and Strings

Arrays and strings in Scala correspond to Java’s arrays and strings.

"this is a string"
// res0: String = this is a string

Yet all the familiar collection methods are available on them.

"is it true?".map(elt => true)
// res1: scala.collection.immutable.IndexedSeq[Boolean] = Vector(true, true, true, true, true, true, true, true, true, true, true)

Array(1, 2, 3).map(_ * 2)
// res2: Array[Int] = Array(2, 4, 6)

This conversion is done automatically using implicit conversions. There are two conversions. The Wrapped conversions (WrappedArray and WrappedString) wrap the original array or string in an object supporting the Seq methods. Operations on such a wrapped object return another wrapped object.

val sequence = new scala.collection.immutable.WrappedString("foo")
// sequence: scala.collection.immutable.WrappedString = foo

sequence.reverse
// res3: scala.collection.immutable.WrappedString = oof

The Ops conversions (ArrayOps and StringOps) add methods that return an object of the original type. Thus these objects are short-lived.

val sequence = new scala.collection.immutable.StringOps("foo")
// sequence: scala.collection.immutable.StringOps = foo

sequence.reverse
// res4: String = oof

The choice of conversion is based on the required type. If we use a string, say, where a Seq is expected the string will be wrapped. If we just want to use a Seq method on a string then an Op conversion will be used.

val sequence: Seq[Char] = "foo"
// sequence: Seq[Char] = foo

sequence.getClass
// res5: Class[_ <: Seq[Char]] = class scala.collection.immutable.WrappedString

10.2.1 Performance

You might be worried about the performance of implicit conversions. The Ops conversions are normally optimised away. The Wrapped conversions can give a small performance hit which may be an issue in particularly performance sensitive code.

10.3 Iterators and Views

Iterators and views are two parts of the collection library that don’t find much use outside of a few special cases.

10.3.1 Iterators

Scala’s iterators are like Java’s iterators. You can use them to walk through the elements of a collection, but only once. Iterators have hasNext and next methods, with the obvious semantics. Otherwise they behave like sequences, though they don’t inherit from Seq.

Iterators don’t find a great deal of use in Scala. Two primary use cases are operating on collections that are too large to fit in memory or in particularly high performance code.

10.3.2 Views

When performing a sequence of transformations on a collection, a number of intermediate collections will be constructed. For example, in the below example two intermediate collections will be created by the first and second call to map.

Seq(1, 2, 3).map(_ * 2).map(_ + 4).map(_.toString)
// res0: Seq[String] = List(6, 8, 10)

It is as if we’d written

val intermediate1 = Seq(1, 2, 3).map(_ * 2)
val intermediate2 = intermediate1.map(_ + 4)
val result = intermediate2.map(_.toString)

These intermediate collections are not strictly necessary. We could instead do the full sequence of transformations on an element-by-element basis. Views allows this. We create a view by calling the view method on any collection. Any traversals of a view are only applied when the force method is called.

val view = Seq(1, 2, 3).view.map(_ * 2).map(_ + 4).map(_.toString)
// view: scala.collection.SeqView[String,Seq[_]] = SeqViewMMM(...)

view.force
// res1: Seq[String] = List(6, 8, 10)

Note that when a view is forced the original type is retained.

For very large collections of items with many stages of transformations a view can be worthwhile. For modest sizes views are usually slower than creating the intermediate data structures.

10.4 Traversable and Iterable

So far we’ve avoided discussing the finer details of the collection class hierarchy. As we near the end of this section it is time to quickly go over some of the intricacies.

10.4.1 Traversable

The trait Traversable sits at the top of the collection hierarchy and represents a collection that allows traversal of its contents. The only abstract operation is foreach. Most of the collection methods are implemented in Traversable, though classes extending it may reimplement methods for performance.

10.4.1.1 TraversableOnce

TraversableOnce represents a collection that can be traversed one or more times. It is primarily used to reduce code duplication between Iterators and Traversable.

10.4.2 Iterable

Iterable is the next trait below Traversable. It has a single abstract method iterator that should return an Iterator over the collection’s contents. The foreach method is implemented in terms of this. It adds a few methods to Traversable that can only be efficiently implemented when an iterator is available.

10.5 Java Interoperation

The prefered way to convert between Scala and Java collections is use the JavaConverters implicit conversions. We use it by importing scala.collection.JavaConverters._ and then methods asJava and asScala become available on many of the collections.

import scala.collection.JavaConverters._
Seq(1, 2, 3).asJava
// res0: java.util.List[Int] = [1, 2, 3]

Java does not distinguish mutable and immutable collections at the type level but the conversions do preserve this property by throwing UnsupportOperationException as appropriate.

val javaCollection = Seq(1, 2, 3).asJava
// javaCollection: java.util.List[Int] = [1, 2, 3]
javaCollection.set(0, 5)
// java.lang.UnsupportedOperationException
//  at java.util.AbstractList.set(AbstractList.java:115)
//     ...

The conversions go the other way as well.

val list: java.util.List[Int] = new java.util.ArrayList[Int]()
// list: java.util.List[Int] = []

list.asScala
// res5: scala.collection.mutable.Buffer[Int] = Buffer()

Note that the Scala equivalent is a mutable collection. If we mutate an element we see that the underlying Java collection is also changed. This holds for all conversions; they always share data and are not copied.

list.asScala += 5
// res6: scala.collection.mutable.Buffer[Int] = Buffer(5)

list
// res7: java.util.List[Int] = [5]

10.5.1 JavaConversions

There is another set of conversions in scala.collection.JavaConversions, which perform conversions without needing the calls to asJava or asScala. Many people find this confusing in large systems and thus it is not recommended.

10.6 Mutable Sequences

Most of the interfaces we’ve have covered so far do not have any side-effects—like the copy method on a case class, they return a new copy of the sequence. Sometimes, however, we need mutable collections. Fortunately, Scala provides two parallel collections hierarchies, one in the scala.collection.mutable package and one in the scala.collection.immutable package.

The default Seq is defined to be scala.collection.immutable.Seq. If we want a mutable sequence we can use scala.collection.mutable.Seq.

val mutableCollection = scala.collection.mutable.Seq(1, 2, 3)
// mutableCollection: scala.collection.mutable.Seq[Int] = ArrayBuffer(1, 2, 3)

Note that the concrete implementation class is now an ArrayBuffer and not a List.

10.6.1 Destructive update

In addition to all the methods of an immutable sequence, a mutable sequence can be updated using the update method. Note that update returns Unit, so no value is printed in the REPL after this call. When we print the original sequence we see it is changed:

mutableCollection.update(0, 5)
mutableCollection
// res1: scala.collection.mutable.Seq[Int] = ArrayBuffer(5, 2, 3)

A more idiomatic way of calling update is to use assignment operator syntax, which is another special syntax built in to Scala, similar to infix operator syntax and function application syntax:

mutableCollection(1) = 7
mutableCollection
// res3: scala.collection.mutable.Seq[Int] = ArrayBuffer(5, 7, 3)

10.6.2 Immutable methods on mutable sequences

Methods defined on both mutable and immutable sequences will never perform destructive updates. For example, :+ always returns a new copy of the sequence without updating the original:

val mutableCollection = scala.collection.mutable.Seq[Int](1, 2, 3)
// mutableCollection: scala.collection.mutable.Seq[Int] = ArrayBuffer(1, 2, 3)

mutableCollection :+ 4
// res4: scala.collection.mutable.Seq[Int] = ArrayBuffer(1, 2, 3, 4)

mutableCollection
// res5: scala.collection.mutable.Seq[Int] = ArrayBuffer(1, 2, 3)

10.6.2 Using Mutable Collections Safely

Scala programmers tend to favour immutable collections and only bring in mutable ones in specific circumstances. Using import scala.collection.mutable._ at the top of a file tends to create a whole series of naming collisions that we have to work around.

To work around this, I suggest importing the mutable package itself rather than its contents. We can then explicitly refer to any mutable collection using the package name as a prefix, leaving the unprefixed names referring to the immutable versions:

import scala.collection.mutable

mutable.Seq(1, 2, 3)
// res6: scala.collection.mutable.Seq[Int] = ArrayBuffer(1, 2, 3)

Seq(1, 2, 3)
// res7: Seq[Int] = List(1, 2, 3)

10.6.3 In summary

Scala’s collections library includes mutable sequences in the scala.collection.mutable package. The main extra operation is update:

Method We have We provide We get

update

Seq[A]

Int, A

Unit

10.6.4 Exercises

10.6.4.1 Animals

Create a Seq containing the Strings "cat", "dog", and "penguin". Bind it to the name animals.

val animals = Seq("cat", "dog", "penguin")
// animals: Seq[String] = List(cat, dog, penguin)

Append the element "tyrannosaurus" to animals and prepend the element "mouse".

"mouse" +: animals :+ "tyrannosaurus"
// res8: Seq[String] = List(mouse, cat, dog, penguin, tyrannosaurus)

What happens if you prepend the Int 2 to animals? Why? Try it out… were you correct?

The returned sequence has type Seq[Any]. It is perfectly valid to return a supertype (in this case Seq[Any]) from a non-destructive operation.

2 +: animals

You might expect a type error here, but Scala is capable of determining the least upper bound of String and Int and setting the type of the returned sequence accordingly.

In most real code appending an Int to a Seq[String] would be an error. In practice, the type annotations we place on methods and fields protect against this kind of type error, but be aware of this behaviour just in case.

Now create a mutable sequence containing "cat", "dog", and "penguin" and update an element to be an Int. What happens?

If we try to mutate a sequence we do get a type error:

val mutable = scala.collection.mutable.Seq("cat", "dog", "penguin")
// mutable: scala.collection.mutable.Seq[String] = ArrayBuffer(cat, dog, penguin)

mutable(0) = 2
// <console>:9: error: type mismatch;
//  found   : Int(2)
//  required: String
//               mutable(0) = 2
//                            ^

  1. これは完全に正しくはありません。Scala コードを実行するプログラムである Java 仮想マシンは2種類のオブジェクトを識別します。プリミティブ型は、値の表現と一緒にどんな型の情報も保持しません。オブジェクト型は型の情報を保持します。しかし、この型の情報は完全ではなく失われる場合もあります。それゆえに、コンパイル時と実行時の間にある区別を曖昧にすることは危険です。実行時にある型の情報を頼りにしない(本書ではそういうパターンを明らかにしていきます。)のであれば、それらの問題に遭遇することはないでしょう。

  2. パターンマッチングと呼ばれるオブジェクトを作用させる別の方法があります。パターンマッチングはのちほど紹介します。

  3. 副作用とは正確には何でしょうか?ひとつの実用的な定義は、間違った結果を置換によって生じさせてしまう何かのことです。副作用がなければ、置換は必ず機能するのでしょうか?Scala の本当に正しいモデルを示すには、置換を適用する順番を定義する必要があります。いくつかの考えうる順番があります。(例えば、置換を左から右へ実行するのか、右から左へ実行するのか?置換をできるだけ早くするのか、値が必要になるまで遅延するのか?)ほとんどいつも置換の順番は問題になりませんが、それが問題になる場合もあります。Scala はいつも「左から右へ」「できるだけ早く」置換を適用します。

  4. 実際には AnyVal の派生型を定義でき、それは値クラスとして知られています。これらはいくつかの特殊な状況で有用なのですが、ここでは議論しません。

  5. 実際のところ、パターンは逐次的な検証より効率的な形にコンパイルされますが、それが意味するところは変わりません。

  6. オブジェクトリテラルの演習で見たように、これはすべて統一アクセス原理 (uniform access principle) の一部です。

  7. We actually can define data in this manner if we delay the construction of the recursive case, like final case class LazyList(head: Int, tail: () => LazyList). This uses a feature of Scala, functions, that we haven’t seen yet. We can do some fairly mind-bending things with this construction, such as defining an infinite stream of ones with the declaration val ones: LazyList = LazyList(1, () => ones). Since we only ever realise a finite amount of this list we can use it to implement certain types of data that would be difficult to implement in other ways. If you’re interested in exploring this area further, what we have implemented in called a lazy list, and an “odd lazy list” in particular. The “even list”, described in How to add laziness to a strict language wihtout even being odd, is a better implementation. If you wish to explore further, there is a rich literature on lazy datastructures and more mind melting theory under the name of “coinductive data”.

  8. The traditional name this element is a Cons cell. We don’t use this name as it’s a bit confusing if you don’t know the story behind it.

  9. This is how Scala’s built-in List data structure works. We will be introduced to List in the chapter on Collections.

  10. Note that we only can drop the parentheses around the argument list on single-argument functions—we still have to write () => foo and (a, b) => foo on functions of other arities.

  11. The term “syntactic sugar” is used to refer to convenience syntax that is not needed but makes programming sweeter. Operator syntax is another example of syntactic sugar that Scala provides.

  12. There is a little bit more to being a functor or monad. For a monad we require a constructor, typically called point, and there are some algebraic laws that our map and flatMap operations must obey. A quick search online will find more information on monads, or they are covered in more detail in our book on Scala with Cats.

  13. Type enrichment is sometimes referred to as pimping in older literature. We will not use that term.