Это довольно много слов, поэтому давайте начнем с простого примера того, о чем эта статья.
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”) }