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
27: Implicits and Given Instances (Scala 3)
I see this all the time when developers move from Java or Python to Scala: they treat given and using as just a "clever" way to avoid typing arguments in a function call. They think of it as a form of dependency injection or a shortcut to hide boilerplate. But if you view it as "magic hidden arguments," you'll eventually hit a wall where the compiler starts complaining and you have no idea why.
Givens aren't "hidden arguments," they are proofs of capability
The mistake is thinking that given is about convenience. In reality, it's about evidence. When you define a given instance, you aren't just providing a value; you are telling the compiler: "I have a way to handle this specific type for this specific purpose."
Let's look at a real-world scenario. Imagine we're building a system that converts objects to JSON. We don't want to write the serialization logic inside every single function. Instead, we want to say: "This function can run, provided there is a JsonWriter available for the type I'm passing in."
case class User(name: String, age: Int)
trait JsonWriter[T]:
def write(value: T): String
// If we did this "manually" without givens:
def serializeUserManual(user: User, writer: JsonWriter[User]): String =
writer.write(user)
val userWriter = new JsonWriter[User]:
def write(value: User): String = s"""{"name": "${value.name}", "age": ${value.age}}"""
// This is tedious. Every time I call serialize, I have to lug the writer around.
serializeUserManual(User("Alice", 30), userWriter)
In the manual version, the JsonWriter is just another parameter. But in a complex system, you might have 10 levels of function calls, and every single one of them needs that writer. Passing it manually is a nightmare. This is where given and using come in. They shift the responsibility of providing the writer from the caller to the scope.
Separating the 'Provision' from the 'Requirement'
In Scala 2, the keyword implicit did everything. It was used to define the value, the parameter, and the conversion. It was confusing as hell. Scala 3 splits this into two distinct concepts: given (the provision) and using (the requirement).
Here is how we rewrite that JSON logic using the Scala 3 way. I personally find this much cleaner because you can glance at a function signature and immediately know which arguments are mandatory and which are contextual.
trait JsonWriter[T]:
def write(value: T): String
// 'using' tells the compiler: "Look in the scope for a 'given' of this type"
def serialize[T](value: T)(using writer: JsonWriter[T]): String =
writer.write(value)
// 'given' tells the compiler: "Here is the evidence that we know how to write a User"
given userWriter: JsonWriter[User] with
def write(value: User): String = s"""{"name": "${value.name}", "age": ${value.age}}"""
// Now, the call is clean. The compiler finds 'userWriter' automatically.
val json = serialize(User("Bob", 25))
Notice that serialize is now generic. It doesn't care if you're passing a User, a Product, or a List, as long as a given JsonWriter exists for that type. If you try to call serialize(123) without defining a given JsonWriter[Int], the code won't even compile. The compiler isn't just looking for a variable; it's verifying that the capability to serialize an Int exists.
Controlling the Scope
One thing to keep in mind: the compiler looks for given instances in the local scope, then in the imported scopes. I often suggest grouping your givens into a trait or an object and importing them specifically when needed. This prevents your global namespace from becoming a dumping ground for instances.
object UserFormatting:
given userWriter: JsonWriter[User] with
def write(value: User): String = s"""{"name": "${value.name}"}"""
// Inside some other service:
import UserFormatting.given
serialize(User("Charlie", 40))
By importing UserFormatting.given, you are explicitly bringing that "evidence" into your current context. It's a powerful pattern because it allows you to switch behaviors (like switching from a JSON formatter to an XML formatter) just by changing an import, without touching the business logic in your serialize function.
📋 Practical Task
Implementing a Locale-Aware Currency Formatter
You are building a financial dashboard. Depending on the user's locale, currencies need to be formatted differently (e.g., $100.00 for US and 100,00 € for EU). You need to implement this using given and using so that the formatting logic is decoupled from the display logic.
Requirements:
- Create a trait
CurrencyFormatterwith a methodformat(amount: Double): String. - Create two
giveninstances ofCurrencyFormatter: one forUSFormatterand one forEUFormatter. - Write a function
printBalance(amount: Double)(using formatter: CurrencyFormatter)that prints the formatted amount to the console. - Demonstrate how to call
printBalanceusing both formatters by controlling the scope (using localgivendefinitions or imports).
Starter Code:
trait CurrencyFormatter:
def format(amount: Double): String
def printBalance(amount: Double)(using formatter: CurrencyFormatter): Unit =
println(s"Your balance is ${formatter.format(amount)}")
// Your implementation here
There are no comments for now.