Scala: зачем использовать неявный аргумент функции?

У меня есть следующая функция:

def getIntValue(x: Int)(implicit y: Int ) : Int = {x + y}

Я вижу выше декларации везде. Я понимаю, что делает вышеуказанная функция. Это функция карри, которая принимает два аргумента. Если вы опустите второй аргумент, он вызовет неявное определение, которое вместо этого возвращает int. Поэтому я думаю, что это очень похоже на определение значения по умолчанию для аргумента.

implicit val temp = 3

scala> getIntValue(3)
res8: Int = 6

Мне было интересно, каковы преимущества вышеуказанной декларации?

3 ответа

Решение

Вот мой "прагматичный" ответ: вы обычно используете карри как более "условное обозначение", чем что-либо еще значимое. Это очень удобно, когда ваш последний параметр является параметром "по имени" (например: : => Boolean):

def transaction(conn: Connection)(codeToExecuteInTransaction : => Boolean) = {

   conn.startTransaction  // start transaction

   val booleanResult = codeToExecuteInTransaction //invoke the code block they passed in

  //deal with errors and rollback if necessary, or commit
  //return connection to connection pool
}

Что это говорит: "У меня есть функция под названием transaction, его первый параметр - это Connection, а его второй параметр будет блоком кода ".

Это позволяет нам использовать этот метод следующим образом (используя "Я могу использовать фигурную скобку вместо правила скобок"):

transaction(myConn) {

   //code to execute in a transaction
  //the code block's last executable statement must be a Boolean as per the second
  //parameter of the transaction method

}

Если бы вы не использовали этот метод транзакции, это выглядело бы неестественно:

transaction(myConn, {

   //code block

})

Как насчет implicit? Да, это может показаться очень неоднозначной конструкцией, но вы привыкнете к ней через некоторое время, и хорошая вещь в неявных функциях заключается в том, что у них есть правила видимости. Таким образом, это означает, что для производства вы можете определить неявную функцию для получения этого соединения с базой данных из базы данных PROD, но в своем интеграционном тесте вы определите неявную функцию, которая заменит версию PROD, и она будет использоваться для получения соединения. вместо базы данных DEV для использования в вашем тесте.

Как пример, как насчет того, чтобы добавить неявный параметр в метод транзакции?

def transaction(implicit conn: Connection)(codeToExecuteInTransaction : => Boolean) = {

}

Теперь, предполагая, что где-то в моей базе кода есть неявная функция, которая возвращает соединение, вот так:

def implicit getConnectionFromPool() : Connection = { ...}

Я могу выполнить метод транзакции так:

transaction {
   //code to execute in transaction
}

и Scala переведет это на:

transaction(getConnectionFromPool) {
  //code to execute in transaction
}

Подводя итог, можно сказать, что Implicits - это очень хороший способ не заставлять разработчика предоставлять значение для обязательного параметра, когда этот параметр в 99% случаев будет одинаковым везде, где вы используете функцию. В тот 1% времени, когда вам нужно другое соединение, вы можете предоставить свое собственное соединение, передав значение вместо того, чтобы позволить Scala выяснить, какая неявная функция предоставляет значение.

В вашем конкретном примере нет никаких практических преимуществ. Фактически использование последствий для этой задачи только запутывает ваш код.

Стандартный вариант использования имплицитов - шаблон класса типа. Я бы сказал, что это единственный вариант использования, который практически полезен. Во всех остальных случаях лучше, чтобы все было ясно.

Вот пример класса типов:

// A typeclass
trait Show[a] {
  def show(a: a): String
}

// Some data type
case class Artist(name: String)

// An instance of the `Show` typeclass for that data type
implicit val artistShowInstance =
  new Show[Artist] {
    def show(a: Artist) = a.name
  }

// A function that works for any type `a`, which has an instance of a class `Show`
def showAListOfShowables[a](list: List[a])(implicit showInstance: Show[a]): String =
  list.view.map(showInstance.show).mkString(", ")

// The following code outputs `Beatles, Michael Jackson, Rolling Stones`
val list = List(Artist("Beatles"), Artist("Michael Jackson"), Artist("Rolling Stones"))
println(showAListOfShowables(list))

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

Там много деталей, не упомянутых, как синтаксический сахар, def случаи и т. д. Это огромная тема, и, к счастью, она широко освещается в Интернете. Просто гуглите "класс типа скала".

Есть много преимуществ, кроме вашего примера. Я дам только один; в то же время, это также трюк, который вы можете использовать в определенных случаях.

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

trait MyContainer[A] {
  def containedValue:A
}

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

Но поскольку вы хотите, чтобы ваш класс был полезен для всех типов, вы не хотите ограничивать A быть из Seq тип или Traversableили что-нибудь в этом роде. По сути, вы хотите метод, который говорит: "Я могу быть вызван только если A имеет Seq типа."И если кто-то вызывает его, скажем, MyContainer[Int], это должно привести к ошибке компиляции.

Это возможно Что вам нужно, это некоторые доказательства того, что A имеет тип последовательности. И вы можете сделать это с помощью Scala и неявных аргументов:

trait MyContainer[A] {
  def containedValue:A
  def aggregate[B](f:B=>B)(implicit ev:A=>Seq[B]):B =
    ev(containedValue) reduce f
}

Итак, если вы вызываете этот метод на MyContainer[Seq[Int]]компилятор будет искать неявное Seq[Int]=>Seq[B], Это действительно просто решить для компилятора. Потому что есть глобальная неявная функция, которая называется identityи это всегда в рамках. Его тип подписи что-то вроде: A=>A

Он просто возвращает любой аргумент, переданный ему.

Я не знаю, как называется эта модель. (Кто-нибудь может помочь?) Но я думаю, что это ловкий прием, который иногда бывает полезен. Вы можете увидеть хороший пример этого в библиотеке Scala, если вы посмотрите на сигнатуру метода Seq.sum, В случае sumиспользуется другой неявный тип параметра; в этом случае неявный параметр является свидетельством того, что содержащийся тип является числовым, и, следовательно, сумма может быть построена из всех содержащихся значений.

Это не единственное использование следствий и, конечно, не самое выдающееся, но я бы сказал, что это достойное упоминание.:-)

Другие вопросы по тегам