В 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]
неявное преобразование
- поскольку я создаю новый объект, я могу кэшировать там значения или вычислить лучшее представление; но следует ли мне этого избегать, поскольку это может происходить несколько раз, а явное преобразование, вероятно, будет вызвано только один раз?
Чего я жду от ответа
Представьте один (или несколько) вариантов использования, в которых разница между обеими концепциями имеет значение, и объясните, почему я предпочитаю одно другому. Также было бы неплохо объяснить суть двух концепций и их связь друг с другом, даже без примеров.
источник
size
список в значении и сказать , что это позволяет избежать дорогостоящего обхода списка на последующих вызовы количественных, но на каждый вашем призыв к сработали сначала заново, таким образом восстанавливая и пересчитывая собственность. Я говорю о том, что на самом деле нет способа кэшировать результаты с помощью неявных преобразований.quantify
list2quantifiable
Quantifiable
quantify
Ответы:
Хотя я не хочу дублировать свой материал из 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, и даже это может быть «опасным», если вы не будете осторожны.
источник
Один из критериев, который может иметь значение, - это то, как вы хотите, чтобы новая функция «ощущалась»; используя неявные преобразования, вы можете сделать вид, будто это просто еще один метод:
"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
свойством.источник
default
для будущих читателей.Вы можете думать о различии между этими двумя методами по аналогии с приложением функции, просто с именованной оболочкой. Например:
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» .На самом деле я хочу сказать, что в принципе:
источник