Это довольно много слов, поэтому давайте начнем с простого примера того, о чем эта статья.

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])
    }
  }
}


Разделение Attempt.apply[E] и PartiallyApplied.apply[A] на две последовательные функции делает API более удобным в использовании, поскольку A может быть определено компилятором, а E должно указываться явно. Этот паттерн довольно популярен в библиотеках, целью которых является предоставление эргономичного API.

В приведенном выше примере мы можем написать такой код:

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

Если не утруждать себя разбиением вызова метода на две последовательные функции, определение становится намного проще, но только за счет удобства на месте вызова:

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”)
}

Портирование на Скала 3

Я столкнулся с этой темой при портировании приведенного выше кода на Scala 3. Итак, давайте продолжим, поступая так наивно:

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])


Никаких сюрпризов. Самым большим изменением является отсутствие фигурных скобок и переключение с implicit на using. Но теперь пришло время поближе познакомиться с новыми функциями Scala 3 и посмотреть, сможем ли мы избавиться от промежуточного класса. Ответом на нашу проблему являются "Полиморфные типы функций". Сначала я немного боролся с новым синтаксисом, поэтому я делюсь им здесь!

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])


Приведенный выше пример call-site продолжит работать без изменений, но промежуточный вспомогательный класс исчез!

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