Функция Scala принимает и возвращает тот же тип верхней границы.

Вопрос или проблема

Рассмотрим следующее:

  sealed trait IntTree
  case class Leaf(value: Int) extends IntTree
  case class Branch(left: IntTree, value: Int, right : IntTree)

  def test[A <: IntTree](x: A): A = {
    x match {
      case Leaf(v) => Leaf(v)
      case Branch(l, v, r) => Branch(l, v, r)
    }
  }

В функции test у меня есть параметр с верхней границей A <: IntTree
Я хотел бы, чтобы она работала следующим образом:

test(Leaf(1)) // должен вернуть Leaf
test(Branch(Leaf(1), 1, Leaf(2))) // должен вернуть Branch

val tree: IntTree = Branch(Leaf(1), 1, Leaf(2))
test(tree) // должен вернуть IntTree

Однако, это не компилируется
Как бы, он не может доказать, что созданные Leaf и Branch внутри сопоставления с образцом на самом деле имеют тип A
Возможно ли это выразить без приведения типов во время выполнения?

Лучшее решение, которое я нашел до сих пор, это:

def test[A <: IntTree](x: A): A = {
  x match {
    case Leaf(v) => Leaf(v).asInstanceOf[A]
    case Branch(l, v, r) => Branch(l, v, r).asInstanceOf[A]
  }
}

Не элегантно, но работает

Ответ или решение

Как создать в Scala функцию, которая принимает и возвращает один и тот же верхний предел с использованием обобщённых типов

Ваша задача – реализовать функцию test, которая может принимать и возвращать объекты типа, производного от IntTree. Рассмотрим вашу исходную реализацию и предложим более элегантное решение без использования приведения типов (asInstanceOf).

Изначальная реализация

Вы попытались реализовать функцию следующим образом:

def test[A <: IntTree](x: A): A = {
  x match {
    case Leaf(v) => Leaf(v).asInstanceOf[A]
    case Branch(l, v, r) => Branch(l, v, r).asInstanceOf[A]
  }
}

Хотя этот код компилируется, использование asInstanceOf не является хорошей практикой, так как может привести к ошибкам времени выполнения и нарушает принципы чистоты типизированных языков, таких как Scala.

Причина невозможности компиляции

Проблема в вашем исходном определении функции заключается в том, что компилятор не может гарантировать, что объект, возвращаемый в случае совпадения с Leaf или Branch, соответствует типу A. Дело в том, что Leaf и Branch могут быть не единственными подтипами IntTree, и возвращать их напрямую не соответствует ожидаемому типу.

Элегантное решение

Одно из возможных решений заключается в использовании обобщённых типов в комбинации с шаблонами. Мы можем создать специальный метод, который будет возвращать результат в виде A, благодаря использованию обёрток для каждого случая. Это требует создания абстрактных методов для обработки каждого узла дерева:

sealed trait IntTree {
  def transform: IntTree
}

case class Leaf(value: Int) extends IntTree {
  override def transform: Leaf = this
}

case class Branch(left: IntTree, value: Int, right: IntTree) extends IntTree {
  override def transform: Branch = Branch(left.transform, value, right.transform)
}

def test[A <: IntTree](x: A): A = {
  x.transform.asInstanceOf[A]
}

Объяснение решения

  1. Смешанный интерфейс: Мы добавили метод transform в каждый тип дерева (Leaf и Branch). Это позволяет обрабатывать трансформации дерева на уровне интерфейса без необходимости повторять код.

  2. Типизация: Метод test теперь использует transform, который возвращает новые экземпляры Leaf или Branch, что гарантирует корректность типов.

  3. Удаление приведения: В результате, использование asInstanceOf в test сводится к минимизации, так как transform обеспечивает корректную типизацию.

Использование

Теперь вы можете вызывать функцию test, и она будет работать так, как ожидалось:

val leaf: Leaf = Leaf(1)
val branch: Branch = Branch(Leaf(1), 1, Leaf(2))

val transformedLeaf = test(leaf) // вернёт Leaf(1)
val transformedBranch = test(branch) // вернёт Branch(Leaf(1), 1, Leaf(2))

val tree: IntTree = Branch(Leaf(1), 1, Leaf(2))
val transformedTree = test(tree) // вернёт Branch(Leaf(1), 1, Leaf(2))

Заключение

Изменение вашей реализации добавляет как гибкость, так и безопасность типов, что делает код более чистым и поддерживаемым. Но, действительно ли вам необходимо, чтобы функция возвращала тот же тип, что и входящий? Этот вопрос зависит от контекста использования, и вы должны убедиться, что ваша архитектура дерева позволяет такие трансформации. Надеюсь, это поможет вам создать более устойчивый и элегантный код в Scala!

Оцените материал
Добавить комментарий

Капча загружается...