Skip to Content
Course content

175: Context Functions in Scala 3

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've likely spent a fair amount of time using using clauses in Scala 3 to handle dependency injection or configuration. It works, but it has a "viral" quality: if a function at the bottom of your call stack needs a RequestContext, every single function above it in the chain must also declare using ctx: RequestContext just to pass it along. It's tedious, and it clutters your method signatures.

Context functions (introduced as a way to make "contextual abstractions" first-class citizens) solve this by allowing you to define a type that is a function expecting an implicit value. Instead of a method that uses a context, you create a method that returns a context-aware computation.

The "Why isn't this executing?" bug

When I first started using context functions, I made a mistake that almost every developer makes. I confused a function that requires a context with a function that returns a context function. Look at this code for a database session manager:

case class DbSession(id: String)

// We define a type alias for a context function
type DbAction[A] = DbSession ?=> A

def fetchUserName(userId: Int): DbAction[String] = {
  val session = summon[DbSession]
  s"User $userId fetched via session ${session.id}"
}

def main(): Unit = {
  given DbSession("session-123")
  
  // I expected this to print: "User 42 fetched via session session-123"
  println(fetchUserName(42)) 
}

If you run this, you won't see the user's name. Instead, you'll see something like DbAction(Function1). Why? Because fetchUserName didn't actually execute the logic; it returned a function object that is waiting for a DbSession to be provided.

I had fallen into the trap of treating the context function as if it were a regular method with a using parameter. In a regular method, the implicit is resolved at the moment the method is called. In a context function, the ?=> syntax means the function itself is the value being returned, and it won't execute its body until that function is actually invoked.

Invoking the context function

To fix this, you have to realize that fetchUserName(42) returns a function of type DbSession => String. To get the string out, you need to call that returned function. Since we have a given DbSession in scope, Scala 3 can handle this automatically if we apply the function.

Here is the corrected version:

case class DbSession(id: String)
type DbAction[A] = DbSession ?=> A

def fetchUserName(userId: Int): DbAction[String] = {
  val session = summon[DbSession]
  s"User $userId fetched via session ${session.id}"
}

def main(): Unit = {
  given DbSession("session-123")
  
  // Notice the extra parentheses! 
  // fetchUserName(42) returns the context function.
  // The second set of () invokes it, pulling the 'given' from the scope.
  val result = fetchUserName(42)(using summon[DbSession]) 
  
  // Or more simply, if you are in a context where you can just apply it:
  val simpleResult = fetchUserName(42)
  // Wait, this still returns the function. To run it:
  println(fetchUserName(42)(summon[DbSession]))
}

Actually, the most common way to use these is to have a "runner" that handles the execution, which keeps the business logic clean of the plumbing.

Decoupling execution from definition

The real power here isn't just avoiding using in signatures; it's the ability to pass the "intent" to do something around without providing the context yet. I prefer to create a run method that manages the lifecycle of the context.

case class DbSession(id: String)
type DbAction[A] = DbSession ?=> A

object DbRunner {
  def run[A](action: DbAction[A], session: DbSession): A = {
    // We provide the session as a given locally, then call the action
    given DbSession = session
    action
  }
}

def fetchUserName(userId: Int): DbAction[String] = {
  val session = summon[DbSession]
  s"User $userId fetched via session ${session.id}"
}

def updateLastLogin(userId: Int): DbAction[Unit] = {
  val session = summon[DbSession]
  println(s"Updating user $userId in session ${session.id}")
}

// Now we can compose these without mentioning DbSession in the signatures
def userMaintenance(userId: Int): DbAction[String] = {
  updateLastLogin(userId)
  fetchUserName(userId)
}

def main(): Unit = {
  val session = DbSession("prod-db-01")
  val name = DbRunner.run(userMaintenance(42), session)
  println(name)
}

Notice how userMaintenance doesn't need to know how to create a session or even explicitly pass one along. It just returns a DbAction. We've successfully separated the description of the work from the execution of the work.

  • T ?=> A: This is the type of a context function. It means "a function that, given an implicit T, returns an A".
  • summon[T]: Used inside the context function to grab the actual value of the context.
  • First-class status: Because DbAction[A] is just a type alias for a function, you can put it in a List, pass it to other functions, or map over it.



📋 Practical Task

Building a Context-Aware Permission Guard

You are building a security layer for an API. Instead of passing a User object through every single service method, you want to use a Context Function to handle authorization.

Requirements:

  1. Define a case class User(name: String, isAdmin: Boolean).
  2. Create a type alias type AuthAction[A] = User ?=> A.
  3. Implement a function deleteDatabase(): AuthAction[String]. Inside this function:
    • Use summon[User] to check the current user.
    • If the user is an admin, return "Database deleted successfully".
    • If the user is not an admin, throw a RuntimeException("Unauthorized").
  4. Implement a function viewProfile(userId: Int): AuthAction[String] that returns s"Profile for $userId" (this should be accessible to any user).
  5. Create a SecurityRunner object with a method run[A](action: AuthAction[A], user: User): A that executes the action using the provided user.
  6. In your main method, test both viewProfile and deleteDatabase with both an admin user and a regular user to verify that the permission guard works correctly.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.