Хороший пример неявного параметра в Scala?

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

Вопрос: Вы могли бы показать реальный (или близкий) хороший пример, когда действительно работают неявные параметры. IOW: что-то более серьезное, чем showPrompt, что оправдывало бы такой дизайн языка.

Или наоборот - можете ли вы продемонстрировать надежный дизайн языка (может быть мнимым), который не подразумевал бы необходимости. Я думаю, что даже никакой механизм лучше, чем implicits, потому что код более ясный, и нет никаких предположений.

Обратите внимание, что я задаю вопрос о параметрах, а не о неявных функциях (преобразованиях)!

Обновления

Глобальные переменные

Спасибо за отличные ответы. Возможно, я уточню свое возражение против "глобальных переменных". Рассмотрим такую ​​функцию:

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

вы называете это

max(5,6);

вы можете (!) сделать это вот так:

max(x:5,y:6);

но в моих глазах implicits работает вот так:

x = 5;
y = 6;
max()

он не очень отличается от такой конструкции (PHP-like)

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

Ответ Дерека

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

Ответ 1

В некотором смысле, да, implicits представляют глобальное состояние. Однако они не изменяются, что является настоящей проблемой с глобальными переменными - вы не видите, что люди жалуются на глобальные константы, не так ли? Фактически, стандарты кодирования обычно определяют, что вы преобразовываете любые константы в свой код в константы или перечисления, которые обычно являются глобальными.

Обратите внимание также, что implicits не находятся в плоском пространстве имен, что также является общей проблемой с глобальными. Они явно привязаны к типам и, следовательно, к иерархии пакетов этих типов.

Итак, возьмите свои глобальные переменные, сделайте их неизменными и инициализированными на сайте объявления и поместите их в пространства имен. Они все еще выглядят как глобальные? Они все еще выглядят проблематичными?

Но не останавливайся. Implicits привязаны к типам, и они так же "глобальны", как типы. Означает ли факт, что типы вас беспокоят глобально?

Что касается вариантов использования, их много, но мы можем сделать краткий обзор, основанный на их истории. Первоначально afaik, Scala не имел признаков. Что такое Scala, были типы просмотров, особенность многих других языков. Мы все еще можем видеть, что сегодня, когда вы пишете что-то вроде T <% Ordered[T], это означает, что тип T можно рассматривать как тип Ordered[T]. Типы просмотров - это способ автоматического выбора параметров в параметрах типа (generics).

Scala затем обобщил эту функцию с implicits. Автоматические отбрасывания больше не существуют, и вместо этого у вас есть неявные преобразования, которые являются просто значениями 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. Фактическое использование для них затем последовало, а синтаксический сахар для этих применений - последним.

Одним из них является Context Bounds, используемый для реализации шаблона типа type (шаблон, поскольку он не является встроенной функцией, а просто способом использования языка, который предоставляет аналогичную функциональность классу типа Haskell). Ограничение контекста используется для предоставления адаптера, который реализует функциональные возможности, присущие классу, но не объявленные им. Он предлагает преимущества наследования и интерфейсов без их недостатков. Например:

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!
                        ^

Одна библиотека, которая широко использует эту функцию, - Shapeless.

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

Если вам нравится, когда вам предписано (например, Python), тогда Scala просто не для вас.

Ответ 2

Конечно. Акка получил отличный пример в отношении своих Актеров. Когда вы находитесь внутри метода Actor 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)

Но обычно этого не происходит. Вы просто сохраняете естественное использование, которое стало возможным благодаря неявному определению значения в черте Actor. Есть около миллиона других примеров. Коллекционные классы огромны. Попробуйте бродить по любой нетривиальной библиотеке Scala, и вы найдете грузовик.

Ответ 3

В качестве примера можно привести операции сравнения на Traversable[A]. Например. max или sort:

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

Это может быть разумно определено только при операции < на A. Таким образом, без импликации wed должен каждый раз использовать контекст 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.

он может пахнуть гнилыми и злыми глобальными переменными. Однако решающим моментом является то, что в области может быть только одна неявная переменная для каждого типа. Ваш пример с двумя Int не будет работать.

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

Обычно вы не используете implicit для повседневных типов. И со специализированными типами (например, Ordering[Int]) не слишком много риска для их затенения.

Ответ 4

Другим хорошим общим использованием неявных параметров является то, что тип возвращаемого метода зависит от типа некоторых переданных ему параметров. Хорошим примером, упомянутым Йенсом, является структура коллекций и методы типа map, полная подпись которых:

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

Обратите внимание, что тип возврата That определяется наилучшим образом CanBuildFrom, который может найти компилятор.

В качестве другого примера см. ответ. Там тип возврата метода Arithmetic.apply определяется в соответствии с определенным типом неявного параметра (BiConverter).

Ответ 5

Это легко, просто помните:

  • объявить переменную, которая будет передана как неявная тоже
  • объявить все неявные параметры после неявных параметров в отдельном()

например.

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

Ответ 6

Основываясь на моем опыте, нет реального хорошего примера использования параметров implicits или преобразования implicits.

Небольшое преимущество использования implicits (не нужно явно писать параметр или тип) избыточно по сравнению с проблемами, которые они создают.

Я разработчик уже 15 лет и работаю с scala в течение последних 1,5 лет.

Я видел много ошибок, которые были вызваны тем, что разработчик не знал о том, что имплициты используются, и что определенная функция фактически возвращает другой тип, указанный указанным. Из-за неявного преобразования.

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

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

или

import org.some.common.library._

Оба кода будут скомпилированы и запущены. Но они не всегда будут получать одинаковые результаты, так как вторая версия импортирует implicits conversion, что приведет к поведению кода по-разному.

"Ошибка", вызванная этим, может произойти очень долго после написания кода, если некоторые значения, на которые повлияло это преобразование, первоначально не использовались.

Как только вы столкнулись с ошибкой, это нелегкая задача найти причину. Вы должны провести глубокое расследование.

Даже если вы почувствуете себя экспертом в scala после того, как нашли ошибку и исправили ее, изменив инструкцию импорта, вы фактически потратили много драгоценного времени.

Дополнительные причины, по которым я вообще против неявки:

  • Они затрудняют понимание кода (меньше кода, но вы не знаете, что он делает)
  • Время компиляции. scala код компилируется намного медленнее, когда используются implicits.
  • На практике он изменяет язык от статически типизированного, до динамически типизированного. Это правда, что, следуя очень строгим правилам кодирования, вы можете избежать таких ситуаций, но в реальном мире это не всегда так. Даже при использовании IDE "удалить неиспользуемые импорты" может привести к тому, что ваш код все еще будет компилироваться и запускаться, но не совпадает с ранее удаленным "неиспользуемым" импортом.

Нет возможности компилировать scala без implicits (если есть, пожалуйста, исправьте меня), и если бы была опция, ни одна из общих библиотек сообщества scala не скомпилировалась бы.

По всем вышеперечисленным причинам я думаю, что implicits - одна из худших практик, которые использует язык scala.

Scala имеет много замечательных функций, и многие из них не так велики.

При выборе языка для нового проекта, implicits является одной из причин против scala, а не в пользу этого. По-моему.

Ответ 7

Неявные параметры сильно используются в API коллекции. Многие функции получают неявный CanBuildFrom, который гарантирует, что вы получите "лучшую" реализацию набора результатов.

Без имплицитов вы либо проходили бы такую ​​вещь все время, что сделало бы обычное использование громоздким. Или используйте менее специализированные коллекции, которые будут раздражать, потому что это будет означать, что вы потеряете производительность/мощность.

Ответ 8

Я комментирую этот пост немного поздно, но я начал изучать 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)
}

}
}

Как мы можем видеть вышеприведенный код, у меня есть два неявных объекта в моем абстрактном классе, и я передал эти две неявные переменные в качестве неявных параметров функции/метода/определения. Я думаю, что это лучший вариант использования, который мы можем изобразить с точки зрения использования неявных переменных.