SLC: Easy conversion from normal function to a Contextual one `FunctionN#applyToContext`

Following up on Standard Library: Now open for improvements and suggestions!, I’d like to propose a small addition to all FunctionN traits:

trait Function2[-T1, -T2, +R]:
  def apply(v1: T1, v2: T2): R
  def applyToContext(using v1: T1, v2: T2): R = apply(v1, v2) // this is the new thing I'm proposing

It applies an ordinary function to arguments that come from the implicit/given context.
Prototype:

Motivation

1. Bridging ordinary and contextual functions

Contextual functions (?=>) are supposed to be a hallmark of Scala 3, but there are still issues with ergonomics and interoperability. Much existing code (constructors, companion applys, library functions) takes ordinary parameters. Today, turning (A, B) => R into (A, B) ?=> R needs a hand-written lambda per arity. With applyToContext:

val f: (Config, Logger) => Service = Service.apply
val g: (Config, Logger) ?=> Service = f.applyToContext

2. Boilerplate-free, “no magic” dependency injection

Components stay plain case classes with ordinary constructors (no implicits in their signatures, so they are not “viral”). Only the wiring site uses the context:

object app:
  private given ConnectionPool = ConnectionPool(10) // is private, doesn't need to "leak" out
  lazy val value               = (UserSubscription.apply _).applyToContext // can be in any order
  private given UserDatabase   = (UserDatabaseLive.apply _).applyToContext
  private given EmailService   = (EmailService.apply _).applyToContext

Declaration order does not matter, missing dependencies are reported by the compiler, IDE navigation works, and the components can be swapped for tests. It works on Scala 2 and 3, JVM, JS and Native. More discussion: Reddit: The simplest Dependency Injection.

Why the standard library

Everyone who needs this re-implements 23 overloads (Function0–Function22). The method is trivial, but only a generated stdlib definition can cover all arities consistently.

Open questions

  1. Name: applyToContext, fromContext, toContextual, applyUsing, …?
  2. Placement: a method on FunctionN, or overloads in object Function?
  3. Venue: the prototype targets scala/scala 2.13.x; should it move to scala/scala3 now that the stdlib lives there?

(Disclosure: I’ve used the help of LLM to write up this proposal.)

3 Likes

This I dont think scales to FunctionXXL, similar to .tupled which is meant to be replaced by TupledFunction type class which i really wish would move out of experimental by this point.

I welcome the core idea however

1 Like

Thank you Jamie for your advice! I’ve also implemented it for FunctionXXL.

1 Like

what if one have curried function in hand, say, `(A, B) => (C, D) => R`, and wanting for `(A, B) ?=> (C, D) ?=> R`.

val f: (A, B) => (C, D) => R = ...
val g: (A, B) ?=> (C, D) ?=> R = f.applyToContext.applyToContext

Does that really work ?
I would have expected something would be needed for the inner function

Maybe it’s because of the implicit wrapping in (_: A, _: B) ?=> on the rhs
If that is the case, then it would probably fail with:
(A, B) => (C, D) => (E, F) => R

Does this answer your concerns

Yes !
(Though I still don’t understand how that works)

we should make applyToContext work with dependent function types as well.

How would that look like?

Update:
The PR got moved to the Scala 3 repository (but the idea is still the same).

Also, it’s now implemented with extension methods instead of own methods on FunctionX. And we also have support for > 22 arguments (thanks @bishabosha for your help!).

2 Likes