Scala 3 Quick Tip: Replacing the partially applied pattern with polymorphic functions

Niklas Klein

--

The “Helix Bridge” in Singapore at night with red illumination
“Helix Bridge” by @robynnexy

That’s quite a mouthful of words, so let’s start with a simple example of what this is article is about.

type Database[A] = Kleisli[IO, Session, A]object Database {
def apply[A](f: Session => IO[A]): Database[A] = Kleisli(f)
type Attempt[E, A] = Database[Either[E, A]] object Attempt {
def apply[E <: Throwable]: Database.Attempt.PartiallyApplied[E] =
new PartiallyApplied[E]
final class PartiallyApplied[E <: Throwable] {
def apply[A](f: Session => IO[A])(
implicit tag: ClassTag[E]
): Database.Attempt[E, A] = Database(f(_).attemptNarrow[E])
}
}
}

By splitting Attempt.apply[E] and PartiallyApplied.apply[A] into two consecutive functions, the API is more convenient to use, because A can be inferred by the compiler, while E has to be explicitly provided. This pattern is quite popular in libraries that aim to provide an ergonomic API.

With the example above, we are able to write code like this:

Database.Attempt[IllegalStateException] { session =>
IO.pure(“success”)
}

If we don’t bother to split the method call into two consecutive functions, the definition becomes a lot easier, but only by sacrificing convenience on the call-site:

object Attempt {
def apply[E <: Throwable, A](f: Session => IO[A])(
implicit tag: ClassTag[E]
): Database.Attempt[E, A] = Database(f(_).attemptNarrow[E])
}
// we want to avoid that ------vvvvvv
Attempt[IllegalStateException, String] { session =>
IO.pure(“success”)
}

Porting to Scala 3

I encountered this topic while porting the code above to Scala 3. So let’s continue by doing so naively:

type Database[A] = Kleisli[IO, Session, A]object Database:
def apply[A](f: Session => IO[A]): Database[A] = Kleisli(f)
type Attempt[E, A] = Database[Either[E, A]]object Attempt:
def apply[E <: Throwable]: Database.Attempt.PartiallyApplied[E] =
new PartiallyApplied[E]
final class PartiallyApplied[E <: Throwable]:
def apply[A](f: Session => IO[A])(
using ClassTag[E]
): Database.Attempt[E, A] = Database(f(_).attemptNarrow[E])

No surprises here. The biggest change is the omitting of braces and the switch from implicit to using. But now it’s time to take a closer look at the new Scala 3 features and see if we can get rid off the intermediate class. The answer to our problem are “Polymorphic Function Types”. I initially struggled a bit with the new syntax, so I’m sharing it here!

type Database[A] = Kleisli[IO, Session, A]object Database:
def apply[A](f: Session => IO[A]): Database[A] = Kleisli(f)
type Attempt[E, A] = Database[Either[E, A]] object Attempt:
def apply[E <: Throwable](
using ClassTag[E]
): [A] => (Session => IO[A]) => Database.Attempt[E, A] = [A] =>
(f: (Session => IO[A])) => Database(f(_).attemptNarrow[E])

The call-site example from above will keep working without changes, but the intermediate helper class is gone!

Database.Attempt[IllegalStateException] { session =>
IO.pure(“success”)
}

--

--

No responses yet