They Joy of Writing Scala

Watch the video on YouTube

In a new .scala file let’s create an Awesome object implementing the App trait. This is a convenient way of creating an executable program, and has some delayed initialisation benefits compared to the more conventional main method approach.

object Awesome {
    def main(args: Array[String]): Unit = {
        println("Hello, Scala!")
    }
}

Next, let’s define a Product class, with a name and a price field. I am using the val keyword to mark the fields as immutable, since immutability is a big aspect of functional programming. Of course, once an object is created, you can’t modify its fields directly, so you’ll have to create a different object in the heap if you need to update the data. The alternative is to use the var keyword, but, without additional synchronization you’ll lose the thread-safety.

object Awesome {
    val product = Product("Shirt", 30)
    println(product)
}

class Product(val name: String, val price: Double):

end Product

At the moment, we don’t get any meaningful information when printing the Product. So let’s go ahead and define a toString method in our class. Note the following - we are overriding the default toString method, we are using the new Scala 3 class notation to increase readability, and, thanks to this special notation, we can do string interpolation.

object Awesome {
    val product = Product("Shirt", 30)
    println(product)
}

class Product(val name: String, val price: Double):
    override def toString: String = s"$name,$price\n"

end Product

We’ll end up persisting products in a CSV file, and we can enforce this behavior by defining and extending a CSV trait.

object Awesome {
    val product = Product("Shirt", 30)
    println(product)
}

trait CSV {
    def toCSVEntry: String
}

class Product(val name: String, val price: Double):
    override def toString: String = s"$name,$price\n"
    override def toCSVEntry: String = s"$name,$price"

end Product

Finally, let’s also create a helper method which will allow us to change the product price.

object Awesome {
    val product = Product("Shirt", 30)
    println(product)
}

trait CSV {
    def toCSVEntry: String
}

class Product(val name: String, val price: Double):
    override def toString: String = s"$name,$price\n"
    override def toCSVEntry: String = s"$name,$price"
    def increasePrice(value: Double): Unit = price = price + value
end Product

Next, I’m writing a function to retrieve a list of Products from the disk. We’ll open the CSV file, read the text lines, split the values based on the comma, and then convert the bits into entities. The file has to be closed, and the last expression in the function body will be returned. Of course, I/O operations can fail, and we have to use the try / catch construct to ensure our program will not fail.

def fetchProducts(): List[Product] = {
    var products = List [Product]()
    try {
        val source = Source.fromFile("products.csv")
        products = source
            .getLines()
            .map(it =>
                val bits = it.split(",")
                new Product(bits(0), bits(1).toDouble)
            )
            .toList
        source.close
    } catch {
        case e: FileNotFoundException => println("File not found")
    }
    products
}

While this code is self explanatory, it is not really following Scala principles. Let me explain. Let’s refactor the function like this, and analyze it in more detail.

def fetchProductsTwo(): List[Product] = {
    Try(Source.fromFile("products.csv"))
        .fold(_ => List[Product](), { source =>
            val products = source
                .getLines()
                .flatMap(it => {
                    it.split(",") match
                        case Array(name, age)
                            => Some (Product (name, age.toDouble))
                        case _ => None
                })
                .toList
            source.close
            products
        })
}

The Try object is a construct used to handle exceptions in a functional manner. It represents a computation that may either result in a Success object wrapper or in a Failure object associated with an exception. With .fold() we can return an empty List on failure, and perform our file reading on success.

Quick side note, we can use the underscore notation to ignore unused values.

Then, we are doing the same CSV line conversions. When we are splitting lines based on the comma, the result can either be an array of two values, or something else.

Pattern matching is a powerful expressive feature especially in Scala, and we can use it to return the correct entity based on the matched value. Of course, the matched array will become Some Product while any other format might be a corrupted entry, so we are going to ignore it.

Because flatMap is used, the Some and None values are flattened into an Iterator, where all the None values are filtered out, leaving only the Some entries, which end up being the actual Product instances.

With the products available, we can now add a new instance in our list using the Cons operator (::).

object Awesome extends App {
    val product = Product("Shirt", 30)
    val products = fetchProducts()
    val updated = product :: products
}

Again there is a lot to unpack in this single line of code.

    val updated = product :: products

On one hand, immutability is the default in Scala, so we can’t simply add a value to an existing List. Second, since Cons ends in a colon, it is right-associative. That means the method is actually part of the element on the right, which in this case is the List.

Finally, in Scala, operators are just class methods, so this operation is equivalent to this method call.

    val updated = product .:: products

Following the same notion, a simple addition in Scala can also be defined like this.

    val sum = 1.+(2)

Scala can be really expressive, powerful, and fun to work with and there is a reason big companies are relying on it, especially in big data processing or for building in concurrent, distributed systems.

Finally, let’s write the products back to our file.

def persistProducts (products: List[Product]): Unit = {
    new PrintWriter("products.csv") {
        write(products.map(_.toCSVEntry).mkString("\n"))
        close()
    }
}

The list of entities is mapped to CSV lines via a lambda function, and note that since the CSVEntry function doesn’t accept any parameter, the parenthesis can be completely omitted.

Until next time, thank you for reading!