Skip to content
AgileLogics
5 min read

Higher-Kinded Types

Prerequisites: Type classes, Variance

List, Option, and Future aren't types on their own — they're type constructors that need a type argument (List[Int]). A higher-kinded type parameter abstracts over them: F[_].

Abstracting over containers

trait Functor[F[_]]:
  extension [A](fa: F[A]) def fmap[B](f: A => B): F[B]

given Functor[List] with
  extension [A](fa: List[A]) def fmap[B](f: A => B): List[B] = fa.map(f)

given Functor[Option] with
  extension [A](fa: Option[A]) def fmap[B](f: A => B): Option[B] = fa.map(f)

def doubleAll[F[_]: Functor](numbers: F[Int]): F[Int] = numbers.fmap(_ * 2)

@main def functors(): Unit =
  println(doubleAll(List(1, 2, 3)))
  println(doubleAll(Option(21)))
  println(doubleAll(Option.empty[Int]))
List(2, 4, 6)
Some(42)
None

doubleAll is written once and works for any F with a Functor instance. F[_]: Functor is a context bound on a type constructor.

Monads

Add pure (wrap a value) and flatMap (chain computations) and you get a Monad — the abstraction behind for-comprehensions:

trait Monad[F[_]]:
  def pure[A](a: A): F[A]
  extension [A](fa: F[A])
    def flatMap[B](f: A => F[B]): F[B]
    def map[B](f: A => B): F[B] = fa.flatMap(a => pure(f(a)))

given Monad[Option] with
  def pure[A](a: A): Option[A] = Some(a)
  extension [A](fa: Option[A])
    def flatMap[B](f: A => Option[B]): Option[B] = fa match
      case Some(a) => f(a)
      case None    => None

def addAll[F[_]: Monad](a: F[Int], b: F[Int], c: F[Int]): F[Int] =
  for
    x <- a
    y <- b
    z <- c
  yield x + y + z

@main def monads(): Unit =
  println(addAll(Option(1), Option(2), Option(3)))
  println(addAll(Option(1), None, Option(3)))
Some(6)
None

In practice

You'll rarely define these yourself — Cats provides Functor, Monad, Traverse, and many more, with instances for standard types. But recognizing F[_] lets you read "tagless final" code like def loadUser[F[_]: Monad](id: Int): F[User], which works with any effect type — IO in production and a simple test type in unit tests.