Методы расширения - детали

Методы расширения (extension methods) позволяют добавлять методы к типу после его определения, т.е. позволяют добавлять новые методы в закрытые классы. Например, предположим, что кто-то другой создал класс Circle:

case class Circle(x: Double, y: Double, radius: Double)

Теперь представим, что необходим метод circumference, но нет возможности изменить исходный код Circle. До того как концепция вывода терминов была введена в языки программирования, единственное, что можно было сделать, это написать метод в отдельном классе или объекте, подобном этому:

object CircleHelpers:
  def circumference(c: Circle): Double = c.radius * math.Pi * 2

Затем этот метод можно было использовать следующим образом:

val aCircle = Circle(2, 3, 5)
// aCircle: Circle = Circle(x = 2.0, y = 3.0, radius = 5.0)
CircleHelpers.circumference(aCircle)
// res0: Double = 31.41592653589793

Методы расширения позволяют создать метод circumference для работы с экземплярами Circle:

extension (c: Circle)
  def circumference: Double = c.radius * math.Pi * 2

В этом коде:

Затем в коде метод circumference можно использовать так же, как если бы он был изначально определен в классе Circle:

aCircle.circumference
// res1: Double = 31.41592653589793

Импорт методов расширения

Представим, что circumference определен в пакете lib - его можно импортировать с помощью

import lib.circumference
aCircle.circumference

Если импорт отсутствует, то компилятор выводит подробное сообщение об ошибке, подсказывая возможный импорт, например так:

value circumference is not a member of Circle, but could be made available as an extension method.

The following import might fix the problem:

  import lib.circumference

Операторы

Синтаксис метода расширения также можно использовать для определения операторов. Примеры:

extension (x: String)
  def < (y: String): Boolean = ...
extension (x: Elem)
  def +: (xs: Seq[Elem]): Seq[Elem] = ...
extension (x: Number)
  infix def min (y: Number): Number = ...

"ab" < "c"
1 +: List(2, 3)
x min 3

Generic расширения

Также возможно расширить generic типы, добавив параметры типа в расширение. Например:

extension [T](xs: List[T])
  def second = xs.tail.head

extension [T: Numeric](x: T)
  def + (y: T): T = summon[Numeric[T]].plus(x, y)

Параметры типа в расширениях также можно комбинировать с параметрами типа в самих методах:

extension [T](xs: List[T])
  def sumBy[U: Numeric](f: T => U): U = ...

Аргументы типа, соответствующие параметрам типа метода, передаются стандартно:

List("a", "bb", "ccc").sumBy[Int](_.length)

Напротив, аргументы типа, соответствующие параметрам типа extension, могут быть переданы, только если на метод ссылаются как на метод без расширения:

sumBy[String](List("a", "bb", "ccc"))(_.length)

Или при передаче обоих аргументов типа:

sumBy[String](List("a", "bb", "ccc"))[Int](_.length)

Расширения также могут принимать предложения using. Например, приведенное выше расширение + можно было бы записать с предложения using:

extension [T](x: T)(using n: Numeric[T])
  def + (y: T): T = n.plus(x, y)

Коллективные расширения

Ключевое слово extension объявляет о намерении определить один или несколько методов расширения для типа, заключенного в круглые скобки. Чтобы определить для типа несколько методов расширения, используется следующий синтаксис:

extension (c: Circle)
  def circumference: Double = c.radius * math.Pi * 2
  def diameter: Double = c.radius * 2
  def area: Double = math.Pi * c.radius * c.radius

Коллективные расширения, подобные этим, являются сокращением для индивидуальных расширений, где каждый метод определяется отдельно. Например, расширение выше - это:

extension (c: Circle)
  def circumference: Double = c.radius * math.Pi * 2

extension (c: Circle)
  def diameter: Double = c.radius * 2

extension (c: Circle)
  def area: Double = math.Pi * c.radius * c.radius

Коллективные расширения также могут принимать параметры типа и содержать предложения using. Пример:

extension [T](xs: List[T])(using Ordering[T])
  def smallest(n: Int): List[T] = xs.sorted.take(n)
  def smallestIndices(n: Int): List[Int] =
    val limit = smallest(n).max
    xs.zipWithIndex.collect { case (x, i) if x <= limit => i }

Вызов методов расширения

Чтобы преобразовать ссылку в метод расширения, компилятор должен знать о методе расширения. В этом случае говорится, что метод расширения применим в точке отсчета.

Существует четыре возможных способа применения метода расширения:

Вот пример первого правила:

trait IntOps:
  extension (i: Int) def isZero: Boolean = i == 0

  extension (i: Int) def safeMod(x: Int): Option[Int] =
    // метод расширения, определенный в той же области видимости - IntOps
    if x.isZero then None
    else Some(i % x)

object IntOpsEx extends IntOps:
  extension (i: Int) def safeDiv(x: Int): Option[Int] =
    // метод расширения, введенный в область видимости через наследование от IntOps
    if x.isZero then None
    else Some(i / x)

trait SafeDiv:
  import IntOpsEx.*

  extension (i: Int) def divide(d: Int): Option[(Int, Int)] =
    // методы расширения импортированы и, следовательно, находятся в области видимости
    (i.safeDiv(d), i.safeMod(d)) match
      case (Some(d), Some(r)) => Some((d, r))
      case _ => None

Согласно второму правилу, метод расширения можно сделать доступным, определив экземпляр given, содержащий его, например так:

given ops1: IntOps()  // приносит safeMod в область видимости
1.safeMod(2)

По третьему и четвертому правилу метод расширения доступен, если он находится в неявной области действия типа получателя или в экземпляре given в этой области. Пример:

class List[T]:
  ...
object List:
  ...
  extension [T](xs: List[List[T]])
    def flatten: List[T] = xs.foldLeft(List.empty[T])(_ ++ _)

  given [T: Ordering]: Ordering[List[T]] with
    extension (xs: List[T])
      def < (ys: List[T]): Boolean = ...
end List

// метод расширения доступен, поскольку он находится в неявной области видимости List[List[Int]]
List(List(1, 2), List(3, 4)).flatten

// метод расширения доступен, поскольку он находится в given Ordering[List[T]], 
// который сам находится в неявной области видимости List[Int]
List(1, 2) < List(3)

Ссылки: