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


пока неявные параметры в 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-like)

max() : Int
{
  global x : Int;
  global y : Int;
  ...
}
Дерек

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

8 70

8 ответов:

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

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

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

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

один из них -Контексте Границ, используется для реализации тип класса pattern (шаблон, потому что это не встроенная функция, а просто способ использования языка, который обеспечивает аналогичную функциональность для класса типа 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!
                        ^

одна библиотека, которая широко использует эта функция Бесформенный.

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

если вам нравится быть предписанным (например, Python делает), то Scala просто не для вас.

конечно. У акки есть отличный пример этого по отношению к своим актерам. Когда вы находитесь внутри актера receive метод, вы можете отправить сообщение другому актеру. Когда вы это сделаете, Akka будет связывать (по умолчанию) текущего актера как sender из сообщения, например:

trait ScalaActorRef { this: ActorRef =>
  ...

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

  ...
}

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

trait Actor {
  ...

  implicit val self = context.self

  ...
}

это создает неявное значение в рамках вашего собственного кода, и это позволяет делать простые вещи, как это:

someOtherActor ! SomeMessage

теперь вы можете сделать это, если хотите:

someOtherActor.!(SomeMessage)(self)

или

someOtherActor.!(SomeMessage)(null)

или

someOtherActor.!(SomeMessage)(anotherActorAltogether)

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

одним из примеров могут быть операции сравнения на Traversable[A]. Е. Г. max или sort:

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

они могут быть разумно определены только тогда, когда есть операция < on A. Так, без неявные преобразования, мы должны поставить контекст Ordering[B] каждый раз, когда мы хотели бы использовать эту функцию. (Или отказаться от типа статической проверки внутри max и риск ошибки во время выполнения броска.)

Если, однако, неявное сравнение класс в области видимости, например некоторые Ordering[Int], мы можем просто использовать его сразу или просто изменить метод сравнения, предоставляя какое-либо другое значение для неявного параметра.

конечно, имплициты могут быть затенены и, таким образом, могут быть ситуации, в которых фактический имплицит, который находится в области действия, недостаточно ясен. Для простого использования max или sort это действительно может быть достаточно, чтобы иметь фиксированный порядок trait on 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.

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

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

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

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

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

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

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

это просто, Просто помните:

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

например

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

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

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

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

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

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

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

или:

import org.some.common.library._

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

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

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

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

дополнительные причины того, почему я вообще против неявные преобразования являются:

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

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

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

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

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

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

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

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

Scala лучше всего подходит для написания кодов Apache Spark. В 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)
}

}
}