Неявный класс преобразования и типа

В 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) - общий случай.