Mike Slinn

Scala I/O

— Draft —

Published 2013-09-15. Last modified 2019-09-08.
Time to read: 4 minutes.

This lecture introduces reading and writing files and shows an example of tail recursion.

Scala has a convenient API for reading data from various sources, but relies on the Java runtime library for output.

Console Input

Reading input from the user is often necessary for console applications. scala.io.StdIn.readLine can accept an optional prompt parameter. Here I use the Scala REPL to execute io.StdIn.readLine and assign the input entered by the user (that’s me!) into an immutable variable called line. The String argument passed to readLine is the prompt, which I specified as "input please> ". In response, I typed blah and pressed carriage return. Once again we see that the REPL does not exactly work the same as a real program because the text that I typed was not echoed by the REPL.

Scala REPL
scala> scala
Welcome to Scala 2.12.9 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_74).
Type in expressions for evaluation.
Or try :help.
scala>
val line = io.StdIn.readLine("input please> ") input please> line: String = blah

Exercise – Prompt Loop

Write a small Scala program that repeatedly prompts the user for a number, then prints a running total. Handle String to Int conversion problems using try / catch. The dialog should look like this:

Sample dialog
Total: 0; Input a number to add, Enter to stop> 2
Total: 2; Input a number to add, Enter to stop> 33
Total: 35; Input a number to add, Enter to stop> asdf
Invalid number ignored.
Please try again.
Total: 35; Input a number to add, Enter to stop> 3
Total: 38; Input a number to add, Enter to stop>

Hints

  1. You can convert an Int to a String like this:
    Scala REPL
    scala> "3".toInt
    res19: String  = 3
    scala>
    "3asdf".toInt java.lang.NumberFormatException: For input string: "3asdf" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    scala>
    try { "3asdf".toInt } catch { case _: Throwable => 0 } res23: Int = 0
  2. You could use a mutable variable to accumulate the result, or you could use recursion. Tail recursion is optimized by the Scala compiler so it does not use large amounts of stack memory. You can tell the compiler that a method is intended to implement tail recursion by annotating the method with @tailrec.
  3. sys.exit can be used to terminate a program.
  4. sys.err exits a program with an error condition and outputs an error message through stderr.

Solutions

Solution #1

This solution looks like a Java programmer wrote it. It works, runs efficiently, and is easy to understand, so long as you recognize the Java hack of returning an out-of-bounds value (0) to flag a value that should not be used.

Scala code
object PromptLoop extends App {
  var total = 0
  do {
    val line = io.StdIn.readLine(s"Total: $total; Input a number to add, Enter to stop> ").trim
    if (line.isEmpty) sys.exit()
    val number: Int = try {
      line.toInt
    } catch {
      case nfe: NumberFormatException =>
        println("Invalid number ignored.\nPlease try again.")
        0
case throwable: Throwable => sys.error(throwable.getMessage) } total = total + number } while (true) }

You can run the above solution like this:

Shell
$ sbt "runMain solutions.PromptLoop1"

Solution #2

This solution uses tail recursion, so it does not require a mutable variable to accumulate results. It is also efficient and easy to understand, with the same caveat as for Solution #1. Because no mutable variables are used, this style of programming is useful for multicore operations.

Scala code
object PromptLoop2 extends App {
  @tailrec
  def getValue(increment: Int, subtotal: Int): Int = {
    val total = subtotal + increment
    val line = io.StdIn.readLine(s"Total: $total; Input a number to add, Enter to stop> ").trim
    if (line.isEmpty) 0 else {
      val userValue = try {
        line.toInt
      } catch {
        case nfe: NumberFormatException =>
          println("Invalid number ignored.\nPlease try again.")
          0
case throwable: Throwable => sys.error(throwable.getMessage) } getValue(userValue, total) } }
getValue(0, 0) }

You can run the above solution like this:

Shell
$ sbt "runMain solutions.PromptLoop2"

io.Source

Scala has some easy ways to read in text files, unfortunately they are poorly designed, because the files are not closed when used as documented. I recommend that do not you use these methods to read files. I will show you a better way in a moment.

The sample code for this lecture can be found in courseNotes/src/main/scala/IoSource.scala.

Scala REPL
scala> io.Source.fromFile("/etc/passwd") foreach print
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
...
scala>
io.Source.fromFile("/etc/passwd").mkString.length res1: Int = 2200
scala>
val rootLines = io.Source.fromFile("/etc/passwd").getLines() rootLines: Iterator[String] = non-empty iterator
Beware: iterators can only be traversed once; this is a potential source of bugs in your software!

As you can see, Source.getLines returns an Iterator of String. If we did not convert the Iterator to a List, referencing the Iterator a second time would return nothing, like this.

Scala REPL
scala> s"${rootLines.length} lines printed from /etc/passwd:\n  " + rootLines.mkString("\n  ")
res0: String =
"28 lines printed from /etc/passwd:
  "

We can convert the Iterator to a List easily; notice that the contents of the file are now displayed.

Scala REPL
scala> val rootLines = io.Source.fromFile("/etc/passwd").getLines().toList
val rootLines: List[String] = List(root:x:0:0:root:/root:/bin/bash, daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin, bin:x:2:2:bin:/bin:/usr/sbin/nologin, sys:x:3:3:sys:/dev:/usr/sbin/nologin, sync:x:4:65534:sync:/bin:/bin/sync, games:x:5:60:games:/usr/games:/usr/sbin/nologin, man:x:6:12:man:/var/cache/man:/usr/sbin/nologin, lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin, mail:x:8:8:mail:/var/mail:/usr/sbin/nologin, news:x:9:9:news:/var/spool/news:/usr/sbin/nologin, uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin, proxy:x:13:13:proxy:/bin:/usr/sbin/nologin, www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin, backup:x:34:34:backup:/var/backups:/usr/sbin/nologin, list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin, irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin, gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin, nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin, systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin, systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin, systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin, messagebus:x:103:106::/nonexistent:/usr/sbin/nologin, syslog:x:104:110::/home/syslog:/usr/sbin/nologin, _apt:x:105:65534::/nonexistent:/usr/sbin/nologin, tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false, uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin, tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin, sshd:x:109:65534::/run/sshd:/usr/sbin/nologin, landscape:x:110:115::/var/lib/landscape:/usr/sbin/nologin, pollinate:x:111:1::/var/cache/pollinate:/bin/false, mslinn:x:1000:1000:,,,:/home/mslinn:/bin/bash, rtkit:x:112:120:RealtimeKit,,,:/proc:/usr/sbin/nologin, dnsmasq:x:113:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin, usbmux:x:114:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin, pulse:x:115:122:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologin, avahi:x:116:124:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin, cups-pk-helper:x:117:125:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin, geoclue:x:118:126::/var/lib/geoclue:/usr/sbin/nologin, saned:x:119:128::/var/lib/saned:/usr/sbin/nologin, colord:x:120:129:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin, gdm:x:121:130:Gnome Display Manager:/var/lib/gdm3:/bin/false, whoopsie:x:122:133::/nonexistent:/bin/false, kernoops:x:124:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin, lightdm:x:125:135:Light Display Manager:/var/lib/lightdm:/bin/false, speech-dispatcher:x:126:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false, hplip:x:127:7:HPLIP system user,,,:/run/hplip:/bin/false, xrdp:x:128:137::/run/xrdp:/usr/sbin/nologin, cockpit-ws:x:129:139::/nonexistent:/usr/sbin/nologin, cockpit-wsinstance:x:130:140::/nonexistent:/usr/sbin/nologin, postgres:x:131:141:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash, fwupd-refresh:x:132:142:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin, cups-browsed:x:133:125::/nonexistent:/usr/sbin/nologin, dhcpcd:x:134:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false, polkitd:x:999:999:polkit:/nonexistent:/usr/sbin/nologin, ollama:x:998:998::/usr/share/ollama:/bin/false, nvidia-persistenced:x:135:145:NVIDIA Persistence Daemon,,,:/nonexistent:/usr/sbin/nologin, gnome-remote-desktop:x:997:997:GNOME Remote Desktop:/var/lib/gnome-remote-desktop:/usr/sbin/nologin, sbt:x:996:996:sbt daemon-user:/home/sbt:/bin/false)
scala>
s"${rootLines.length} lines printed from /etc/passwd:\n" + rootLines.mkString("\n") res2: String = 28 lines printed from /etc/passwd: man:x:6:12:man:/var/cache/man:/bin/sh lp:x:7:7:lp:/var/spool/lpd:/bin/sh mail:x:8:8:mail:/var/mail:/bin/sh news:x:9:9:news:/var/spool/news:/bin/sh uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh www-data:x:33:33:www-data:/var/www:/bin/sh backup:x:34:34:backup:/var/backups:/bin/sh list:x:38:38:Mailing List Manager:/var/list:/bin/sh irc:x:39:39:ircd:/var/run/ircd:/bin/sh gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh libuuid:x:100:101::/var/lib/libuuid:/bin/sh messagebus:x:102:105::/var/run/dbus:/bin/false avahi-autoipd:x:103:106:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false speech-dispatcher:x:108:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/sh ...

The Scala compiler converts the following into the same code as the above.

Scala REPL
scala> val rootLines2 = (for {
     |     line <- io.Source.fromFile("/etc/passwd").getLines()
     |   } yield line)
scala> .toList
rootLines2: List[String] = List(man:x:6:12:man:/var/cache/man:/bin/sh, lp:x:7:7:lp:/var/spool/lpd:/bin/sh, mail:x:8:8:mail:/var/mail:/bin/sh, news:x:9:9:news:/var/spool/news:/bin/sh, uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh, www-data:x:33:33:www-data:/var/www:/bin/sh, backup:x:34:34:backup:/var/backups:/bin/sh, list:x:38:38:Mailing List Manager:/var/list:/bin/sh, irc:x:39:39:ircd:/var/run/ircd:/bin/sh, gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh, libuuid:x:100:101::/var/lib/libuuid:/bin/sh, messagebus:x:102:105::/var/run/dbus:/bin/false, avahi-autoipd:x:103:106:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false, speech-dispatcher:x:108:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/sh, colord:x:109:117:colord colour management daemon,,,:/var/li...)
scala>
s"${rootLines2.length} lines printed from /etc/passwd:\n " + rootLines2.mkString("\n ") res3: String = 28 lines printed from /etc/passwd containing ’/var/’: man:x:6:12:man:/var/cache/man:/bin/sh lp:x:7:7:lp:/var/spool/lpd:/bin/sh mail:x:8:8:mail:/var/mail:/bin/sh news:x:9:9:news:/var/spool/news:/bin/sh uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh www-data:x:33:33:www-data:/var/www:/bin/sh backup:x:34:34:backup:/var/backups:/bin/sh list:x:38:38:Mailing List Manager:/var/list:/bin/sh irc:x:39:39:ircd:/var/run/ircd:/bin/sh gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh libuuid:x:100:101::/var/lib/libuuid:/bin/sh messagebus:x:102:105::/var/run/dbus:/bin/false avahi-autoipd:x:103:106:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false speech-dispatcher:x:108:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/sh colo...

You can run this sample code by typing:

Shell
$ sbt "runMain IoSource"

The io.Source companion object has several other handy ways of reading text data, including fromFile, fromInputStream, fromRawBytes, fromURI, and fromURL. New for Scala 2.12 is fromResource, which can be used to read data from a classpath resource, using either the context classloader, which is the default, or a specific classloader. For example, this will read from a file called readme.txt in a resources directory.

Scala code
val readmeText : Iterator[String] = ioSource.fromResource("readme.txt").getLines

We will learn how to read binary files in the lecture on Higher-Order Functions. The Using Partially Applied Functions lecture shows how to handle errors encountered while reading using partial functions by implementing the Loan Pattern, thereby guaranteeing that the input stream or file is always closed.

Using

This is a much better way of reading files that what is provided in the io.Source object. Let’s define two methods that I recommend you add to a package object in your Scala project.

We discussed package objects in the Scala Imports and Packages lecture of the Introduction to Scala course. I’ve defined it in the com.micronautics package, and you could rename the package name to suit your project.

Scala code
package com.micronautics {
  def readLines(file: File): List[String] =
    using(new BufferedReader(new FileReader(file)))
        { reader => unfold(())(_ => Option(reader.readLine).map(_ -> ((): Unit))) }
def using[A <: AutoCloseable, B] (resource: A) (f: A => B): B = try f(resource) finally resource.close()
/** Scala 2.13 defines `Iterator.unfold`. This is essentially the same method, provided for all Scala versions. */ def unfold[A, S](start: S) (op: S => Option[(A, S)]): List[A] = Iterator. iterate(op(start))(_.flatMap{ case (_, s) => op(s) }) .map(_.map(_._1)) .takeWhile(_.isDefined) .flatten .toList }

The two methods are called using and readLines. Scala 2.13 introduced the scala.utils.Using object, which has some peculiar syntax. I find the above more intuitive to work with, and it works with older versions of Scala perfectly well.

The sample code for this lecture can be found in compatLibDemo/project/Settings.scala.

Here is how to use using to write a file called ~/.config/hub:

Scala code
val home = System.getProperty("user.home")
if (home=="/home/travis") justForTravis(home)
val hubConfigFile = new File(s"$home/.config/hub")
if (hubConfigFile.exists)
  readLines(hubConfigFile)
    .find(_.contains("user:"))
    .map(_.split(" ").slice(2, 3).mkString)
    .mkString
else
  "noGithubUserFound"

Using can also write files; this code writes to ~/.config/hub.

Scala code
val configDir = new File(s"$home/.config/")
configDir.mkdir()
val hubFile = new File(configDir, "hub")
using(new BufferedWriter(new FileWriter(hubFile))) { bw =>
  bw.write(
    """- user: travis
    oauth_token: 12345678901234567890
    protocol: https
    """.stripMargin
  )
}

Writing to Files

Scala uses Java to write to files, so if you know Java, Scala uses the same API. However, you can of course use Scala constructs with the Java API.

For Java 6 and older versions of Java, you could write something like this to write Marvin the Martian’s famous phrase to a file called marvin.txt. These old and out-of-date versions of Java required you to first create a File object, then create a PrintWriter from the File. The PrintWriter instance could then be used to write the text, and the PrintWriter would need to be closed.

Scala code
@inline def writeToTextFile(fileName: String, content: String) = {
  import java.io.{File, PrintWriter}
  val writer = new PrintWriter(new File(fileName))
  writer.write(content)
  writer.close()
}
writeToTextFile("marvin.txt", "Being disintegrated makes me so angry!")

The @inline tag hints to the Scala compiler that the method definition should be expanded in place instead of invoking a method.

Java 7 introduced a more succinct syntax with the java.nio.file.Files class, and it is more efficient as well. Java 8 expanded this API considerably. The Files class has a write method that accepts a Path pointing to the file you want to write to, then an Array[Byte] is extracted from the String you wish to write. The third option specifies if the write should create a new file or append to an existing file.

Scala code
@inline def writeToTextFile(fileName: String, content: String) = {
  import java.nio.file.{Files, Paths, StandardOpenOption}
Files.write(Paths.get(fileName), content.getBytes, StandardOpenOption.CREATE) }
writeToTextFile("marvin.txt", "Being disintegrated makes me so angry!")

The Using Partially Applied Functions lecture later in this course will show how to handle I/O errors gracefully and efficiently.


* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.