Неявный класс преобразования и типа
В Scala мы можем использовать по крайней мере два метода для модификации существующих или новых типов. Предположим, мы хотим выразить, что что-то можно количественно определить с помощью Int
. Мы можем определить следующий признак.
Неявное преобразование
trait Quantifiable{ def quantify: Int }
И тогда мы можем использовать неявные преобразования для количественной оценки, например. Строки и списки.
implicit def string2quant(s: String) = new Quantifiable{
def quantify = s.size
}
implicit def list2quantifiable[A](l: List[A]) = new Quantifiable{
val quantify = l.size
}
После их импорта мы можем вызвать метод quantify
для строк и списков. Обратите внимание, что количественный список сохраняет свою длину, поэтому он избегает дорогого обхода списка при последующих вызовах на quantify
.
Типы классов
Альтернативой является определение "свидетеля" Quantified[A]
, в котором указано, что некоторый тип A
можно количественно определить.
trait Quantified[A] { def quantify(a: A): Int }
Затем мы предоставляем экземпляры этого класса типа для String
и List
где-то.
implicit val stringQuantifiable = new Quantified[String] {
def quantify(s: String) = s.size
}
И если мы затем напишем метод, который должен количественно определить его аргументы, напишем:
def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) =
as.map(ev.quantify).sum
Или используя синтаксис, связанный с контекстом:
def sumQuantities[A: Quantified](as: List[A]) =
as.map(implicitly[Quantified[A]].quantify).sum
Но когда использовать какой метод?
Теперь возникает вопрос. Как я могу решить эти две концепции?
То, что я заметил до сих пор.
классы типов
- типы классов допускают синтаксис с учетом синтаксиса
- с классами классов Я не создаю новый объект-обертку при каждом использовании
- синтаксис, связанный с контекстом, больше не работает, если класс типа имеет несколько параметров типа; Представьте, что я хочу количественно оценивать вещи не только с целыми числами, но и со значениями некоторого общего типа
T
. Я хотел бы создать класс типа Quantified[A,T]
неявное преобразование
- так как я создаю новый объект, я могу кэшировать значения там или вычислять лучшее представление; но я должен избегать этого, так как это может произойти несколько раз, и явное преобразование, вероятно, будет вызвано только один раз?
Что я ожидаю от ответа
Представьте один (или более) вариант использования (-ов), в котором разница между обеими концепциями имеет значение и объясняет, почему я бы предпочел один из них. Также объяснение сущности двух понятий и их отношение друг к другу было бы неплохо даже без примера.
Ответы
Ответ 1
Пока я не хочу дублировать свой материал из Scala In Depth, я считаю, что стоит отметить, что типы классов/типы являются бесконечно более гибкими.
def foo[T: TypeClass](t: T) = ...
имеет возможность выполнять поиск в своей локальной среде для класса типа по умолчанию. Однако я могу переопределить поведение по умолчанию в любое время одним из двух способов:
- Создание/импорт экземпляра неявного типа класса в области видимости для неявного поиска коротких замыканий
- Непосредственно передать класс типа
Вот пример:
def myMethod(): Unit = {
// overrides default implicit for Int
implicit object MyIntFoo extends Foo[Int] { ... }
foo(5)
foo(6) // These all use my overridden type class
foo(7)(new Foo[Int] { ... }) // This one needs a different configuration
}
Это делает типы классов бесконечно более гибкими. Другое дело, что классы классов/черты поддерживают неявный поиск лучше.
В первом примере, если вы используете неявное представление, компилятор выполнит неявный поиск для:
Function1[Int, ?]
Что будет выглядеть сопутствующим объектом Function1
и сопутствующим объектом Int
.
Обратите внимание, что Quantifiable
нигде не встречается в неявном поиске. Это означает, что вы должны поместить неявное представление в объект пакета или импортировать его в область видимости. Это больше работает, чтобы помнить, что происходит.
С другой стороны, класс типа является явным. Вы видите, что он ищет в сигнатуре метода. У вас также есть неявный поиск
Quantifiable[Int]
который будет выглядеть в Quantifiable
сопутствующем объекте и Int
сопутствующем объекте. Значение, которое вы можете предоставить по умолчанию и новые типы (например, класс MyString
), может предоставить значение по умолчанию в своем сопутствующем объекте и будет неявным образом искать.
В общем, я использую классы типов. Они являются более гибкими для первоначального примера. Единственное место, где я использую неявные преобразования, - это использование уровня API между оболочкой Scala и библиотекой Java, и даже это может быть "опасно", если вы не будете осторожны.
Ответ 2
Одним из критериев, который может вступить в игру, является то, как вы хотите, чтобы новая функция "чувствовала"; используя неявные преобразования, вы можете заставить его выглядеть так, как будто это еще один метод:
"my string".newFeature
... при использовании классов типов он всегда будет выглядеть так, как вы вызываете внешнюю функцию:
newFeature("my string")
Одна вещь, которую вы можете достичь с помощью классов типов, а не с неявными преобразованиями, - это добавление свойств к типу, а не к экземпляру типа. Затем вы можете получить доступ к этим свойствам, даже если у вас нет экземпляра доступного типа. Канонический пример:
trait Default[T] { def value : T }
implicit object DefaultInt extends Default[Int] {
def value = 42
}
implicit def listsHaveDefault[T : Default] = new Default[List[T]] {
def value = implicitly[Default[T]].value :: Nil
}
def default[T : Default] = implicitly[Default[T]].value
scala> default[List[List[Int]]]
resN: List[List[Int]] = List(List(42))
В этом примере также показано, как понятия тесно связаны: типы классов не были бы столь же полезными, если бы не было механизма для создания бесконечного числа их экземпляров; без метода implicit
(а не преобразования, правда), я мог бы иметь только конечное число типов с свойством Default
.
Ответ 3
Вы можете придумать разницу между двумя методами по аналогии с функциональным приложением, только с именованной оболочкой. Например:
trait Foo1[A] { def foo(a: A): Int } // analogous to A => Int
trait Foo0 { def foo: Int } // analogous to Int
Экземпляр первого инкапсулирует функцию типа A => Int
, тогда как экземпляр последнего уже применяется к A
. Вы можете продолжить шаблон...
trait Foo2[A, B] { def foo(a: A, b: B): Int } // sort of like A => B => Int
таким образом вы могли бы думать о Foo1[B]
вроде как частичное приложение Foo2[A, B]
для некоторого экземпляра A
. Отличный пример этого был написан Майлсом Сабином как "Функциональные зависимости в Scala" .
Я действительно считаю, что в принципе:
- "сутенерство" класса (через неявное преобразование) - это случай "нулевого порядка"...
- Объявление класса типов - это случай первого порядка...
- многопараметрические типы с платами (или что-то вроде fundeps) - общий случай.