Scala - Read/Write File (Source.fromFile)

• Scala's `Source.fromFile` provides a simple API for reading text files with automatic resource management through try-with-resources patterns or using `Using` from Scala 2.13+

Key Insights

• Scala’s Source.fromFile provides a simple API for reading text files with automatic resource management through try-with-resources patterns or using Using from Scala 2.13+ • Writing files requires Java’s PrintWriter or Files.write since Scala’s standard library doesn’t include native file writing utilities • Always handle encoding explicitly and close resources properly to prevent memory leaks and file handle exhaustion in production applications

Reading Files with Source.fromFile

The scala.io.Source object provides the primary interface for reading files in Scala. The fromFile method creates a BufferedSource that wraps a file reader.

import scala.io.Source

val filename = "data.txt"
val source = Source.fromFile(filename)

try {
  val lines = source.getLines().toList
  lines.foreach(println)
} finally {
  source.close()
}

The getLines() method returns an iterator over file lines, which you can convert to a collection or process lazily. Always close the source in a finally block to release file handles.

Using the Using Construct for Automatic Resource Management

Scala 2.13 introduced scala.util.Using for automatic resource management, eliminating manual try-finally blocks:

import scala.io.Source
import scala.util.Using

val filename = "data.txt"

Using(Source.fromFile(filename)) { source =>
  source.getLines().foreach(println)
}

Using automatically closes the resource when the block completes, whether normally or through an exception. It returns a Try[T] that you can pattern match:

import scala.util.{Using, Success, Failure}

Using(Source.fromFile("data.txt")) { source =>
  source.getLines().toList
} match {
  case Success(lines) => lines.foreach(println)
  case Failure(exception) => println(s"Error reading file: ${exception.getMessage}")
}

Reading Entire File Content

For small files, read the entire content into a string:

import scala.io.Source
import scala.util.Using

val content: String = Using(Source.fromFile("config.txt")) { source =>
  source.mkString
}.getOrElse("")

The mkString method concatenates all characters including newlines. For line-by-line processing with line separators preserved:

val linesWithSeparator = Using(Source.fromFile("data.txt")) { source =>
  source.getLines().mkString("\n")
}.getOrElse("")

Handling Character Encoding

Always specify encoding explicitly to avoid platform-dependent behavior:

import scala.io.{Source, Codec}
import scala.util.Using

implicit val codec: Codec = Codec.UTF8

Using(Source.fromFile("utf8-file.txt")) { source =>
  source.getLines().toList
}

For specific encodings:

import java.nio.charset.StandardCharsets

val source = Source.fromFile("iso-file.txt")(Codec(StandardCharsets.ISO_8859_1))

Writing Files with PrintWriter

Scala doesn’t provide native file writing in scala.io, so use Java’s PrintWriter:

import java.io.PrintWriter
import scala.util.Using

val data = List("line 1", "line 2", "line 3")

Using(new PrintWriter("output.txt")) { writer =>
  data.foreach(writer.println)
}

For more control over formatting:

Using(new PrintWriter("output.txt")) { writer =>
  writer.write("Header\n")
  data.foreach(line => writer.write(s"$line\n"))
  writer.write("Footer\n")
}

Writing with Java NIO Files

Java NIO provides modern file writing capabilities with better performance:

import java.nio.file.{Files, Paths, StandardOpenOption}
import java.nio.charset.StandardCharsets

val content = "File content\nSecond line"
val path = Paths.get("output.txt")

Files.write(path, content.getBytes(StandardCharsets.UTF_8))

For appending to existing files:

import scala.jdk.CollectionConverters._

val lines = List("new line 1", "new line 2")
Files.write(
  Paths.get("output.txt"),
  lines.asJava,
  StandardCharsets.UTF_8,
  StandardOpenOption.APPEND
)

Reading and Processing Large Files

For large files, avoid loading everything into memory. Process lines lazily:

import scala.io.Source
import scala.util.Using

Using(Source.fromFile("large-file.txt")) { source =>
  source.getLines()
    .filter(_.contains("ERROR"))
    .take(100)
    .foreach(println)
}

The iterator processes lines on-demand without loading the entire file. This approach scales to files larger than available memory.

Reading Binary Files

For binary data, use Java’s file reading APIs:

import java.nio.file.{Files, Paths}

val bytes: Array[Byte] = Files.readAllBytes(Paths.get("image.png"))
println(s"Read ${bytes.length} bytes")

For streaming binary data:

import java.io.{FileInputStream, BufferedInputStream}
import scala.util.Using

Using(new BufferedInputStream(new FileInputStream("data.bin"))) { stream =>
  val buffer = new Array[Byte](1024)
  var bytesRead = stream.read(buffer)
  
  while (bytesRead != -1) {
    // Process buffer
    println(s"Read $bytesRead bytes")
    bytesRead = stream.read(buffer)
  }
}

Error Handling Patterns

Comprehensive error handling for file operations:

import scala.io.Source
import scala.util.{Using, Try, Success, Failure}
import java.io.FileNotFoundException

def readFileSafely(filename: String): Either[String, List[String]] = {
  Using(Source.fromFile(filename)) { source =>
    source.getLines().toList
  } match {
    case Success(lines) => Right(lines)
    case Failure(_: FileNotFoundException) => 
      Left(s"File not found: $filename")
    case Failure(exception) => 
      Left(s"Error reading file: ${exception.getMessage}")
  }
}

readFileSafely("data.txt") match {
  case Right(lines) => println(s"Read ${lines.size} lines")
  case Left(error) => println(error)
}

Working with CSV Files

Parse CSV data using Source.fromFile:

import scala.io.Source
import scala.util.Using

case class Record(id: Int, name: String, value: Double)

Using(Source.fromFile("data.csv")) { source =>
  source.getLines()
    .drop(1) // Skip header
    .map { line =>
      val Array(id, name, value) = line.split(",")
      Record(id.toInt, name, value.toDouble)
    }
    .toList
}

Writing CSV data:

import java.io.PrintWriter
import scala.util.Using

val records = List(
  Record(1, "Item A", 10.5),
  Record(2, "Item B", 20.3)
)

Using(new PrintWriter("output.csv")) { writer =>
  writer.println("id,name,value")
  records.foreach { record =>
    writer.println(s"${record.id},${record.name},${record.value}")
  }
}

Performance Considerations

Buffer size affects read performance. The default BufferedSource uses 8KB buffers. For custom buffer sizes:

val source = Source.fromFile("large.txt", 65536) // 64KB buffer

For write-heavy operations, use BufferedWriter:

import java.io.{BufferedWriter, FileWriter}
import scala.util.Using

Using(new BufferedWriter(new FileWriter("output.txt"), 65536)) { writer =>
  (1 to 1000000).foreach { i =>
    writer.write(s"Line $i\n")
  }
}

File I/O is synchronous and blocks the thread. For concurrent file operations in high-performance applications, consider using asynchronous I/O libraries or offloading to dedicated thread pools.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.