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> 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:
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
-
You can convert an
Int
to aString
like this:Scala REPLscala> "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 -
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
. sys.exit
can be used to terminate a program.-
sys.err
exits a program with an error condition and outputs an error message throughstderr
.
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.
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:
$ 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.
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:
$ 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/
.
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
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> 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> 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> 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:
$ 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.
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.
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/
.
Here is how to use using
to write a file called ~/.config/hub
:
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
.
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.
@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.
@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.
© Copyright 1994-2024 Michael Slinn. All rights reserved.
If you would like to request to use this copyright-protected work in any manner,
please send an email.
This website was made using Jekyll and Mike Slinn’s Jekyll Plugins.