7 minutes read

In this topic, we are going to discuss the file reading instruments in Scala. Remember, when reading files, you need to consider error handling, resource closure, and content processing approach.

The Java way

Scala runs in the JVM, which means that all Java libraries are available to us. Accordingly, we can use different reading methods provided by the Java API, for instance:

try
  val file    = File("data.txt")
  val scanner = Scanner(file)

  while scanner.hasNextLine do {
    val line = scanner.nextLine()
    println(line) // or some logic here
  }

  scanner.close()
catch
  case e: FileNotFoundException => println("File not found: " + e.getMessage)
  case e: IOException           => println("Error reading file: " + e.getMessage)

Here we're using the old-fashioned Java Scanner to read content line by line. It is a classical Java try/catch approach with exception support and resource closing.

The Scala way

Scala also has a number of classes of its own to handle files. The simplest option is to use scala.io.Source. Let's look at the fromFile method:

def fromFile(name: String)(implicit codec: Codec): BufferedSource

The method takes a file name (its path) and a Codec that allows you to override the encoding. It returns a BufferedSource, and its getLines method returns an Iterator that treats any of \r\n, \r, or \n as a line separator, so each element in the sequence is a line from the file.

This method also has several overloads that allow you to define buffer sizes, use Java files, and define codecs in different ways, but they work in a similar fashion, so we won't consider them.

These methods throw exceptions too. We can use Try to present them as data and process them:

import scala.io.*
import scala.util.*

def readFileUtf8(filePath: String): Try[String] = Try {
  val source = Source.fromFile(filePath)(Codec.UTF8)
  val content = source.mkString
  source.close()
  content
}

readFileUtf8("data.txt") match
  case Success(text)      => println(text)
  case Failure(exception) => exception.getMessage

Take into account that processing content as a collection or as an entire string (source.mkString) is suitable only for small files. If your file is big or will grow bigger in the future, you will have to process the content step-by-step.

Large files

Let's add more functional techniques and try to use all the possibilities of the Source.fromFile method. In this example, we will implement the loan pattern that uses a generic control-abstraction function to decouple opening and closing resources from content processing.

def withSourceReader[T](path: String)(operate: Iterator[String] => T): T =
  val source = Source.fromFile(path)(Codec.UTF8)
  val lines  = source.getLines()
  try operate(lines)
  finally source.close()

With the finally construct in this function, we guarantee that whatever happens while processing the file, the file itself will be closed.

Then, we can use a function with different behavior to process the file:

val filePath  = "data.txt"
val maxLength = 50

withSourceReader(filePath) { source =>
  source
    .filter(_.length < maxLength) // drop too long lines
    .foreach(println)             // and print what is left
}

withSourceReader(filePath) { source =>
  val number = source.count(_.contains("secret")) // just count the lines with "secret" word
  println(s"$number secrets found!")
}

The advantage of using this approach is that withSourceReader assures that the file is closed at the end and, at the same time, we can define different processing without constructing the whole file's text in a String. This technique is called the loan pattern because a control-abstraction function opens a resource and "lends" it to a function. When the function is completed, it signals that it no longer needs the 'borrowed' resource. The resource is then closed in a finally block to make sure it is indeed closed, regardless of whether the function is completed by returning normally or by throwing an exception.

Conclusion

In this topic, you learned about some issues you should consider during Scala file reading. Using the fromFile and getLines methods give us an opportunity to process the file content step by step. We also implemented the loan pattern to decouple opening and closing resources from content processing to demonstrate the Scala functional approach to file reading.

How did you like the theory?
Report a typo