Working with Files
Here you will find a curated collection of code examples that show the library in action.
Scores
Imagine we have a file with scores in the following format (<name>:<score>):
michel:21839
zeta:9929
thanh:20933
tonio:12340
arman:6969
gabriel:32123
We want to read the file, parse the scores, print them to the console and also append new scores to the file.
In order to perform these operations we first need to create a representation of the score in Scala:
case class Score(name: String, score: Int):
def show: String = s"$name:$score"
And then a function that parses a string into a Either[Throwable, Score]:
def parseScore(strScore: String): Either[Throwable, Score] =
Either.catchNonFatal( // (1)
strScore.split(':') match
case Array(name, score) => Score(name, score.toInt)
case _ => Score("Cant parse this score", -1) // (2)
)
The method is going to return a Left or a Right according to the outcome of the score parsing. The function consists in two steps:
(1) Either.catchNonFatal is a function that catches exceptions and wraps them in a Left value.
(2) If the score cannot be parsed, we return a default score.
We can now create our script:
for
lines <- path.readLines // (1)
scores <- lines.traverse(parseScore(_).liftTo[IO]) // (2)
_ <- IO(scores.foreach(score => println(score.show))) // (3)
_ <- path.appendLine(Score("daniela", 100).show) // (4)
yield ()
(1) We first read every line of the file as part of a list of strings.
(2) Then, we use the traverse method to apply an effectful function to every element in the list and then turn the type inside the list, inside out (like a pure foreach). We are parsing the scores and converting the Either type to an IO type (List[String] => List[Either[Throwable, Score]] => List[IO[Score]] => IO[List[Score]).
(3) After that, we print every score to the console.
Finally, we append a new score to the file (4) via the appendLine method.
Here is the complete example of the Scores example:
import cats.syntax.all.*
import cats.effect.{IO, IOApp}
import fs2.io.file.Path
import catscript.syntax.path.*
object Scores extends IOApp.Simple:
case class Score(name: String, score: Int):
def show: String = s"$name:$score"
def parseScore(strScore: String): Either[Throwable, Score] =
Either.catchNonFatal(
case Array(name, score) => Score(name, score.toInt)
case _ => Score("Cant parse this score", -1)
)
val path = Path("src/main/resources/scores.txt")
def run: IO[Unit] =
for
lines <- path.readLines
scores <- lines.traverse(parseScore(_).liftTo[IO])
_ <- IO(scores.foreach(score => println(score.show)))
_ <- path.appendLine(Score("daniela", 100).show)
yield ()
end Scores
import cats.syntax.all.*
import cats.effect.{IO, IOApp}
import fs2.io.file.Path
import catscript.Catscript
object Scores extends IOApp.Simple:
case class Score(name: String, score: Int):
def show: String = s"$name:$score"
def parseScore(strScore: String): Either[Throwable, Score] =
Either.catchNonFatal(
strScore.split(':') match
case Array(name, score) => Score(name, score.toInt)
case _ => Score("Cant parse this score", -1)
)
val path = Path("src/main/resources/scores.txt")
def run: IO[Unit] =
for
lines <- Catscript.readLines(path)
scores <- lines.traverse(parseScore(_).liftTo[IO])
_ <- IO(scores.foreach(score => println(score.show)))
_ <- Catscript.appendLine(path, Score("daniela", 100).show)
yield ()
end Scores
import cats.syntax.all.*
import cats.effect.{IO, IOApp}
import fs2.Stream
import fs2.io.file.{Files, Path, Flags}
object Scores extends IOApp.Simple:
case class Score(name: String, score: Int):
def show: String = s"$name:$score"
def parseScore(strScore: String): Either[Throwable, Score] =
Either.catchNonFatal(
strScore.split(':') match
case Array(name, score) => Score(name, score.toInt)
case _ => Score("Cant parse this score", -1)
)
val path = Path("src/main/resources/scores.txt")
def run: IO[Unit] =
Files[IO].readUtf8Lines(path)
.evalMap(parseScore(_).liftTo[IO])
.evalTap(scores => IO.println(scores.show))
.last
.flatMap( _ =>
Stream.emit(s"\n${Score("daniela", 100).show}")
.through(Files[IO].writeUtf8(path, Flags.Append))
)
.compile
.drain
end Scores
Uppercase
You can also manipulate entire files in one go. In this example, we are going to read a file, convert it to uppercase, and write it to another file.
As the first thing, we are going to define the locations of the original file and the new file:
val path = Path("src/main/resources/quijote.txt")
val upperPath = Path("src/main/resources/quijote_screaming.txt")
We are going to use El Quijote for this example.
Then, we can easily read the file, perform the transformation, and write it to a new file (we can also overwrite if we use the same path):
for
file <- path.read
_ <- upperPath.write(file.toUpperCase)
yield ()
Note that we loaded the whole file as a string, converted it to uppercase, and finally, wrote it to the new file.
Here, the complete script:
import cats.effect.{IO, IOApp}
import fs2.io.file.Path
import catscript.syntax.path.*
object Uppercase extends IOApp.Simple:
val path = Path("src/main/resources/quijote.txt")
val upperPath = Path("src/main/resources/quijote_screaming.txt")
def run: IO[Unit] =
for
file <- path.read
_ <- upperPath.write(file.toUpperCase)
yield ()
end Uppercase
import cats.effect.{IO, IOApp}
import fs2.io.file.Path
import catscript.syntax.Catscript
object Uppercase extends IOApp.Simple:
val path = Path("src/main/resources/quijote.txt")
val upperPath = Path("src/main/resources/quijote_screaming.txt")
def run: IO[Unit] =
for
file <- Catscript.read(path)
_ <- Catscript.write(upperPath, file.toUpperCase)
yield ()
end Uppercase
import cats.effect.{IO, IOApp}
import fs2.io.file.{Path, Files}
object Uppercase extends IOApp.Simple:
val path = Path("src/main/resources/quijote.txt")
val upperPath = Path("src/main/resources/quijote_screaming.txt")
def run: IO[Unit] =
Files[IO].readUtf8(path)
.map(_.toUpperCase)
.through(Files[IO].writeUtf8(upperPath))
.compile
.drain
end Uppercase
Places
Catscript can handle binary files in custom binary formats since it includes scodec. In this example, we are going to create a binary file with a list of places.
We start by defining type representations of the data we are going to work with:
case class Place(position: Int, contestant: String)
Then we are going to use the scodec library to create a codec for the Place type.
A first approach would be to create your own custom codec using the combinators of scodec:
import scodec.codecs.*
given placeCodec: Codec[Place] = (uint8 :: utf8).as[Place]
But since Scala 3, you can also use the derives feature of Scala 3 to automatically derive a codec for your data types:
case class Place(position: Int, contestant: String) derives Codec
That will automatically create a given instance of Codec[Place] for you!
We can now create a file with a list of places, read it, and print it to the console:
for
exists <- path.exists // (1)
_ <- path.createFile.unlessA(exists) // (2)
_ <- path.writeAs[Place](Place(1, "Michael Phelps")) // (3)
place <- path.readAs[Place] // (4)
_ <- IO.println(place)
yield ()
The scripts start by checking the file in the path value exists (1). If it does, we create the file (2) using the unlessA combinator. It executes the effect if the condition is false, similar of doing a if exists then IO.unit else path.createFile.
Then, when can automatically read and write the Place type to the file using the writeAs and readAs methods (3 and 4). That is thanks to the given codec in scope we created before! It handles the encoding and decoding of the binary file automatically.
Here is the complete script of the Places example:
import cats.syntax.applicative.*
import cats.effect.{IO, IOApp}
import fs2.io.file.Path
import scodec.codecs.*
import scodec.Codec
import catscript.syntax.path.*
object Place extends IOApp.Simple:
case class Place(position: Int, contestant: String) derives Codec
val path = Path("src/main/resources/place.data")
def run: IO[Unit] =
for
exists <- path.exists
// Equivalent of doing `if (exists) IO.unit else path.createFile`
_ <- path.createFile.unlessA(exists)
_ <- path.writeAs[Place](Place(1, "Michael Phelps"))
place <- path.readAs[Place]
_ <- IO.println(place)
yield ()
end Place
import cats.syntax.applicative.*
import cats.effect.{IO, IOApp}
import fs2.io.file.Path
import scodec.codecs.*
import scodec.Codec
import catscript.syntax.Catscript
object Place extends IOApp.Simple:
case class Place(position: Int, contestant: String) derives Codec
val path = Path("src/main/resources/place.data")
def run: IO[Unit] =
for
exists <- Catscript.exists(path)
// Equivalent of doing `if (exists) IO.unit else path.createFile`
_ <- Catscript.createFile(path).unlessA(exists)
_ <- Catscript.writeAs[Place](path, Place(1, "Michael Phelps"))
place <- Catscript.readAs[Place](path)
_ <- IO.println(place)
yield ()
end Place
import cats.syntax.applicative.*
import cats.effect.{IO, IOApp}
import fs2.Stream
import fs2.io.file.Path
import fs2.interop.scodec.*
import scodec.codecs.*
import scodec.Codec
object Place extends IOApp.Simple:
case class Place(position: Int, contestant: String) derives Codec
val path = Path("src/main/resources/place.data")
def run: IO[Unit] =
for
exists <- Files[IO].exists(path)
// Equivalent of doing `if (exists) IO.unit else path.createFile`
_ <- Files[IO].createFile(path).whenA(exists)
_ <- Stream.emit(Place(1, "Michael Phelps"))
.covary[IO]
.through(StreamEncoder.many(placeCodec).toPipeByte)
.through(Files[IO].writeAll(path))
.compile
.drain
_ <- Files[IO].readAll(path)
.through(StreamDecoder.many(placeCodec).toPipeByte)
.evalTap(place => IO.println(place))
.compile
.drain
yield ()
end Place