Хороший приклад неявного параметра в Scala? [зачинено]


75

Поки що неявні параметри в Scala не виглядають для мене добре - це занадто близько до глобальних змінних, однак оскільки Scala здається досить суворою мовою, я починаю сумніватися на власну думку :-).

Питання: чи не могли б ви показати хороший приклад із реального життя (або близького), коли неявні параметри дійсно працюють. IOW: щось більш серйозне, ніж те showPrompt, що виправдовує такий мовний дизайн.

Або навпаки - чи могли б ви показати надійний мовний дизайн (можна уявити), який зробив би неявним не необхідним. Я думаю, що навіть жоден механізм не кращий за імпліцити, оскільки код чіткіший і неможливо здогадатися.

Зверніть увагу, я запитую про параметри, а не неявні функції (перетворення)!

Оновлення

Глобальні змінні

Дякую за всі чудові відповіді. Можливо, я уточнюю своє заперечення щодо "глобальних змінних". Розглянемо таку функцію:

max(x : Int,y : Int) : Int

ти називаєш це

max(5,6);

Ви могли б (!) зробити це так:

max(x:5,y:6);

але в моїх очах implicitsпрацює так:

x = 5;
y = 6;
max()

він не сильно відрізняється від такої конструкції (PHP-подібний)

max() : Int
{
  global x : Int;
  global y : Int;
  ...
}

Відповідь Дерека

Це чудовий приклад, однак, якщо ви можете сприймати як гнучке використання надсилання повідомлень без використання, implicitопублікуйте зустрічний приклад. Мені справді цікаво щодо чистоти мовного дизайну ;-).


Якщо ви робите глобальний неявний (а ви не можете - найкраще, що ви можете зробити, це неявний пакет з масштабом дії), тоді ваше твердження може виконуватися, але лише якщо ви вирішили зробити таке ... ні. Суть в тому, що гнучкість цього API походить від використання імпліцитів. Якщо ви не використовуєте їх, ви не можете отримати таку ж гнучкість. Отже, ви просите видалити функцію, яка робить її чудовою, і все ж зробити її чудовою. Дуже дивне прохання.
Дерек Вайатт,

@Derek Wyatt, останній коментар є дещо дивним - ви не шукаєте оптимізації в житті? Я згоден. Тепер про глобальні змінні - я не кажу, що у вас повинні бути глобальні змінні, щоб використовувати імпліцити, я кажу, що вони подібні у використанні. Оскільки вони прив'язані до імені абонента, неявно, і вони виведені за межі зони абонента, а не з фактичного виклику.
greenoldman 02.03.12

Відповіді:


99

У певному сенсі так, імпліцити представляють глобальну державу. Однак вони не змінюються, що є справжньою проблемою глобальних змінних - ви не бачите, як люди скаржаться на глобальні константи, правда? Насправді стандарти кодування зазвичай диктують, що ви перетворюєте будь-які константи у своєму коді на константи або переліки, які зазвичай є загальносвітовими.

Також зауважте, що наслідками цього не є в плоскому просторі імен, що також є загальною проблемою для глобальних систем. Вони явно прив'язані до типів і, отже, до ієрархії пакетів цих типів.

Отже, візьміть свої глобальні картки, зробіть їх незмінними та ініціалізованими на сайті декларування та розмістіть у просторах імен. Вони все ще схожі на глобальних? Вони все ще виглядають проблематично?

Але не будемо зупинятися на досягнутому. Імпліцити є прив'язані до типів, і вони так само «глобальний» , як типи. Вас турбує той факт, що типи є загальносвітовими?

Що стосується випадків використання, то їх багато, але ми можемо зробити короткий огляд на основі їх історії. Спочатку, афаїк, Скала не мав наслідків. Scala мала типи переглядів, властивість багатьох інших мов. Ми все ще можемо бачити, що сьогодні, коли ви пишете щось на зразок T <% Ordered[T], що означає, що тип Tможна розглядати як тип Ordered[T]. Типи подання - це спосіб зробити доступними автоматичні передачі за параметрами типу (загальні засоби).

Потім Скала узагальнив цю ознаку з імпліцитами. Автоматичні передачі більше не існують, і, натомість, у вас є неявні перетворення - це лише Function1значення і, отже, можуть передаватися як параметри. Відтоді T <% Ordered[T]значення параметра неявного перетворення буде передаватися як параметр. Оскільки приведення здійснюється автоматично, викликаючий функцію не потрібно явно передавати параметр - тому ці параметри стали неявними параметрами .

Зверніть увагу, що є два поняття - неявні перетворення та неявні параметри - які дуже близькі, але не повністю перекриваються.

У будь-якому випадку, типи подання стали синтаксичним цукром для неявних перетворень, що передаються неявно. Їх переписали б так:

def max[T <% Ordered[T]](a: T, b: T): T = if (a < b) b else a
def max[T](a: T, b: T)(implicit $ev1: Function1[T, Ordered[T]]): T = if ($ev1(a) < b) b else a

Неявні параметри - це просто узагальнення цього шаблону, що дозволяє передавати будь- які неявні параметри, а не просто Function1. Потім послідувало фактичне використання для них, а синтаксичний цукор для цих цілей прийшов останнім.

Одним з них є контекстні межі , що використовуються для реалізації шаблону класу типу (шаблон, оскільки він не є вбудованою функцією, а лише способом використання мови, яка забезпечує подібну функціональність класу типу Хаскелла). Прив'язка контексту використовується для надання адаптера, який реалізує функціональність, яка властива класу, але не оголошена ним. Він пропонує переваги успадкування та інтерфейсів без їх недоліків. Наприклад:

def max[T](a: T, b: T)(implicit $ev1: Ordering[T]): T = if ($ev1.lt(a, b)) b else a
// latter followed by the syntactic sugar
def max[T: Ordering](a: T, b: T): T = if (implicitly[Ordering[T]].lt(a, b)) b else a

Ви, напевно, вже цим користувались - є одна поширена справа, яку люди зазвичай не помічають. Саме це:

new Array[Int](size)

Для використання ініціалізації такого масиву використовується контекст, пов’язаний з маніфестами класу. Це можна побачити на цьому прикладі:

def f[T](size: Int) = new Array[T](size) // won't compile!

Ви можете написати це так:

def f[T: ClassManifest](size: Int) = new Array[T](size)

У стандартній бібліотеці найбільш використовуваними межами контексту є:

Manifest      // Provides reflection on a type
ClassManifest // Provides reflection on a type after erasure
Ordering      // Total ordering of elements
Numeric       // Basic arithmetic of elements
CanBuildFrom  // Collection creation

Останні три в основному використовуються з колекціями з такими методами, як max, sumіmap . Бібліотекою, яка широко використовує контекстні межі, є Scalaz.

Іншим поширеним використанням є зменшення котлової плити при операціях, які повинні мати спільний параметр. Наприклад, транзакції:

def withTransaction(f: Transaction => Unit) = {
  val txn = new Transaction

  try { f(txn); txn.commit() }
  catch { case ex => txn.rollback(); throw ex }
}

withTransaction { txn =>
  op1(data)(txn)
  op2(data)(txn)
  op3(data)(txn)
}

Що потім спрощується таким чином:

withTransaction { implicit txn =>
  op1(data)
  op2(data)
  op3(data)
}

Цей шаблон використовується з транзакційною пам'яттю, і я думаю (але я не впевнений), що бібліотека вводу-виводу Scala також використовує його.

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

def flatten[B](implicit ev: A <:< Option[B]): Option[B]

Це робить це можливим:

scala> Option(Option(2)).flatten // compiles
res0: Option[Int] = Some(2)

scala> Option(2).flatten // does not compile!
<console>:8: error: Cannot prove that Int <:< Option[B].
              Option(2).flatten // does not compile!
                        ^

Бібліотека, яка широко використовує цю функцію, - це „Без форми”.

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

Якщо вам подобається прописувати (як, скажімо, Python), то Scala просто не для вас.


7
Книга, яку ви пишете, має бути остаточно англійською мовою! :-) Дякую за чудовий пост.
greenoldman 03.03.12

2
Чому ТАК не дає варіант зірки для такої відповіді? Дійсно чудовий пост!
Chen OT

23

Звичайно. Акка має чудовий приклад цього стосовно своїх акторів. Коли ви знаходитесь у receiveметоді актора , вам може знадобитися надіслати повідомлення іншому акторові. Коли ви це зробите, Akka буде згрупувати (за замовчуванням) поточного Актора як senderповідомлення, приблизно так:

trait ScalaActorRef { this: ActorRef =>
  ...

  def !(message: Any)(implicit sender: ActorRef = null): Unit

  ...
}

senderМається на увазі. В акторі є визначення, яке виглядає так:

trait Actor {
  ...

  implicit val self = context.self

  ...
}

Це створює неявне значення в межах вашого власного коду, і це дозволяє вам робити такі прості речі:

someOtherActor ! SomeMessage

Тепер ви можете зробити і це, якщо хочете:

someOtherActor.!(SomeMessage)(self)

або

someOtherActor.!(SomeMessage)(null)

або

someOtherActor.!(SomeMessage)(anotherActorAltogether)

Але зазвичай ви цього не робите. Ви просто зберігаєте природне використання, яке стало можливим завдяки неявному визначенню значення в ознаці Актор. Є ще близько мільйона інших прикладів. Класи колекціонування величезні. Спробуйте побродити по будь-якій нетривіальній бібліотеці Scala, і ви знайдете вантажівку.


Я думаю, що це навіть кращий приклад, ніж Traversable.maxкласи типу тощо.
Debilski 02.03.12

Це хороший приклад. Я до певної міри вважаю, що неявні змінні - це спосіб ПІДКЛЮЧИТИ глобальні змінні та "божественні одиночки" (за відсутності кращого слова), але все одно тримають ваш код більш читабельним, оскільки вам не потрібно явно проходити деякі основні сантехнічні роботи (вищезазначене) одиночні). І знову ж таки, ви все одно можете передавати їх явно, наприклад, під час тестування. Тому я думаю, що у багатьох випадках вони допускають більш вільне зчеплення та чистіший код.
vertti 02.03.12

@vertti, не зовсім так. Я думаю, що спосіб роботи на C ++ тут ​​кращий - тобто параметри для цілого класу та / або аргументи за замовчуванням. Для мене думка про те, що функція відсмоктує аргумент, взятий звідкись сам по собі, дуже дивна.
greenoldman 02.03.12

1
@Derek Wyatt, ти сприймаєш це занадто особисто. "тут працює краще" - сподіваюсь, я чітко зрозумів, які мої показники, вони не такі, як ваші, тому моє "краще" не те саме, що ваше "краще". Ви раді, що маєте implicits, я не маю - ваш приклад чудовий, як головоломка (і я вдячний за це), як вирішити проблему з іншої точки зору. Це мій POV, тому, будь ласка, позбавте мене порад "вивчіть мову, на якій кодуєте" (правда, але не дуже ввічливо - протегування не вітається в будь-якому обговоренні).
greenoldman 02.03.12

1
@macias О, лайно! Мені дуже шкода, але мій коментар там, наверху, повинен був читати ... Я НЕ покровительствую вам ... Я насправді ні. Тьфу ... пробачте за це.
Дерек Вайатт,

9

Одним з прикладів може бути операція порівняння Traversable[A]. Наприклад, maxабо sort:

def max[B >: A](implicit cmp: Ordering[B]) : A

Це тільки може бути розумно визначено , якщо є операція <на A. Отже, без імпліцитів нам доведеться подавати контекст Ordering[B]кожного разу, коли ми хочемо використовувати цю функцію. (Або відмовтеся від статичної перевірки типу всерединіmax і ризикуйте помилкою виконання).

Якщо, однак, передбачається неявний клас порівняльного типу , наприклад деякіOrdering[Int] , ми можемо просто використати його відразу або просто змінити метод порівняння, надавши якесь інше значення для неявного параметра.

Звичайно, імпліцити можуть бути затьмарені, і, отже, можуть бути ситуації, коли фактичний імпліцит, який є в обсязі, недостатньо чіткий. Для простих застосувань maxабо sortце дійсно може бути досить , щоб мати фіксований порядок traitна Intі використовувати деякий синтаксис , щоб перевірити наявність певної ця ознака. Але це означало б, що ніяких додаткових рис не може бути, і кожен фрагмент коду повинен використовувати ті риси, які були визначені спочатку.

Додаток:
відповідь на порівняння глобальної змінної .

Думаю, ви праві, що в коді, який вирізано як

implicit val num = 2
implicit val item = "Orange"
def shopping(implicit num: Int, item: String) = {
  "I’m buying "+num+" "+item+(if(num==1) "." else "s.")
}

scala> shopping
res: java.lang.String = I’m buying 2 Oranges.

може пахнути гнилими і злими глобальними змінними. Важливим моментом, однак, є те , що може бути тільки одна змінна неявне кожного типу в області. Ваш приклад із двома Ints не спрацює.

Крім того, це означає, що практично неявні змінні використовуються лише тоді, коли для типу існує не обов'язково унікальний, але чіткий первинний екземпляр. selfПосилання актора є хорошим прикладом для такої речі. Прикладом класу типу є ще один приклад. Для будь-якого типу може бути десятки алгебраїчних порівнянь, але є один, який є особливим. (На іншому рівні фактичний номер рядка в самому коді також може створити хорошу неявну змінну, якщо він використовує дуже відмінний тип.)

Зазвичай ви не використовуєте implicits для повсякденних типів. А зі спеціалізованими типами (як Ordering[Int]) не надто багато ризику їх затінити.


Дякую, проте насправді це контрприклад - це має бути "рисою" екземпляра колекції. І тоді ви можете використовувати max (), який використовуватиме впорядкування колекції, або max (порівнювач), який використовуватиме спеціальний.
greenoldman 02.03.12

2
Звичайно, це було б можливо. Але це також означало б, що не можна додавати іншу рису, наприклад, Intабо будь-який інший заздалегідь визначений тип, коли це потрібно. (Часто цитованим прикладом є напівгрупа, яка може не бути оригінальною ознакою на Int, ані на String - і також не можна було б додати цю ознаку у фіксованій формі.) Проблема в тому, що: немає можливості узагальнити тип за всіма можливими ознаками. Це завжди код (анотації типу), який потрібно надавати спеціально, або ви втрачаєте безпеку типу. Неявні змінні просто зменшують шаблонний код для цього.
Debilski 02.03.12

Не Int'sдається колекція Int's, наприклад List або Array. Якщо ви вважаєте, що елементи порівнянні, і ви пишете такі, implicitяк вище, ви могли б також, визначити порядок у верхній частині класу (як у C ++). У C ++ простір імен тут не забруднений довільними іменами типу "cmp", оскільки ви передаєте значення.
greenoldman 02.03.12

Дякую за додавання, я не можу більше проголосувати за вас, вибачте :-)
greenoldman 02.03.12

Якщо ви використовуєте термінологію Haskell, ви можете використовувати її правильно. Значення type Ordering[Int]- це екземпляр класу типу , а не клас класу .
Ротсор,

6

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

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

Я розробник протягом 15 років і працюю із Scala протягом останніх 1,5 років.

Я багато разів бачив помилки, які були спричинені розробником, не знаючи про те, що використовуються імпліцити, і що конкретна функція фактично повертає інший тип, ніж той, що вказаний. Через неявне перетворення.

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

import org.some.common.library.{TypeA, TypeB}

або:

import org.some.common.library._

Обидва коди будуть скомпільовані та запущені. Але вони не завжди дадуть однакові результати, оскільки друга версія імпортує імпліцитні конверсії, які змусять код поводитися по-різному.

Помилка, яка викликана цим, може статися дуже довго після написання коду, якщо деякі значення, на які впливає це перетворення, не використовувалися спочатку.

Як тільки ви зіткнетеся з помилкою, нелегке завдання знайти її причину. Вам доведеться провести глибоке розслідування.

Незважаючи на те, що ви почуваєтесь експертом у Scala, як тільки знайшли помилку та виправили її, змінивши імпортну заяву, ви насправді витратили багато дорогоцінного часу.

Додаткові причини, чому я взагалі проти імпліцитів:

  • Вони ускладнюють розуміння коду (коду менше, але ви не знаєте, що він робить)
  • Час складання. Scala-код компілюється набагато повільніше, коли використовуються імпліцити.
  • На практиці він змінює мову зі статично введеної на динамічно набрану. Це правда, що дотримуючись дуже суворих правил кодування, ви можете уникнути подібних ситуацій, але в реальному світі це не завжди так. Навіть використання IDE "видалення невикористаного імпорту" може спричинити компіляцію та запуск вашого коду, але не те саме, що до того, як ви видалили "невикористаний" імпорт.

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

З усіх перерахованих вище причин, я думаю, що імпліцити є однією з найгірших практик, які використовує мова Scala.

Scala має багато чудових можливостей, і багато не дуже чудових.

При виборі мови для нового проекту імпліцити є однією з причин проти Scala, а не на користь неї. На мою думку.


Варто зазначити, що Котлін позбувся імпліцитів: kotlinlang.org/docs/reference/comparison-to-scala.html
Акавалл,

4

Ще одним хорошим загальним використанням неявних параметрів є створення типу повернення методу в залежності від типу деяких переданих йому параметрів. Хорошим прикладом, згаданим Єнсом, є фреймворк колекцій та такі методи map, чиїм повним підписом зазвичай є:

def map[B, That](f: (A) ⇒ B)(implicit bf: CanBuildFrom[GenSeq[A], B, That]): That

Зверніть увагу, що тип повернення Thatвизначається найкращим чиномCanBuildFrom який може знайти компілятор.

Інший приклад цього див. У цій відповіді . Там тип повернення методу Arithmetic.applyвизначається відповідно до певного неявного типу параметра ( BiConverter).


Можливо, я щось пропускаю. Тут ви не можете здогадатися про тип That, тому вам доведеться це вказати, так? Чи не так би було, якщо пропустити тип That і просто перетворити результат вручну: map (it => it.foo) .toBar () замість map [B, List [Bars]] (it => it.foo)?
greenoldman 02.03.12

@macias: Останній не створює проміжну колекцію. Коли ви явно викликаєте toBar, спочатку повинен бути створений Foo, який потім перетворюється на Bar. Коли є параметр типу, Bar може створювати безпосередньо.
kiritsuku 02.03.12

3
@macias: Якщо ви перетворюєте його вручну, ви робите це на другому кроці, потім. Ви можете отримати Listвзамін, а потім доведеться проїхати його знову, щоб просто отриматиSet . Використовуючи неявну «анотацію», mapметод дозволяє уникнути ініціалізації та заповнення неправильної колекції в першу чергу.
Дебільські 02.03.12

1
@macias: вам не потрібно вказувати параметри типу в методі map - їх можна вивести. val lf: List [Foo] =…; val sb: Set [Bar] = lf map (_.toBar) // немає проміжного списку [Bar]
romusz

4

Це легко, просто пам’ятайте:

  • оголосити змінну, що передається, також неявно
  • оголосити всі неявні параметри після неявних параметрів в окремому ()

напр

def myFunction(): Int = {
  implicit val y: Int = 33
  implicit val z: Double = 3.3

  functionWithImplicit("foo") // calls functionWithImplicit("foo")(y, z)
}

def functionWithImplicit(foo: String)(implicit x: Int, d: Double) = // blar blar

3

Неявні параметри активно використовуються в API колекції. Багато функцій отримують неявний CanBuildFrom, який гарантує, що ви отримаєте найкращу реалізацію збору результатів.

Без наслідків ви б або постійно передавали таке, що зробило б звичне використання звичайним. Або використовуйте менш спеціалізовані колекції, що буде дратувати, оскільки це означатиме втрату продуктивності / потужності.


0

Я коментую цей пост трохи пізно, але останнім часом я почав вивчати Scala. Даніель та інші розповіли приємно про неявне ключове слово. Я дав би мені два центи неявної змінної з точки зору практичного використання.

Scala найкраще підходить, якщо використовується для написання кодів Apache Spark. У Spark ми маємо контекст іскри і, швидше за все, клас конфігурації, який може отримати ключі / значення конфігурації з конфігураційного файлу.

Тепер, якщо у мене є абстрактний клас і якщо я оголошую об'єкт конфігурації та контекст іскри таким чином: -

abstract class myImplicitClass {

implicit val config = new myConfigClass()

val conf = new SparkConf().setMaster().setAppName()
implicit val sc = new SparkContext(conf)

def overrideThisMethod(implicit sc: SparkContext, config: Config) : Unit
}

class MyClass extends myImplicitClass {

override def overrideThisMethod(implicit sc: SparkContext, config: Config){

/*I can provide here n number of methods where I can pass the sc and config 
objects, what are implicit*/
def firstFn(firstParam: Int) (implicit sc: SparkContext, config: Config){ 
    /*I can use "sc" and "config" as I wish: making rdd or getting data from cassandra, for e.g.*/
    val myRdd = sc.parallelize(List("abc","123"))
}
def secondFn(firstParam: Int) (implicit sc: SparkContext, config: Config){
 /*following are the ways we can use "sc" and "config" */

        val keyspace = config.getString("keyspace")
        val tableName = config.getString("table")
        val hostName = config.getString("host")
        val userName = config.getString("username")
        val pswd = config.getString("password")

    implicit val cassandraConnectorObj = CassandraConnector(....)
    val cassandraRdd = sc.cassandraTable(keyspace, tableName)
}

}
}

Як ми можемо бачити код вище, у мене в абстрактному класі є два неявних об'єкти, і я передав ці дві неявні змінні як неявні параметри функції / методу / визначення. Я думаю, що це найкращий варіант використання, який ми можемо зобразити з точки зору використання неявних змінних.

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