Неявна конверсія проти класу типу


93

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

неявне перетворення

  • оскільки я створюю новий об'єкт, я можу кешувати там значення або обчислювати краще представлення; але чи слід уникати цього, оскільки це може відбуватися кілька разів, і явне перетворення, мабуть, буде викликане лише один раз?

Чого я очікую від відповіді

Наведіть один (або більше) випадків використання, коли різниця між обома поняттями має значення, та поясніть, чому я віддав би перевагу одному перед іншим. Також було б непогано пояснити суть двох понять та їх відношення один до одного, навіть без прикладу.


Існує певна плутанина в точках класу типу, де ви згадуєте "перегляд з переглядом", хоча класи класів використовують контекстні межі.
Daniel C. Sobral

1
+1 відмінне запитання; Мене дуже цікавить ґрунтовна відповідь на це.
Ден Бертон,

@Daniel Дякую. Я завжди помиляюся.
ziggystar

2
Ви помиляєтеся в одному місці: в вашому другому прикладі неявного перетворення ви зберігаєте sizeсписок в значенні і сказати , що це дозволяє уникнути дорогого обходу списку на наступних виклики кількісних, але на кожен вашому заклик до спрацювали спочатку, таким чином, поновлюючи та перераховуючи майно. Я кажу, що насправді немає можливості кешувати результати за допомогою неявних перетворень. quantifylist2quantifiableQuantifiablequantify
Микита Волков

@NikitaVolkov Ваше спостереження правильне. І я звертаюся до цього у своєму питанні в останньому абзаці. Кешування працює, коли перетворений об'єкт використовується довше після одного виклику методу перетворення (і, можливо, передається у перетвореному вигляді). У той час як класи типу, ймовірно, будуть прив’язані до неперетвореного об’єкта при заглибленні.
ziggystar

Відповіді:


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, і навіть це може бути "небезпечним", якщо ви не будете обережні.


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

Тож справді моя суть полягає в тому, що в принципі:

  • "сутенерство" класу (шляхом неявного перетворення) - це випадок "нульового порядку" ...
  • оголошення типового класу - це випадок "першого порядку" ...
  • загальноприйнятими є багатопараметричні типи класів із фундапсом (або чимось на зразок фундепсів).
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.