Scala
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
130: Higher-Kinded Types
Look, I'll be honest with you: this is the point in the course where a lot of developers start to feel like they're hitting a wall. Higher-Kinded Types (HKTs) sound like something out of a PhD thesis, but once you strip away the academic jargon, they're actually a very practical tool for reducing repetition in your architecture.
Think about a shipping company. They have a standard set of rules for how to handle any "Package." It doesn't matter if the package is a cardboard box, a wooden crate, or a plastic bin—as long as it is a "Package," the company knows how to put a shipping label on it and how to weigh it. The type of container (box, crate, bin) is the wrapper, and the item inside (a toaster, a vase, a book) is the content. The shipping company doesn't care what the item is, and they don't even care which specific container is being used, as long as that container follows the "Package" standard.
In Scala, a regular generic like List[Int] is like saying "here is a box containing an integer." A Higher-Kinded Type is like talking about the concept of the box itself, regardless of what's inside it. We aren't talking about List[Int] or Option[String]; we're talking about F[A], where F is a placeholder for the container and A is a placeholder for the content.
When a Generic Isn't Enough
You're already used to generics. You can write a function that works for any T. But what if you want to write a function that works for any container? For example, imagine you want a map function. You know List has a map, Option has a map, and Future has a map. You might be tempted to write a trait like this:
trait Mapper[A, B] {
def map(fa: A): B
}
But that doesn't work. A and B here are just types. If you pass in a List[Int] as A, then B becomes List[String]. The trait has no way of knowing that the "container" (the List part) should remain the same while only the "content" (the Int to String part) changes. This is where we need the F[_] syntax.
The Power of the F[_] Notation
The [_] is the magic bit. It tells Scala: "This isn't just a type; it's a type that takes another type." It's a type constructor. When you see F[_], think of it as a hole that needs to be filled with something like List or Option.
Let's build a simple Functor. In functional programming, a Functor is just a fancy name for "something that can be mapped over."
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
Notice how F is defined in the trait header as F[_]. This allows the map method to use F as a container for any A and B. I've used this pattern in production systems to create data-layer abstractions where the business logic doesn't know if the result is wrapped in a Future, an Either, or a List—it just knows that it can map over the result.
Bridging the Gap with Implementations
Defining the trait is the easy part. The real work happens when you provide the implementation for specific containers. Here is how we'd tell Scala how to treat Option and List as Functors:
object FunctorInstances {
implicit val optionFunctor: Functor[Option] = new Functor[Option] {
def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f)
}
implicit val listFunctor: Functor[List] = new Functor[List] {
def map[A, B](fa: List[A])(f: A => B): List[B] = fa.map(f)
}
}
Now, we can write a function that is truly polymorphic. It doesn't care if it's dealing with a list or an option; it just needs F to be a Functor.
def incrementAll[F[_], A](container: F[A])(implicit functor: Functor[F])(f: A => A): F[A] = {
functor.map(container)(f)
}
// Usage:
import FunctorInstances._
val listRes = incrementAll(List(1, 2, 3))(x => x + 1) // Works!
val optRes = incrementAll(Some(1))(x => x + 1) // Also works!
By using HKTs, we've successfully decoupled the operation (mapping a function) from the structure (the container). It's a bit of an investment in syntax, but it prevents you from writing the same logic five times just because your data is wrapped in different types of containers.
📋 Practical Task
Implementing a Generic Container Filter
In this exercise, you will create a typeclass called Filterable. Much like the Functor example in the lesson, Filterable should allow us to filter the contents of a container, provided that container supports filtering.
Requirements:
- Define a trait
Filterable[F[_]]with a methodfilter[A](fa: F[A])(p: A => Boolean): F[A]. - Create an implicit instance of
FilterableforList. - Create an implicit instance of
FilterableforOption(Hint: if the predicate is false, theOptionshould becomeNone). - Write a function called
keepPositiveNumbers[F[_]]that takes a containerF[Int]and uses theFilterableinstance to remove all numbers less than or equal to zero.
Test your code with these cases:
val numbersList = List(-1, 2, -3, 4)
val numbersOpt = Some(-5)
// Should return List(2, 4)
val resultList = keepPositiveNumbers(numbersList)
// Should return None
val resultOpt = keepPositiveNumbers(numbersOpt)
There are no comments for now.