Хороший пример неявного параметра в 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 ответов
в смысле, да, неявные преобразования представляют глобальное состояние. Однако они не изменчивы, что является истинной проблемой с глобальными переменными-вы не видите людей, жалующихся на глобальные константы, не так ли? Фактически, стандарты кодирования обычно диктуют преобразование любых констант в коде в константы или перечисления, которые обычно являются глобальными.
Отметим также, что неявные преобразования-это:не в плоском пространстве имен, что также является общей проблемой с глобалами. Они явно привязан к типам и, следовательно, к иерархии пакетов этих типов.
Итак, возьмите глобалы, сделайте их неизменяемыми и инициализированными на сайте объявления и поместите их в пространства имен. Они все еще выглядят как глобалы? Они все еще выглядят проблематично?
но давайте не будем останавливаться на этом. Неявные преобразования are привязаны к типам, и они так же "глобальны", как и типы. Беспокоит ли вас тот факт, что типы глобальны?
как для использования случаи, их много, но мы можем сделать краткий обзор, основанный на их истории. Изначально, насколько мне известно, Скала не имеют неявные преобразования. У Scala были типы представлений, которые были у многих других языков. Мы все еще можем видеть, что сегодня, когда вы пишете что-то вроде T <% Ordered[T]
, что означает, что тип T
можно рассматривать как тип Ordered[T]
. Типы представлений-это способ сделать автоматические слепки доступными для параметров типов (дженериков).
Scala тогда обобщенное эта функция с неявный. Автоматические слепки больше не существуют, и вместо этого у вас есть неявные преобразования -- которые просто Function1
значения и, следовательно, могут быть переданы в качестве параметров. С тех пор, T <% Ordered[T]
означало, что значение для неявного преобразования будет передано в качестве параметра. Поскольку приведение является автоматическим, вызывающий функцию не обязан явно передавать параметр - поэтому эти параметры стали неявных параметров.
обратите внимание, что есть два понятия-неявные преобразования и неявные параметры-которые очень близки, но не полностью перекрываются.
во всяком случае, типы представлений стали синтаксическим сахаром для неявных преобразований, передаваемых неявно. Они будут переписаны следующим образом:--21-->
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
. Фактическое использование для них затем, и синтаксический сахар для те использование пришло последним.
один из них -Контексте Границ, используется для реализации тип класса шаблон (шаблон, потому что это не встроенная функция, просто способ использования языка, который обеспечивает аналогичную функциональность класса типа 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 просто не для вас.
конечно. У Akka есть отличный пример этого в отношении своих актеров. Когда ты внутри актерского 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[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.
он может пахнуть гнилыми и злыми глобальными переменными. Решающим моментом, однако, является то, что может быть только одна неявная переменная в тип в область. Ваш пример с двумя Int
s не будет работать.
кроме того, это означает, что практически, неявные переменные используются только тогда, когда есть не обязательно уникальный, но внятный экземпляра типа. The self
ссылка на актера является хорошим примером для такой вещи. Другим примером является класс type. Могут быть десятки алгебраических сравнений для любого типа, но есть один, который является особенным.
(На другом уровне, фактическое номер строки в самом коде также может быть хорошей неявной переменной, если она использует очень отличительный тип.)
вы обычно не используете implicit
s для повседневных типов. И со специализированными типами (например,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.
небольшое преимущество, используя неявные преобразования (не нужно явно написать параметр или тип) является избыточным по сравнению с проблемами, которые они создают.
Я разработчик в течение 15 лет и работаю с scala в течение последних 1,5 лет.
Я видел много раз ошибки, которые были вызваны разработчиком, не зная о том, что использовать неявные преобразования, и что конкретная функция на самом деле возвращает другой тип, тот, что указан. Из-за неявного преобразования.
Я тоже слышал заявления о том, что если вам не нравится неявные преобразования, не используйте их. Это не практично в реальном мире, т. к. используется многократно внешних библиотек, и многие из них, используя неявные преобразования, так что ваш код, используя неявные преобразования, и вы, возможно, не осознавая этого. Вы можете написать код, который имеет либо:
import org.some.common.library.{TypeA, TypeB}
или:
import org.some.common.library._
оба кода будут компилироваться и запускаться. Но они не всегда будут давать одни и те же результаты, поскольку вторая версия импорта подразумевает преобразование, которое заставит код вести себя по-разному.
"ошибка", вызванная этим, может произойти очень долго после написания кода, если некоторые значения, на которые влияет это преобразование, не были использованы изначально.
после того, как вы столкнулись с ошибкой, это не просто задача найти причину. Вы должны провести глубокое расследование.
даже если вы чувствуете себя экспертом в scala, как только вы нашли ошибку и исправили ее, изменив оператор импорта, вы на самом деле потратили много драгоценного времени.
дополнительные причины того, почему я вообще против неявные преобразования являются:
- они затрудняют понимание кода (кода меньше, но вы не знаете, что он делает)
- время компиляции. код scala компилирует гораздо медленнее при использовании неявные преобразования несколько.
- на практике он меняет язык со статически типизированного на динамически типизированный. Это правда, что после очень строгих правил кодирования вы можете избежать таких ситуаций, но в реальном мире это не всегда так. Даже использование IDE "удалить неиспользуемые импорт" может привести к компиляции и запуску кода, но не так, как до удаления "неиспользуемых" импорт.
нет возможности скомпилировать scala без implicits (если есть, пожалуйста, исправьте меня), и если бы была опция, ни одна из общих библиотек 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)
}
}
}
Как мы можем видеть код выше, у меня есть два неявных объекта в моем абстрактном классе, и я передал эти две неявные переменные как неявные параметры функции/метода/определения. Я думаю, что это лучший вариант использования, который можно изобразить с точки зрения использования неявных переменных.