DEV Community

Cover image for Scala 102: Collections and Monads
krlz
krlz

Posted on

Scala 102: Collections and Monads

Hey there, welcome back to our Scala series! Last time, we introduce the basic types. But today, we're talking about collections and implementing monads on lists.

Let's start talking about Lists specifically! They're immutable, which means once you create them, they can't be changed - but that's not a bad thing! You can still create new lists from existing ones, like a boss. Lists can hold any type of data you want - even lists within lists! It's super useful in functional programming and will make your coding life a whole lot easier.

val myList1 = List(1, 2, 3, 4, 5)
val myList2 = List("apple", "banana", "cherry")
val myList3 = List(1.0, 2.0, 3.0, 4.0, 5.0)
Enter fullscreen mode Exit fullscreen mode

As you can see, you can create lists of any type in Scala.

But what about lists inside lists? You betcha! Here's an example of a list containing other lists:

val myNestedList = List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9))
Enter fullscreen mode Exit fullscreen mode

In this example, we've created a list that contains three other lists, each with three elements.

Now, let's take a look at a method that generates lists in lists of lists and then applies a single method that flatMap all of them. Essentially, we will generate a nested list structure and then flatten it into a single list.

def generateNestedLists(numLists: Int, numElements: Int): List[List[List[Int]]] = {
List.fill(numLists)(List.fill(numElements)(List.fill(numElements)(scala.util.Random.nextInt(10))))
}

val myNestedList = generateNestedLists(3, 3)

val myFlatList = myNestedList.flatMap(x => x.flatMap(y => y))

Enter fullscreen mode Exit fullscreen mode

In this example, we've created a method called "generateNestedLists" that takes two parameters: the number of nested lists and the number of elements in each nested list. The method generates random integers between 0 and 10 for each element of each nested list. We then call this method and generate a nested list with 3 nested lists, each with 3 elements.

Finally, we apply the flatMap method to the nested list to flatten it into a single list.

The result of "myFlatList" will be a single list containing all of the elements of the nested list.

But... WHAT IS FLATTEN?!

Image description

FLATMAP

Ah, flatMap - a magical incantation in the world of Scala programming! It's like having your own TARDIS, allowing you to traverse multiple dimensions of lists in a single bound.

But what is flatMap, you ask? Well, let me tell you my friend. FlatMap is a method in Scala that takes a list, applies a function to each element in the list, and then flattens the resulting lists into a single list.

Think of it like having a pile of books in your room, a bookshelf in your hallway, and a library on the other side of the world. FlatMap's like having a teleporter that can transport all of your books from your room and the bookshelf to the library in one go!

But seriously, flatMap is a powerful tool for working with nested lists in Scala. It saves you from the headache of having to write complex code to iterate through each nested list, apply a function to each element, and then process the resulting lists.

FlatMap simplifies all of that, allowing you to quickly and easily transform nested lists into a single list.

Here's an example. Let's say you have a nested list of numbers like this:

val myList = List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9))
Enter fullscreen mode Exit fullscreen mode

You can use flatMap to transform this into a single list like this:

val myFlatList = myList.flatMap(x => x)
Enter fullscreen mode Exit fullscreen mode

The result of myFlatList will be a single list containing all the elements of the original nested list.

It's like having a genie that can grant your wishes with just one simple command!

So, if you're feeling overwhelmed with nested lists in your Scala code, remember the magic word: flatMap. It's like having the power of a wizard and the ease of a genie in a single command - not to mention it'll save you from drowning in a sea of nested list headaches.

Image description

Monads in collections

Now, let me tell you something even cooler - implementing monads on collections. A monad is a superpower use only in inmutable values, allowing you to encapsulate and sequence operations in a sleek and composable way. And in Scala, a monad has two operations - map and flatMap as we describe before.

Basically, you can perform a chain of operations on a collection in a fun and organized way without worrying about the nitty-gritty details beforehand.

You could even use monads on a list of integers and make it do all sorts of crazy things. Multiply the numbers by two? Absolutely! Add three to each result? Heck yeah! And don't even get us started on filtering out the numbers that are greater than 10... It's like magic!

Here some more examples:

  • A list of email addresses that need to be validated before sending a message.
val emails = List("example1@example.com", "example2@invalid", "example3@example.com")

val validEmails = emails.filter(email => email.matches("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}"))

println(validEmails)
Enter fullscreen mode Exit fullscreen mode
  • A shopping cart with items that need to be computed for the total price including tax and discount.
case class CartItem(name: String, price: Double, quantity: Int)

val cart = List(CartItem("apple", 0.99, 3), CartItem("banana", 1.49, 2), CartItem("orange", 0.89, 5))

val totalPrice = cart.map(item => item.price * item.quantity).sum * 1.08 * 0.9

println(totalPrice)
Enter fullscreen mode Exit fullscreen mode
  • A list of transactions that need to be sorted by date.
case class Transaction(date: String, amount: Double)

val transactions = List(Transaction("2022-01-01", 100.0), Transaction("2022-01-02", 200.0),
Transaction("2022-01-03", 50.0), Transaction("2022-01-04", 300.0))

val sortedTransactions = transactions.sortBy(_.date)

println(sortedTransactions)
Enter fullscreen mode Exit fullscreen mode
  • A list of orders that need to be grouped by customers for easier tracking and analysis.
case class Order(id: Int, customer: String, amount: Double)

val orders = List(Order(1, "John", 100.0), Order(2, "Jane", 200.0), Order(3, "John", 50.0), Order(4, "Jane", 300.0))

val groupedOrders = orders.groupBy(_.customer)

println(groupedOrders)
Enter fullscreen mode Exit fullscreen mode

Image description

The Monads

When it comes to programming, monads are like the Swiss Army knives of the software world. They can do a lot of things, but they’re most well-known for their ability to combine different types of data. And when it comes to Scala, monads are no exception.

Scala has a number of monads, including List, Try, and Option. Each of these has its own unique set of capabilities, but when you combine them, you get some really powerful results.

Let’s start with List. This is a type of collection that can contain any number of elements. It’s great for storing data and organizing it into a structure. You can use List to store and manipulate data in a variety of ways.

Next, let’s look at Try. This is a monad that helps you handle errors and exceptions in a more organized way. It’s great for dealing with unexpected results and ensuring that your code is robust.

Finally, there’s Option. This is a monad that helps you handle “optional” values. It’s great for dealing with missing data or values that may not always be present.

When you combine these three monads, you get some really powerful results. For example, you can use List to store a collection of values, Try to handle errors and exceptions, and Option to handle optional values. This combination is incredibly powerful and can help you build robust, reliable code.

But here’s the thing: monads can be tricky to understand and use. So if you’re new to Scala, it’s best to start with the basics and work your way up. Once you’ve got the hang of it, you can start to combine different monads to get the most out of them.

And if you’re feeling brave, you can even try combining all three. Just don’t say we didn’t warn you – it’s like playing with fire, and you might end up getting burned. Or, you know, you might just end up with a really powerful piece of code. Either way, you’ll be the one who has to take the blame… or the credit.

Image description

Alright, let's take a crack at combining List, Try, and Option in a complex example using Pokémon.

Say we want to write a program that takes a list of Pokémon names, fetches their data from an API, and then does the following:

  1. For each Pokémon, we want to save their name and their type(s) to a database.
  2. If any errors occur during the process, we want to log them and move on to the next Pokémon.
  3. If any Pokémon data is missing or unknown, we want to log that as well, but still save the available data.

To do this, we can use the combination of List, Try, and Option monads.

First, we'll create a List of Pokémon names:

val pokemonNames = List("Pikachu", "Bulbasaur", "Charmander", "Squirtle")
Enter fullscreen mode Exit fullscreen mode

Next, we'll create a function that takes a Pokémon name and returns their data from the PokéAPI:

import scalaj.http._
import scala.util.{Try, Success, Failure}
import play.api.libs.json._

case class Pokemon(name: String, types: Option[List[String]])

def getPokemonData(name: String): Try[Pokemon] = {
val response = Http(s"https://pokeapi.co/api/v2/pokemon/$name").asString
val json: JsValue = Json.parse(response.body)
Try {
Pokemon(
(json \ "name").as[String],
(json \ "types").asOpt[List[JsObject]].map(types => types.map(obj => (obj \ "type" \ "name").as[String]))
)
}
}
Enter fullscreen mode Exit fullscreen mode

This function fetches the Pokémon data from the API and returns a Try, since there may be errors in the API response. We're also using Play JSON to parse the API response and extract the relevant data.

Now, we can use the map and flatMap methods of the List monad to execute the getPokemonData function for each Pokémon name and store the data in a database:

val db = // database connection here

pokemonNames.flatMap(name =>
getPokemonData(name) match {
case Success(pokemon) =>
val types = pokemon.types.map(_.mkString(",")).getOrElse("Unknown")
val sql = s"INSERT INTO pokemon (name, types) VALUES ('${pokemon.name}', '$types')"
try {
db.execute(sql)
} catch {
case e: Exception =>
println(s"Error saving $name to database: ${e.getMessage}")
}
Some(pokemon)
case Failure(e) =>
println(s"Error fetching $name from API: ${e.getMessage}")
None
}
)
Enter fullscreen mode Exit fullscreen mode

This code uses flatMap to execute the getPokemonData function for each Pokémon name and then either return the Pokémon data or a None value, depending on whether any errors occurred. If a Pokémon's data is successfully retrieved, the name and type(s) are saved in the database using an SQL statement. If any errors occur, they are logged to the console.

Finally, we can use the foldLeft method of the List monad to log any missing or unknown data:

pokemonList
.foldLeft(List.empty[String])((missingData, pokemon) =>
if (pokemon.types.isEmpty) s"${pokemon.name}: Missing type information" :: missingData
else missingData
)
.foreach(println)
Enter fullscreen mode Exit fullscreen mode

This code uses foldLeft to iterate through the list of Pokémon and create a new list of missing or unknown data. If a Pokémon's type(s) are not available, their name and a message are added to the missingData list. Finally, the missing data is logged to the console using the foreach method.

And there you have it – a complex example that combines List, Try, and Option to fetch Pokémon data, save it in a database, and log any errors or missing data. Just be careful – with great power comes great responsibility.

We still need to talk some more about collections, thats why this post will continue next time!.

Top comments (0)