Вопрос или проблема
Рассмотрим следующее:
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]
}
Объяснение решения
-
Смешанный интерфейс: Мы добавили метод
transform
в каждый тип дерева (Leaf
иBranch
). Это позволяет обрабатывать трансформации дерева на уровне интерфейса без необходимости повторять код. -
Типизация: Метод
test
теперь используетtransform
, который возвращает новые экземплярыLeaf
илиBranch
, что гарантирует корректность типов. -
Удаление приведения: В результате, использование
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!