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

94

В 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
+1 отличный вопрос; Меня очень интересует подробный ответ на этот вопрос.
Дэн Бертон
@ Дэниел Спасибо. Я всегда ошибаюсь.
ziggystar
2
Вы ошибаетесь в одном месте: в вашем втором примере неявного преобразования вы храните sizeсписок в значении и сказать , что это позволяет избежать дорогостоящего обхода списка на последующих вызовы количественных, но на каждый вашем призыв к сработали сначала заново, таким образом восстанавливая и пересчитывая собственность. Я говорю о том, что на самом деле нет способа кэшировать результаты с помощью неявных преобразований. quantifylist2quantifiableQuantifiablequantify
Никита Волков
@NikitaVolkov Ваше замечание верно. И я обращаюсь к этому в своем вопросе в предпоследнем абзаце. Кеширование работает, когда преобразованный объект используется дольше после одного вызова метода преобразования (и, возможно, передается в преобразованном виде). В то время как классы типов, вероятно, будут связаны цепочкой вдоль непреобразованного объекта при более глубоком погружении.
ziggystar 07

Ответы:

42

Хотя я не хочу дублировать свой материал из Scala In Depth , я думаю, что стоит отметить, что классы типов / свойства типов намного более гибкие.

def foo[T: TypeClass](t: T) = ...

имеет возможность искать в своей локальной среде класс типа по умолчанию. Однако я могу в любой момент изменить поведение по умолчанию одним из двух способов:

  1. Создание / импорт экземпляра класса неявного типа в Scope для сокращения неявного поиска
  2. Непосредственная передача класса типа

Вот пример:

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, и даже это может быть «опасным», если вы не будете осторожны.

Jsuereth
источник
20

Один из критериев, который может иметь значение, - это то, как вы хотите, чтобы новая функция «ощущалась»; используя неявные преобразования, вы можете сделать вид, будто это просто еще один метод:

"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свойством.

Филипп
источник
@Phillippe - Меня очень интересует описанная вами техника ... но, похоже, она не работает в Scala 2.11.6. Я разместил вопрос с просьбой обновить ваш ответ. заранее спасибо, если вы можете помочь: см. stackoverflow.com/questions/31910923/…
Крис Бедфорд
@ChrisBedford Я добавил определение defaultдля будущих читателей.
Филипп
13

Вы можете думать о различии между этими двумя методами по аналогии с приложением функции, просто с именованной оболочкой. Например:

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 (или что-то вроде fundeps) являются общим случаем.
конфликт слияния
источник