4. Scala in CS1

Warning

This section is still in draft form but is nearly complete in terms of examples, subject to editing. There might still be a few rough spots. Comments welcome to gkt@cs.luc.edu.

This is an elaboration of our Google presentation slides: http://goo.gl/Q68fA.

4.1. Motivating Scala in CS1

  • Programming in the small
  • REPL - Read-Evaluate-Print-Loop
  • Scripting Environment
  • Libraries allow for interesting code (JVM)
  • Static type checking
  • Uniform syntax
  • Everything is a method call
  • Powerful collections

Our motivation is to have the best of both worlds:

  • Concise script-ability of languages like Python and Ruby
  • Static type checking in the small makes it (generally) easy to fix compilation errors

The emerging worksheet model makes it possible to give students many examples that just work and can be adapted to solve new problems.

4.2. Getting Started

Like many agile languages, Scala embraces the notion of being able to get started using a REPL (Read-Evaluate-Print-Loop), which allows for interactive execution and spontaneous feedback.

We’re assuming the reader can set up Scala and Java (needed to run Scala, a language that targets the JVM primarily). Once you have Scala and Java installed, you can open up an interactive session using the command line. (For those who don’t prefer the command line, especially on Windows, we recommend installing IntelliJ Community Edition and the Scala plug-in. This will allow you to get an interactive session as well.)

4.2.1. Hello, World

$ scala-2.10
Welcome to Scala version 2.10.3 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_45).
Type in expressions to have them evaluated.
Type :help for more information.

scala> println("Hello, World")
Hello, World

4.2.2. Types - “Lack” of Primitives

scala> 1
res1: Int = 1

In the following, the user types 1. and then hits the <tab> key–known as tab completion–to show the available operations and methods available for an Int.

scala> 1.<tab>
%              &              *              +              -
/              >              >=             >>             >>>
^              asInstanceOf   isInstanceOf   toByte         toChar
toDouble       toFloat        toInt          toLong         toShort
toString       unary_+        unary_-        unary_^        |

4.2.3. Literals are mostly what you expect

Integers

scala> val a = 95
a: Int = 95

scala> val b = -95
b: Int = -95

Octal and Hexadecimal

scala> val c = 039
<console>:1: error: malformed integer number
       val c = 039
               ^

scala> val c = 037
warning: there were 1 deprecation warning(s); re-run with -deprecation for details
c: Int = 31

scala> val d = 0xff
d: Int = 255

scala> val d = 0xffac
d: Int = 65452

Double and Float

scala> val e = 1.34e-7
e: Double = 1.34E-7

scala> val f = 1.34e-7f
f: Float = 1.34E-7

4.2.4. Statements vs. Expressions

Scala eschews statements as a general rule in favor of expressions (which produce values), owing to its functional heritage, which emphasizes the composition of functions to effect computation.

In practice, however, many expressions, however, behave like statements as they produce Unit as a result.

A notable consequence of this language decision is that some constructs are not provided in Scala, including the familiar break and continue (found in loops).

4.2.5. Everything is an Object

Scala is not designed in a vacuum. Its heritage is one shared with Java, even if its ideas have value above and beyond Java. Everything is achieved in Scala with objects, although you can do a tremendous amount of programming before ever writing a class per se.

There are two consequences to this decision:

  • There are no primitive types per se. Every one of the literals discussed previously is an object. This eliminates the need to distinguish int from Integer (a major pain point for Java prior to boxing/unboxing support).
  • There are very few keywords in Scala. Even when you encounter something that you think is a keyword, it is often just a method call. A really good example is the concept of mapping (map) from functional programming, which is a method common to just about every Scala collection.

Let’s look at a couple of examples of methods. Here is an example of how to see the methods available to an Int instance:

scala> a.<tab>
%              &              *              +              -
/              >              >=             >>             >>>
^              asInstanceOf   isInstanceOf   toByte         toChar
toDouble       toFloat        toInt          toLong         toShort
toString       unary_+        unary_-        unary_^        |

Where you see <tab>, you can use a feature known as tab completion in the REPL to see the options available to value a. You’ll notice one thing immediately, especially if you are familiar with Java: operator overloading! That’s right, every operator is a method.

Let’s use the + method:

scala> a.+(3)
res5: Int = 98

scala> a + 3
res6: Int = 98

Let’s convert an Int to a Float:

scala> val b = a.toFloat
b: Float = 95.0

You can also invoke methods like toFloat (which take no parameters) without using dots. We will take advantage of this syntax as part of good Scala style (in many of our examples).

scala> val b = a.toFloat
b: Float = 95.0

We’re going to look at more advanced objects later (Scala collections) but this should give you a taste of what is possible.

4.2.6. Tuples

A language feature that was popularized (but not invented) by the Python language are tuples. Tuples eliminate the need for unwanted uses of a class for grouping multiple values. Let’s take a quick look.

scala> val t = (3, 4)
t: (Int, Int) = (3,4)

scala> val u = (3.0, 4.0)
u: (Double, Double) = (3.0,4.0)

scala> val v = (3.0, 4)
v: (Double, Int) = (3.0,4)

Notice that Scala infers the type of each one of these value definitions.

When working with a tuple, you’re really working with an object. You can inspect the methods that are available using the tab completion method shown previously.

scala> t.
_1                _2                asInstanceOf      canEqual
copy              isInstanceOf      productArity      productElement
productIterator   productPrefix     swap              toString

scala> u.
_1                _2                asInstanceOf      canEqual
copy              isInstanceOf      productArity      productElement
productIterator   productPrefix     swap              toString

scala> v.
_1                _2                asInstanceOf      canEqual
copy              isInstanceOf      productArity      productElement
productIterator   productPrefix     swap              toString

You can inspect the components of a tuple by using the _1, _2, ... methods.

scala> t._1
res9: Int = 3

scala> t._2
res10: Int = 4

You’ll obviously not want to use these names to refer to the components of a tuple. Using pattern matching, you can extract the components of a tuple and bind them to proper names. For example, if your tuple represents an (x, y) pair, you are likely to use a match expression like this:

scala> t match { case (x, y) => println(s"($x, $y)") }
(3, 4)

4.2.7. Semicolon Inferencing

Python programmers already know and love not having to deal with semicolons. Scala follows this excellent practice as well.

You can do this:

scala> val x = 30;
x: Int = 30

But this works just as well and is the preferred way to write Scala code:

scala> val y = 30
y: Int = 30

4.2.8. Simple Input (and Output)

Much like Python and Java, import can be used to experiment with library objects and functions, even before you know how to create classes:

scala> import scala.tools.jline.console.ConsoleReader

scala> val input = new ConsoleReader
input: scala.tools.jline.console.ConsoleReader = scala.tools.jline.console.ConsoleReader@3ec642e5

Then you can inspect the capabilities of the ConsoleReader by using tab completion (as shown before)

scala> input.r<tab>
readCharacter     readLine          readVirtualKey    redrawLine
removeCompleter   replace           resetPromptLine   restoreLine

Now we look up more details on the readLine() methods, which is what we want to do basic, line-oriented input (a common need in introductory teaching).

scala> input.readLine<tab>

def readLine(): String
def readLine(Character): String
def readLine(String): String
def readLine(String, Character): String

scala> val data = input.readLine("Please enter some text: ")
Please enter some text: Hello, World
data: String = Hello, World

You’ll probably find it necessary to read through the Scala documentation, but in a number of cases, the behavior is similar to what you’ve seen in other language APIs. readLine() is pretty well known in Java circles. As you can see above, readLine(String) gives us what we want: the ability to read input with a prompt of sorts.

4.2.9. val vs. var

  • Values (keyboard val) are used for immutable storage.
  • Variables (keyword var) are used for mutable storage.
  • You can think of this as the reemergence of const but it takes on a more powerful and predictable form in Scala than other languages that preceded it.
  • Scala thinking prefers val to var. So do we.
  • Interesting

4.2.10. Scripts and Worksheets

Similar to modern scripting languages (e.g. Python and Ruby) and the original shell, you can create a Scala script in a file, e.g. myscript.scala and run the script using the scala command.

$ scala myscript.scala

You can also load the script within the Scala REPL:

$ scala
scala> :load myscript.scala

We’ll be taking advantage of this a bit more in our discussion about sbt, the Scala Build Tool.

4.2.11. Conditional and Functions

Functional → expressions

if expression

scala> val a = 25
a: Int = 25

scala> val b = 30
b: Int = 30

scala> val max = if (a > b) a else b
max: Int = 30

Contrast with:

scala> var max = 0
max: Int = 0

scala> if (a > b)
     |   max = a

scala> if (a > b) {
     |    max = a
     | } else {
     |    max = b
     | }

scala> max
res4: Int = 30

Note: Similar to other agile languages, you can enter compound statements and blocks of code in the REPL. The | is a continuation character to indicate that more input is expected. It’s often best to use a text editor once you start entering more complex fragments of code (especially more complicated than what you see here).

Functions are front and center when it comes to Scala programming. Although object-functional, a pure function can be written without the boilerplate associated with OOP. We’re proponents of OOP but prefer to introduce functional thinking and use of objects prior to creating classes.

scala> def square(x : Int) = x * x
square: (x: Int)Int


scala> square(25)
res6: Int = 625

scala> square(25.0)
<console>:12: error: type mismatch;
 found   : Double(25.0)
 required: Int
              square(25.0)
                     ^

As expected, the second invocation of square() results in an error. Scala performs static type checking in real time. That is, this is not a run-time check.

4.2.12. Function literals

Let’s build on the square() example to see how easy it is to generate the first n squares. We’ll use this to show how you can use functions as parameters and to sensitize the use of Scala function literals, which are used rather idiomatically in Scala programming.

We’ll start by generating the first 25 values using a Scala range.

scala> val n = 10
n: Int = 10

scala> val first_n = 1 to n
first_n: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

This shows how to map the square() function to the range of values.

scala> first_n.map(square)
res16: scala.collection.immutable.IndexedSeq[Int] =
   Vector(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)

This shows how to map a function literal to the range of values.

scala> first_n map (n => n * n)
res17: scala.collection.immutable.IndexedSeq[Int] =
   Vector(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)

This shows how you can combine a function literal with a previously defined function:

scala> first_n map (n => square(n))
res18: scala.collection.immutable.IndexedSeq[Int] =
   Vector(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)

You can avoid having to name arguments in function literals using the _ parameter. This syntax is a bit awkward to new programmers (and therefore should be introduced gently in CS1 courses) but allows for concise (and sometimes clearer) expression, especially when used in a disciplined way.

Consider this code that creates the first n even numbers:

scala> 1 to 10 map (_ * 2)
res26: scala.collection.immutable.IndexedSeq[Int] =
   Vector(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

You might be tempted to try this by doing the following:

scala> 1 to 10 map (_ + _)
<console>:11: error: wrong number of parameters; expected = 1
              1 to 10 map (_ + _)

Alas, it doesn’t work. Why? Because each occurrence of _ corresponds to an expected parameter. In this case, there would have to be pairs of values. Unfortunately, in the range 1 to 10, each value is an Int.

Default parameters and named parameters

Similar to other agile languages, Scala allows you to specify default parameter values. This is particularly useful, especially when diving into object-oriented programming, but has uses even before then.

Consider this version of square():

scala> def square( x : Int = 0) = x * x
square: (x: Int)Int

scala> square()
res28: Int = 0

scala> square(5)
res29: Int = 25

Here we are creating a version that has a default value of zero, if the caller doesn’t specify x. (This is not necessarily intended to be pedagogically interesting but is effective, considering we spent most of our time in this section looking at the square() function!)

4.3. Early Collection, Arrays and Lists

Scala has some very advanced and comprehensive collections libraries. For CS1 you can focus only on the simplest of these, beginning with Arrays and Lists. In actually makes sense to introduce these before loops (and iteration in general) in Scala for two reasons. First, the collections have many methods that allow you to do standard tasks that would often go in loops. As such, Scala code often doesn’t include all that many loops. Second, the for loop in Scala is really a foreach type loop that works with collections so it makes more sense to introduce it after the basic collections.

4.3.1. Making Arrays and Lists

The simplest way to make an Array or a List is with the following syntax.

scala> Array(1,2,3,4)
res0: Array[Int] = Array(1, 2, 3, 4)

scala> List(1.3,4.2,3.14)
res1: List[Double] = List(1.3, 4.2, 3.14)

Note that the Array and List types take type parameters. These appear in square brackets. It is standard across the Scala language that value arguments appear in parentheses, type arguments appear in square brackets, and curly braces are used to denote code blocks for complex expressions or statements. Also note that as before, Scala’s type inference figures out the types for these expressions.

Note

Technically these are making calls to the apply methods of the Array and List companion objects. The details of how that works don’t really matter in CS1, but you can find them in the section on object-orientation under CS2. We cover this topic later in our discussion of classes and objects.

You can also made longer arrays using the new syntax that one uses in Java.

scala> new Array[Int](100)
res2: Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

Here we need to specify the type and say how big the Array is. Array objects in Scala are implemented as Java arrays and they have the same general properties. Like Java Arrays, they are mutable and they can’t dynamically change their size. Note that this approach to making Arrays is not the one typically used in Scala because if the type is something like String you get the same behavior you have in Java and all the values are initialized to null. Later in this section we will see the fill and tabulate methods, which are much less likely to produce a NullPointerException and are generally favored over this style of creating a large Array.

Lists can be built using the :: operator. We read :: as cons, following the tradition of Scheme (Lisp and ML. :: is a right associative operator so it takes a value on the left and a List on the right. For example:

scala> 1::2::3::4::Nil
res4: List[Int] = List(1, 2, 3, 4)

The Nil object is a short name for an empty List. You can also use List() (the empty list) in place of Nil. Note that the List type is a singly-linked, immutable list. So prepending as shown above is an efficient operation. Also note that you can’t change values without making a new List because it is immutable.

This form of building a List with :: is probably most beneficial with recursive functions and works very well in situations where you don’t know how many values you will be working with.

scala> def readNumbers(n:Int):List[Int] = if(n<1) Nil else
     | readInt :: readNumbers(n-1)
readNumbers: (n: Int)List[Int]

scala> readNumbers(5)
1
2
3
4
5
res6: List[Int] = List(1, 2, 3, 4, 5)

scala> def readUntilQuit():List[Int] = {
     | val data = readLine
     | if(data!="quit") data.toInt :: readUntilQuit() else Nil
     | }
readUntilQuit: ()List[Int]

scala> readUntilQuit()
1
2
3
4
5
quit
res7: List[Int] = List(1, 2, 3, 4, 5)

4.3.2. Indexing/Traversing Arrays and Lists

You can get to values in an Array or a List by indexing with parentheses.

scala> val arr = Array(6,8,2,4,1)
arr: Array[Int] = Array(6, 8, 2, 4, 1)

scala> val lst = List('g','e','o','r','g','e')
lst: List[Char] = List(g, e, o, r, g, e)

scala> arr(2)
res1: Int = 2

scala> lst(2)
res2: Char = o

The use of parentheses instead of square brackets keeps the syntax consistent. (Having parentheses is the next best thing to square brackets as well. It is certainly preferable to having an explicit method call as found in Java to access elements in the array. We also note that the well-known math/science focused programming language, FORTRAN, uses parentheses for subscripting!)

Neither the Array type, nor the List type have any special place in the Scala syntax. They are both handled in the programming language just like any other library code. The compiler does optimize Arrays to Java bytecode arrays, but that is an implementation detail beyond the scope of our workshop.

You can also use the syntax you would expect to set the values of elements in an Array. You can’t do this for Lists because they are immutable.

scala> arr(2) = 99

scala> arr
res4: Array[Int] = Array(6, 8, 99, 4, 1)

The common way to run through a List is using the head and tail methods. The head method returns the first element of the List while tail returns the last remaining after the first element. (Note that these can be called on an Array as well, but tail will be very inefficient as it builds a new, shorter Array.) The use of head and tail is particularly well suited for recursion.

scala> def sumList(lst:List[Int]):Int = if(lst.isEmpty) 0 else
     | lst.head+sumList(lst.tail)
sumList: (lst: List[Int])Int

scala> sumList(List(1,2,3,4,5))
res5: Int = 15

This same function can be defined using a match and patterns in the following way.

scala> def sumList(lst:List[Int]):Int = lst match {
     | case Nil => 0
     | case h::t => h + sumList(t)
     | }
sumList: (lst: List[Int])Int

4.3.3. Standard Methods

The collection types in Scala have a large number of methods defined on them. We won’t list them here. Instead, you can look in the [ScalaAPI] for the names and details. This is what you get when you use tab completion in the REPL on a List.

scala> lst.<tab>
++                   ++:                  +:
/:                   /:\                  :+
::                   :::                  :\
addString            aggregate            andThen
apply                applyOrElse          asInstanceOf
canEqual             collect              collectFirst
combinations         companion            compose
contains             containsSlice        copyToArray
copyToBuffer         corresponds          count
diff                 distinct             drop
dropRight            dropWhile            endsWith
exists               filter               filterNot
find                 flatMap              flatten
fold                 foldLeft             foldRight
forall               foreach              genericBuilder
groupBy              grouped              hasDefiniteSize
head                 headOption           indexOf
indexOfSlice         indexWhere           indices
init                 inits                intersect
isDefinedAt          isEmpty              isInstanceOf
isTraversableAgain   iterator             last
lastIndexOf          lastIndexOfSlice     lastIndexWhere
lastOption           length               lengthCompare
lift                 map                  mapConserve
max                  maxBy                min
minBy                mkString             nonEmpty
orElse               padTo                par
partition            patch                permutations
prefixLength         product              productArity
productElement       productIterator      productPrefix
reduce               reduceLeft           reduceLeftOption
reduceOption         reduceRight          reduceRightOption
removeDuplicates     repr                 reverse
reverseIterator      reverseMap           reverse_:::
runWith              sameElements         scan
scanLeft             scanRight            segmentLength
seq                  size                 slice
sliding              sortBy               sortWith
sorted               span                 splitAt
startsWith           stringPrefix         sum
tail                 tails                take
takeRight            takeWhile            to
toArray              toBuffer             toIndexedSeq
toIterable           toIterator           toList
toMap                toSeq                toSet
toStream             toString             toTraversable
toVector             transpose            union
unzip                unzip3               updated
view                 withFilter           zip
zipAll               zipWithIndex

You might notice that there is a sum method, so the listSum written above is something you never need to write. There are also methods for converting between types such at toList and toArray.

4.3.4. Higher Order Methods

Some of the methods in that list are worth describing instead of leaving it to the reader to explore the API. In particular, the most commonly used higher- order methods, like map and filter, are used to a significant extent in Scala programming. The foreach method is also helpful in a number of situations.

Higher-order methods are methods that take or return functions. In this situation, we are considering methods that take a function as an argument. The filter method, for example, takes a function that takes one argument of the type of the collection and returns a Boolean. That function is called on every element of the collection. A new collection is produced that contains only the elements for which the function returned true. To see how this is used, consider the following code where we filter a List of Ints in two different ways.

scala> val lst = List(6,4,9,1,2,8,3,7)
lst: List[Int] = List(6, 4, 9, 1, 2, 8, 3, 7)

scala> lst.filter(_ < 6)
res8: List[Int] = List(4, 1, 2, 3)

scala> lst.filter(_ % 2 == 0)
res9: List[Int] = List(6, 4, 2, 8)

Both examples use the shorthand underscore syntax for writing the lambda expressions/function literals. They could have been written using the longer syntax as n => n<6 and n => n%2 == 0 respectively.

Another higher-order method that is used extensively is the map method (one of our favorites). The argument passed into map is a function that takes the type of the collection and returns something. The return type could be the same as the input type, but it could be different. The map method returns a new collection of the return type of the function. A first example will use the list of integers from above and make a collection that contains values that are twice the original values.

scala> lst.map(_*2)
res10: List[Int] = List(12, 8, 18, 2, 4, 16, 6, 14)

To show that the types can be different, this second example shows mapping Strings to their length.

scala> val words = "This is a sentence with words".split(" ")
words: Array[String] = Array(This, is, a, sentence, with, words)

scala> words.map(_.length)
res0: Array[Int] = Array(4, 2, 1, 8, 4, 5)

The foreach method takes a function and simply executes the function on all the elements of the collection. While map and filter return new collections, foreach is used for its side effects and returns Unit.

scala> words.foreach(println)
This
is
a
sentence
with
words

There are other higher-order methods that are very accessible for CS1 students such as count, exists, and forall, all of which take predicate functions on a single argument. There are more complex methods like the reduce methods and fold methods that can be very helpful, but which are more challenging for CS1 students to understand. We use these occasionally in this workshop but recommmend that they be used sparingly and with an emphasis on clarity (as opposed to conciseness).

4.3.5. Currying and By-Name Arguments

The best ways to create large Arrays and Lists in Scala are with the fill and tabulate methods. These use some syntax that needs to be introduced. The first element of new syntax is that these methods are curried. This means that the arguments are broken across multiple argument lists. So instead of having f(x,y) you would have f(x)(y). This is a common feature of functional programming languages and it is done to allow functions to be partially applied. (A partial application means that you apply the function to a subset of the arguments, which results in a new function of the remaining arguments.) The syntax for defining a curried function in Scala is fairly straightforward.

scala> def currySum(x:Int)(y:Int) = x+y
currySum: (x: Int)(y: Int)Int

scala> currySum(3)(5)
res2: Int = 8

scala> val plus3 = currySum(3)_
plus3: Int => Int = <function1>

scala> plus3(5)
res3: Int = 8

The need for the underscore in the partially applied call removes ambiguity in the Scala syntax. Often functions are curried in Scala to help with type inference or to simplify the syntax for calling them.

The second new element of the syntax is pass-by-name arguments. By default, variables in Scala are passed in a manner similar to Java. The variables are references and the references are passed by-value. With pass-by-name semantics, the code is passed more like a function instead of by-value. Instead of being evaluated at the call point, the code is wrapped in a “thunk” (a term that originated with Algol 60). Every time the variable is used, the code is executed. We specify that a parameter is pass-by-name using a rocket with no arguments.

scala> def multiplyThrice(x: => Double) = x*x*x
multiplyThrice: (x: => Double)Double

scala> var i = 5
i: Int = 5

scala> multiplyThrice(i)
res5: Double = 125.0

scala> multiplyThrice({i += 1; i})
res6: Double = 336.0

The first invocation of multiplyThrice does what one would expect, even though the argument is passed by-name.

We use call-by-name for timing blocks of code in the Monte Carlo Pi example later in this section. See Monte Carlo Calculation.

4.3.6. Fill and Tabulate

The fill method, which is defined on the Array object, is...

4.4. Recursion for Iteration

scala> def sum(n : Int) : Int = if (n <= 0) 0 else n + sum(n-1)
sum: (n: Int)Int

scala> sum(0)
res0: Int = 0

scala> sum(1)
res1: Int = 1

scala> sum(2)
res2: Int = 3

scala> sum(3)
res3: Int = 6

scala> sum(100)
res4: Int = 5050

Recursion does take awhile to master, but much of the trouble students have with it in practice is a consequence of side-effect-full thinking. Nevertheless, you can replace this with a more imperative (von Neumann) style. See our loops section.

4.5. Some CS1 Friendly Examples

4.5.1. GCD

This is based on a classic algorithm by Euclid and one that is covered in many introductory computer science courses. (See [GCD] for a more detailed discussion of this method and the ways it has been coded over the years, especially in imperative programming languages.)

def gcd(a : Int, b : Int) = if (b == 0) a else gcd(b, a % b)

The Scala version looks a lot like Euclid’s definition!

4.5.2. Factorial

This is the basic example that comes from [MiscExplorationsScala], which also show show to use Scala and functional programming to rework explicitly recursive computations into non-recursive versions using higher-order functional thinking.

This shows how to explicitly define the fac(n : Int) : Int function.

def fac(n : Int) : Int = if (n <= 0) 1 else n * fac(n - 1)

This shows how to define a function object. It makes use of the literal syntax we introduced earlier, You read this as “fac is a function that maps Int into Int and binds the name n to the Int parameter.

val fac: Int => Int = n => if (n <= 0) 1 else n * fac(n - 1)

In many cases, you can write these functions without explcit recursion, simply by taking advantage of Scala’s innate collections:

def fac(n : Int) : Int = (1 to n).foldLeft(1)((l,r) => l * r)

Or even more concisely (and cryptically?)

def fac(n : Int) : Int = (1 to n).foldLeft(1)(_ * _)

We’ll look more at higher-order thinking shortly. Many who think of functional programming (especially the 1960’s and 1970’s style) tend to think Lisp, which very much required you to think recursively most of the time in practice. Today, the recursive element is still there, but you’ll find that the need to use it explicitly is not required and can be replaced with higher-order abstractions. In the end, students still need to know about this technique, if only to understand that certain methods (e.g. folds) often have a recursive nature to them.

In addition to rather outdated notions many tend to have about functional programming when it comes to recursion, the optimization of recursive calls is achieved through a version of tail-recursion elimination (a.k.a. tail call optimization). See [TailCalls] for more information.

4.5.3. Monte Carlo \(\pi\) Calculation

An example that often resonates well with students is the Monte Carlo method, which uses randomness to perform the \(\pi\) computation. In the interests of showing how Scala’s approach to functional programming follows the textual description, we will write the steps and show the code (in Scala) to perform each step in an integrated fashion.

In the Monte Carlo method for calculating \(\pi\), we will randomly generate a given number of darts using a Scala stream. We fire the darts into a unit circle, which is bounded by a square, whose dimensions are \(2 \times 2\) units. The darts that fall within the unit circle satisfy the constraint \(x^2 + y^2 \leq 1\).

Let’s start by establishing the needed functions. First, here is the now- familiar square(x:Double) function, again for reference.

def sqr(x: Double) = x * x

We use this function to create another function, which tests whether a given dart, specified as an (x, y) coordinate pair (a Scala tuple), falls within the unit circle:

val inCircle: ((Double, Double)) => Boolean = { case (x, y) => sqr(x) + sqr(y) <= 1.0 }

Let’s to a quick sanity check here:

scala> inCircle((0, 0))
res1: Boolean = true

scala> inCircle((1, 1))
res2: Boolean = false

scala> inCircle((0.7, 0.7))
res3: Boolean = true

scala> inCircle((0.7, -0.7))
res4: Boolean = true

scala> inCircle((1, -1))
res5: Boolean = false

So how do we generate a set of darts and test whether they fall within the circle? We start by using a Scala Stream (more on this in collections).

scala> val randomPairs = Stream continually (math.random, math.random)
randomPairs: scala.collection.immutable.Stream[(Double, Double)] =
   Stream((0.45422625790687077,0.1916739269602844), ?)

Note

The examples we are showing here, much like typical introductory examples, trade good pedagogy (not bogging down students with too many details) for performance and scalability. If you need to operate on a larger number of darts, you’ll want to take advantage of StreamIterator, not Stream. A StreamIterator can be obtained from a Stream by reworking the code above as follows:

scala> val randomPairs = Stream continually (math.random, math.random) iterator
randomPairs: scala.collection.immutable.Stream[(Double, Double)] =
   Stream((0.45422625790687077,0.1916739269602844), ?)

Notice that the iterator() method is being invoked. This can be taught after students have learned more about Scala collections.

What you see here is the first item of the stream being displayed. The ”?” indicates that there are more values (an infinite number, in theory) which will be obtained on demand. This principle is an advanced one but one that is teachable and can be understood in greater detail later.

So how do we get a finite number of darts? This is where a number of famous methods from functional programming come to play. One is known as take(n), which can take however many we want. Let’s use this to show how to print the first 5 random coordinate pairs.

scala> randomPairs.take(5)
res7: scala.collection.immutable.Stream[(Double, Double)] =
   Stream((0.45422625790687077,0.1916739269602844), ?)

scala> randomPairs.take(5).foreach(println)
(0.45422625790687077,0.1916739269602844)
(0.9252028282996272,0.5638265909110913)
(0.5588908846857542,0.21516929857230815)
(0.5842149396390998,0.7226255374753748)
(0.8454163994561401,0.6805038035803781)

So what is going on here? We’re taking the first 5 coordinate pairs in the stream and applying the println() function to each item in the stream (that is, each coordinate pair). While there are some aspects of this that are advanced, there are some aspects that are vastly simpler than their equivalent in tradition imperative object-oriented languages (e.g. C++, Java, C#). For example, we rely on the notion of a tuple (a pair of Double values), which usually requires the premature exploration of a Pair class in other languages. Before long, you need to learn a lot of arcane type theory to understand Pair, whereas in Scala, the type information is inferred.

Looking more closely:

scala> (math.random, math.random)
res10: (Double, Double) = (0.20679803333001656,0.91233235776938)

This generates a random pair, and it even looks like a random pair from mathematics. Of course, it’s also type-safe!

So we’re now near the point where we can put all of the pieces together. We have a function to determine whether a randomly generated coordinate pair falls within the unit circle. Let’s compute \(\pi\).

scala> val n = 1000000
n: Int = 1000000

scala> val darts = randomPairs.take(n)
darts: scala.collection.immutable.Stream[(Double, Double)] =
   Stream((0.45422625790687077,0.1916739269602844), ?)

scala> val dartsInCircle = darts.count(inCircle)
dartsInCircle: Int = 784894

scala> val totalDarts = darts.length
totalDarts: Int = 1000000

scala> val area = 4.0 * dartsInCircle / totalDarts
area: Double = 3.139576

This is a good time to introduce the dot-less syntax, which is often associated with object-oriented programming but actually precedes these languages (C struct et al).

You can also write the above code (where you see dots) as follows:

scala> val darts = randomPairs take n
darts: scala.collection.immutable.Stream[(Double, Double)] =
   Stream((0.45422625790687077,0.1916739269602844), ?)

scala> val dartsInCircle = darts count inCircle
dartsInCircle: Int = 784894

scala> val totalDarts = darts length
totalDarts: Int = 1000000

scala> val area = 4.0 * dartsInCircle / totalDarts
area: Double = 3.139576

At some point, you realize that you want to enter the code into a text editor that can be loaded into the Scala interpreter (as opposed to being entered interactively).

The actual code for this can be found at https://bitbucket.org/loyolachicagocs_plsystems/numerical-explorations-scala. You can pull up the Scala worksheet from MonteCarloPiStreamIteratorChunkFree.sc (by drilling into Source).

Here’s the final version of our function to calculate \(\pi\).

We also provide a simple timing function, that allows us to time a block of Scala statements. We’ll not present it in detail now, but this function could be given to your students with the hope of sensitizing them to the notion of performance. It also shows the tremendous power available in Scala to work with a block of Scala code as an object (which can produce a value).

We’re going to use this to show how to measure the execution time of our \(\pi\) calculation by varying the problem size. Here is the fragment of code that tries different problem sizes up to \(n = 10^k\), where \(k = \lfloor log_{10}(Int.MaxValue) \rfloor\) (\(k = \lfloor log_{10}(2147483647) \rfloor = 9\))

Note

You might wonder why we are limited to Int in this version as opposed to a 64-bit Int. It is a perfectly valid idea to ponder, especially in the modern era. Scala collections do not support operations like take() and drop() with 64-bit Int values. The explanation for this is a bit beyond the scope here, but we have worked out 64-bit versions where we perform the Monte Carlo \(\pi\) calculation in chunks. You can visit our repository for these versions, which are a bit complex compared to the Int version. We hope future versions of Scala will evolve beyond 32-bit thinking but don’t see this as a show stopper for introductory teaching. (We also hope the friendly competition between F# and Scala, where F# supports Int64, will eventually make its way to Scala.

Here is the output (some output has been deleted for conciseness).

scala> :load MonteCarloPiStreamIteratorChunkFree.sc
Loading MonteCarloPiStreamIteratorChunkFree.sc...
sqr: (x: Double)Double
inCircle: ((Double, Double)) => Boolean = <function1>
randomPairs: Iterator[(Double, Double)] = non-empty iterator
n: Int = 1000000
darts: Iterator[(Double, Double)] = non-empty iterator
dartsInCircle: Int = 785719
totalDarts: Int = 0
area: Double = Infinity
longDartsInCircle: (numDarts: Int)Long
monteCarloCircleArea: (numDarts: Int)Double
time: [R](block: => R)R
powers: scala.collection.immutable.Range.Inclusive =
   Range(1, 2, 3, 4, 5, 6, 7, 8, 9)
sizes: scala.collection.immutable.IndexedSeq[Int] =
   Vector(10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000)
problemSizes: scala.collection.immutable.IndexedSeq[Int] =
   Vector(1000000, 10000000, 100000000, 1000000000)
Trying these probem sizes
1000000
10000000
100000000
1000000000
numDarts: 1000000
Elapsed time: 0.149393s
The area is 3.141644
numDarts: 10000000
Elapsed time: 1.110151s
The area is 3.1406688
numDarts: 100000000
Elapsed time: 10.744608s
The area is 3.141555
numDarts: 1000000000

We will discuss performance and timing issues again when speaking to parallel computing in Basic Parallelism using Par.

4.6. More Scala

4.6.1. While Loop

  • While loop
  • Not an expression

In Scala, you can do imperative style loops and interactive loops:

Consider this interactive session to compute well-known example for sum

scala> def sum(n : Int) : Int = {
     |   var i = 0
     |   var sum = 0
     |   while (i <= n) {
     |     sum = sum + n
     |     i = i + 1
     |   }
     |   sum
     | }
sum: (n: Int)Int

scala> sum(100)
res5: Int = 10100

scala> sum(0)
res6: Int = 0

Much is possible without iteration with the added advantage of being recursive and side-effect free underneath the hood.

scala> def sum(n : Int) = (1 to n).sum sum: (n: Int)Int

scala> sum(0) res8: Int = 0

scala> sum(100) res9: Int = 5050

What about interactive loops?

scala> val reader = new ConsoleReader
reader: scala.tools.jline.console.ConsoleReader =
   scala.tools.jline.console.ConsoleReader@26075b18

scala> var response = 0
response: Int = 0

scala> while (response < 0 || response > 100) {
     |    response = reader.readLine("Enter a number >= 0  and <= 100? ").toInt
     | }

scala> var response = -1
response: Int = -1

scala> while (response < 0 || response > 100) {
     |    response = reader.readLine("Enter a number >= 0  and <= 100? ").toInt
     | }
Enter a number >= 0  and <= 100? -5
Enter a number >= 0  and <= 100? 105
Enter a number >= 0  and <= 100? 100
scala> ...

It is interesting to think about whether we can turn an interactive while loop into one without side effects. There are so many bad things that happen to us as CS1 educators when we work with interactive loops:

  • Improper initialization of the response variable.
  • Improper setting of response upon termination.
  • etc.

This is my early attempt to figure out how to have a side-effect free interactive while loop. Basic idea:

  • Use Stream.continually to get a continuous stream of input lines read.
  • Because Stream is lazy only in the tail, assume a blank line as the first value of the stream (which is safely ignored). I’d like to figure out the right way to eliminate this without introducing complexity.
  • Use takeWhile to accept input until an appropriate terminating condition (the word “no” is entered)
  • Convert each line to integer, if possible. We simplify exception management by applying a Try wrapper to each attempted conversion. Try is an important Scala idiom for representing a computation that either succeeds with a result value or fails with an exception.
  • Use flatMap to get only the input values where the conversion succeeded. In the process of applying flatMap, however, we need to convert each Try value to an Option value. Option is very similar to Try but simpler in that it represents all failures as None, which flatMap discards from the resulting list.
scala> val s1 = "" #:: Stream.continually(reader.readLine("Prompt: "))
s1: scala.collection.immutable.Stream[String] = Stream(, ?)

scala> val s = s1.takeWhile(_ != "no")
s: scala.collection.immutable.Stream[String] = Stream(, ?)

scala> val l = s.toList
Prompt: 25
Prompt: 35
Prompt: 55
Prompt:
Prompt: s
Prompt: no
l: List[String] = List("", 25, 35, 55, "", s)

scala> import scala.util.Try
import scala.util.Try

scala> def toInteger(s: String) = Try(s.toInt)
toInteger: (s: String)scala.util.Try[Int]

scala> l.map(toInteger)
res3: List[scala.util.Try[Int]] = List(Failure(java.lang.NumberFormatException: For input string: ""), Success(25), Success(35), Success(55), Failure(java.lang.NumberFormatException: For input string: ""), Failure(java.lang.NumberFormatException: For input string: "s"))

scala> l.map(t => toInteger(t).toOption)
res6: List[Option[Int]] = List(None, Some(25), Some(35), Some(55), None, None)

scala> l.flatMap(t => toInteger(t).toOption)
res7: List[Int] = List(25, 35, 55)

scala> l.flatMap(t => toInteger(t).toOption).sum
res8: Int = 115

4.6.2. for loop

Similar to the while loop, a for loop exists for imperative style programming, often when there is a need to do something where a side-effect is needed.

scala> var sum = 0
sum: Int = 0

scala> var n = 100
n: Int = 100

scala> for (i <- 1 to n)
     | {
     |    sum = sum + i
     | }

scala> println(s"The sum is $sum")
The sum is 5050

4.6.3. for comprehension

A for comprehension is designed for where you want a more functional style. That is, there is no intention of having side effects, and it is likely that you want to use the result of the comprehension in another computation.

Let’s look at something a bit more interesting: Getting the first n squares:

scala> val n = 10
n: Int = 10

scala> val first_n_squares = for (i <- 1 to n) yield { i * i }
first_n_squares: scala.collection.immutable.IndexedSeq[Int] =
   Vector(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)

You could wrap this up nicely in a Scala function as follows:

scala> def squares(n : Int) = for (i <- 1 to n) yield { i * i }
squares: (n: Int)scala.collection.immutable.IndexedSeq[Int]

scala> squares(10)
res7: scala.collection.immutable.IndexedSeq[Int] =
   Vector(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)

4.6.4. options and failure sans exceptions

Per the Scala documentation: An option represents optional values. Instances of Option are either an instance of scala.Some or the object None.

The Scala Option type is useful for dealing with the notion of success and failure (but is not limited to supporting this concept). In the interests of keeping this discussion focused on being CS1-friendly, an Option is always connected to an underlying type. For example Option[Int] means that you either get the Int, or nothing (None).

scala> val i = Some(3)
i: Some[Int] = Some(3)

scala> val j = None
j: None.type = None

Some and None are both case classes that extend Option[A] (a generic class that allows you to use any time). While knowing all of the details requires a mastery of object-oriented programming (and perhaps more), the introduction of this idea is remarkably straightforward and one that can help students to write better code from the beginning. In CS1 courses, we tend to spend a lot of time handling special cases, and options give us a framework for dealing with missing information, etc.

As you can see from the above, i is defined as some value, in this case 3. It’s a wrapped value, of course, that must be unbundled later. For example, here is an attempt to use the value i (incorrectly):

scala> i + 5
<console>:9: error: type mismatch;
 found   : Int(5)
 required: String
              i + 5
                  ^

So how do we make use of i and j? A method associated with options, getOrElse allows us to get the option, if set to some value. Rather delightfully, we can set a value to be used, if the option was not set previously.

scala> i getOrElse(-1)
res13: Int = 3

scala> j getOrElse(-1)
res14: Int = -1

So now if we want to use this value in a computation, we can just do this:

scala> i.getOrElse(-1) + j.getOrElse(-1)
res17: Int = 2

Options have their uses throughout Scala, notably its libraries. For example, maps (a.k.a. associative structures/arrays) use option to return the result of getting a key from the map:

scala> val map = scala.collection.mutable.HashMap.empty[String, Int]
map: scala.collection.mutable.HashMap[String,Int] = Map()

scala> map += ("scala" -> 10)
res4: map.type = Map(scala -> 10)

scala> map += ("java" -> 20)
res5: map.type = Map(scala -> 10, java -> 20)

scala> map += ("C#" -> 15)
res6: map.type = Map(scala -> 10, java -> 20, C# -> 15)

scala> map += ("F#" -> 25)
res7: map.type = Map(scala -> 10, F# -> 25, java -> 20, C# -> 15)

scala> map += ("scala" -> 22)
res8: map.type = Map(scala -> 22, F# -> 25, java -> 20, C# -> 15)

Here’s an attempt to get the case-sensitive and case-insensitive versions of key, F#, from the map:

scala> map.get("F#")
res9: Option[Int] = Some(25)

scala> map.get("f#")
res10: Option[Int] = None

Seasoned Java programmers know that an entry not found in the map will result in the null value being returned. This differs from Scala, because the value gotten from the map must be tested before attempting to use it in any way.

In Scala, because None and Some(25) are both options, you can use getOrElse to obtain the options value (irrespective of whether the value is null, or not set) without writing an (unwanted) if-then-else statement, which results in bloat (in most programming languages).

scala> val entry = map.get("F#").getOrElse(-1)
entry: Int = 25

scala> println(s"The entry for F# is $entry")
The entry for F# is 25

It’s often the case that we have default values associated with failure to accomplish a certain task. The Option idiom is an attempt to standardize this for core data structures and (as we’ll see) other situations (e.g. working with complex for comprehensions). In the case of maps, an entry not found would usually default to 0 or -1 (a convention that dates back to the earliest days of C), which is preferable to throwing exceptions for no good reason (not to mention our general dislike of prematurely covering exceptions as a programming technique in CS1 in particular.)

4.6.5. yield

Yield is used to take a collection and produce a new one with mapped values.

Let’s produce a List[Int] of squares from a List[Int] of the first three integers:

scala> val l = List(1, 2, 3)
l: List[Int] = List(1, 2, 3)

scala> l
res10: List[Int] = List(1, 2, 3)

scala> for (i <- l) yield { i * i }
res11: List[Int] = List(1, 4, 9)

Let’s try this with an Array[Int]:

scala> val a = Array(1, 2, 3)
a: Array[Int] = Array(1, 2, 3)

scala> for (i <- a) yield i * i
res16: Array[Int] = Array(1, 4, 9)

For the most part, when iterating over values and using yield, you will always get back the same type, or another type that makes sense.

In these basic examples, the above can also be written as follows (without the for):

scala> l map (i => i * i)
res17: List[Int] = List(1, 4, 9)

scala> a map (i => i * i)
res18: Array[Int] = Array(1, 4, 9)

4.6.6. Ranges

Ranges should be familiar to you if you’ve worked with other agile scripting languages, e.g. Python.

scala> Range(1, 5)
res20: scala.collection.immutable.Range = Range(1, 2, 3, 4)

This gives a range of values from 1 to 5 but stopping at the last value before 5. The increment is +1.

scala> Range(1, 9, 2)
res22: scala.collection.immutable.Range = Range(1, 3, 5, 7)

You can also work backwards:

scala> Range(9, 0, -2)
res24: scala.collection.immutable.Range = Range(9, 7, 5, 3, 1)

4.6.7. Multiple generators

4.6.8. If guards

4.6.9. Variables

4.6.10. Patterns

4.7. Files

  • Can use Scanner
  • scala.io.Source
  • Scala Iterator[Char]
  • getLines : Iterator[String]
  • Use with higher-order methods
  • Write with PrintWriter
  • Introduce APIs?

4.8. Classes and Objects

4.8.1. Classes

Here is the familiar Point class. It’s often shown where the (x, y) coordinate pair are Int (even in the Scala documentation) but is even more interesting with Double. This is an elaborated version that includes elements appropriate mostly to CS1 and some that are best covered in CS2 and beyond.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Point(initial_x: Double, initial_y: Double) {
   var x: Double = initial_x
   var y: Double = initial_y

   def move(dx: Double, dy: Double) {
      x = x + dx
      y = y + dy
   }

   def distanceToOrigin() : Double = {
      math.sqrt(x * x + y * y)
   }

   def distanceTo(p : Point) = {
      math.sqrt( (p.x - x) * (p.x - x) + (p.y - y) * (p.y - y) )
   }

   def add(p : Point) = {
      x = x + p.x
      y = y + p.y
      this
   }

   def +(p : Point) = {
      add(p)
   }

   override def toString(): String = s"($x, $y)";
}

What does this class Point show?

  • how to create a simple Scala class. Notice the complete lack of keyboards and public/private/static that tend to confuse students!
  • Scala brings back disciplined operator overloading. We’d probably not use this in CS1, but it is entirely appropriate for CS2.
  • Shows how to refer to the object (familiarly) with this. Again, the methods relying on this might be more appropriate for CS2 or even later courses that dive into OOP’s complexity.
  • Constructors? The Scala class definition itself makes it clear how one constructs an instance. Just like a function definition in general, there can be default values. It is liberating not to have constructors (especially too many of them), especially when trying to introduce a topic. (This will become readily apparent when we look at case classes, which provide a mechanism for more data-centric OO abstraction.)
  • Convert an instance of class Point to a String representation using ToString(). ToString() can be a valuable pedagogical tool, done right (as observed in languages like Python). Scala 2.10 gives us the ability to do type-safe string interpolation by substituting the value of variables (their String representation) into a String template. In Scala, prefixing a string literal with s will give you a string where any variables (beginning with $) are substituted. Here, it allows us to get a beautiful representation of a point as an (x, y) pair with virtually no effort or complexity!

Let’s take a look at how the Point class is used:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
val p = new Point(2, 3)
val q = new Point(-2, 3)

println(s"Two points $p and $q")

val distanceToOrigin = p.distanceToOrigin()

println(s"distance from p to origin = $distanceToOrigin")

val dpq = p.distanceTo(q)
val dqp = q.distanceTo(p)

println(s"d(p,q) = $dpq")
println(s"d(q,p) = $dqp")

val pointSum = p.add(q)
val pointSumOp = p + q

println(s"p.add(q) = $pointSum; p + q = $pointSumOp")

This results in the following output.

scala> :load point.sc
:load point.sc
Loading point.sc...
defined class Point
p: Point = (2.0, 3.0)
q: Point = (-2.0, 3.0)
Two points (2.0, 3.0) and (-2.0, 3.0)
distanceToOrigin: Double = 3.605551275463989
distance from p to origin = 3.605551275463989
dpq: Double = 4.0
dqp: Double = 4.0
d(p,q) = 4.0
d(q,p) = 4.0
pointSum: Point = (0.0, 6.0)
pointSumOp: Point = (-2.0, 9.0)
p.add(q) = (-2.0, 9.0); p + q = (-2.0, 9.0)

4.8.2. A Look at Singleton Objects

At some point, relying on the interpreter for trying out the Point class (as we have been doing until now) grows a bit tedious. Furthermore, sometimes you want to have a complete, functioning program that includes not just the class Point but also a driver program–often found in a main() method–that allows you to run it from the command line.

Scala’s answer to main() (largely a vestige of C-based languages) is to support singleton objects, which we rely upon in some more advanced examples. While we consider the singleton pattern to be a bit overrated and overused (e.g. Runtime.getRuntime() and many others like it in Java’s API), the singleton object as found here is completely decoupled from any class and allows you to create an environment so to speak with its own namespace but without the burden of a full class definition.

In this example, we create a singleton object to act as our main() driver.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
object PointDemo {
   def apply() {
      val p = new Point(2, 3)
      val q = new Point(-2, 3)

      println(s"Two points $p and $q")

      val distanceToOrigin = p.distanceToOrigin()

      println(s"distance from p to origin = $distanceToOrigin")

      val dpq = p.distanceTo(q)
      val dqp = q.distanceTo(p)

      println(s"d(p,q) = $dpq")
      println(s"d(q,p) = $dqp")

      val pointSum = p.add(q)
      val pointSumOp = p + q

      println(s"p.add(q) = $pointSum; p + q = $pointSumOp")
   }
}

PointDemo()

Similar to a class definition, you can have methods. Notably absent, however, are any parameters to the object name. To create an entry point that allows the object to be used like any other Scala function, we define an apply() method, which may or may not have parameters. In our case, we just want to be able to run the same demo code we produced previously and then invoke the PointDemo as a function, e.g. PointDemo(). This could be further wrapped with some logic to handle command-line arguments, etc. More on that towards the end.

4.9. Useful REPL functionality

The Scala REPL supports a number of commands that can be greatly helpful for working interactively. We’ve relied on many of these in the preparation of this tutorial but will focus on the highlights, especially for use in CS1 teaching.

scala> :help
All commands can be abbreviated, e.g. :he instead of :help.
Those marked with a * have more detailed help, e.g. :help imports.

:cp <path>                 add a jar or directory to the classpath
:help [command]            print this summary or command-specific help
:history [num]             show the history (optional num is commands to show)
:h? <string>               search the history
:imports [name name ...]   show import history, identifying sources of names
:implicits [-v]            show the implicits in scope
:javap <path|class>        disassemble a file or class name
:load <path>               load and interpret a Scala file
:paste                     enter paste mode: all input up to ctrl-D compiled together
:power                     enable power user mode
:quit                      exit the interpreter
:replay                    reset execution and replay all previous commands
:reset                     reset the repl to its initial state, forgetting all session entries
:sh <command line>         run a shell command (result is implicitly => List[String])
:silent                    disable/enable automatic printing of results
:type [-v] <expr>          display the type of an expression without evaluating it
:warnings                  show the suppressed warnings from the most recent line which had any

4.9.1. paste

When writing longer definitions in the REPL, it can be tricky. Having paste mode allows you to take some code you have (perhaps from an editor where you are typing a Scala program) and copy/paste into the Scala session.

This shows an example of entering a slightly more verbose than needed definition of the square() function (presented earlier in this section):

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

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

// Exiting paste mode, now interpreting.

square: (x: Int)Int

Notice that you don’t see the continuation characters when entering multiple lines of text.

4.9.2. load

Many programmers coming to Scala find it a bit frustrating at first that some things (like interactive scripts, found in languages like Python) don’t work quite the same way. More often than not, the real issue is whether there is an easy way to load a script into the REPL–as opposed to having to run it on the command line (which is also possible but not the focus of this section). It is a matter of loading the filename, which may be an absolute or relative path.

scala> :load myscript.scala

4.9.3. history

History should be familiar to anyone who has used modern Unix shells. Even if you haven’t, you’ve probably used the history buffer, which allows you to use the up/down arrows or emacs/vi commands (^p, ^n, j, k) to access previous and next commands in the history buffer.

Scala also allows you to do ^r to perform a regex search for text within a previous command. We rely on this heavily in our work, especially in this section, where it was necessary to look up previous attempts within the REPL so they could be pasted into the notes!

In this example, I used ^r to search for the substring “val” so I could find a previous value definition in my REPL session:

(reverse-i-search)`val': val entry = map.get("F#").getOrElse(-1)

When you type ^r, you’ll be given the “(reverse-i-search)” prompt to perform a search. While the full functionality of regex is provided, the nominal use is to type a few characters of something you probably remember (at least partially). More often than not, I am looking for previous “val” or “def” (functions).