Неявное преобразование против класса типа - PullRequest
91 голосов
/ 15 декабря 2011

В 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]

неявное преобразование

  • так как я создаю новый объект, я могу кэшировать значения там или вычислять лучшее представление; но стоит ли этого избегать, поскольку это может произойти несколько раз, и явное преобразование может быть вызвано только один раз?

Что я ожидаю от ответа

Представьте один (или несколько) вариантов использования, в которых имеет значение различие между этими двумя понятиями, и объясните, почему я предпочел бы одно другому. Также было бы неплохо объяснить суть этих двух понятий и их связь друг с другом, даже без примера.

Ответы [ 3 ]

39 голосов
/ 16 декабря 2011

Хотя я не хочу дублировать свой материал из 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
}

Это делает классы типов бесконечно более гибкими.Другое дело, что классы / признаки типов лучше поддерживают неявный lookup .

В первом примере, если вы используете неявное представление, компилятор выполнит неявный поиск для:

Function1[Int, ?]

, который будет смотреть на сопутствующий объект Function1 и сопутствующий объект Int.

Обратите внимание, что Quantifiable - это нигде в неявном поиске.Это означает, что вы должны поместить неявное представление в объект пакета или и импортировать его в область видимости.Это большая работа, чтобы вспомнить, что происходит.

С другой стороны, класс типов явный .Вы видите, что он ищет в сигнатуре метода.У вас также есть неявный поиск

Quantifiable[Int]

, который будет искать в сопутствующем объекте Quantifiable и Int сопутствующий объект.Это означает, что вы можете задать значения по умолчанию и ; новые типы (например, класс MyString) могут предоставить значение по умолчанию в их сопутствующем объекте, и будет выполняться неявный поиск.

В общем, я использую типклассы.Они бесконечно более гибки для начального примера.Единственное место, где я использую неявные преобразования, - это использование уровня API между оболочкой Scala и библиотекой Java, и даже это может быть «опасно», если вы не будете осторожны.

20 голосов
/ 16 декабря 2011

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

"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.

13 голосов
/ 16 декабря 2011

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

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