Когда использовать вызов по имени и вызов по значению?

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

6 ответов

Решение

Есть много мест, где по имени может получить производительность или даже корректность.

Простой пример производительности: регистрация. Представьте себе такой интерфейс:

trait Logger {
  def info(msg: => String)
  def warn(msg: => String)
  def error(msg: => String)
}

А потом использовал вот так:

logger.info("Time spent on X: " + computeTimeSpent)

Если info метод ничего не делает (потому что, скажем, уровень ведения журнала был настроен на более высокий уровень), затем computeTimeSpent никогда не вызывается, экономя время. Это часто случается с регистраторами, где часто можно увидеть манипуляции со строками, которые могут быть дорогостоящими по сравнению с регистрируемыми задачами.

Пример корректности: логические операторы.

Вы, наверное, видели такой код:

if (ref != null && ref.isSomething)

Скажи, что ты объявил && метод как это:

trait Boolean {
  def &&(other: Boolean): Boolean
}

тогда всякий раз, когда ref является nullвы получите ошибку, потому что isSomething будет вызван на null ссылка перед передачей в &&, По этой причине фактическая декларация:

trait Boolean {
  def &&(other: => Boolean): Boolean =
    if (this) this else other
}

Так что на самом деле можно задаться вопросом, когда использовать вызов по значению. Фактически, в языке программирования Haskell все работает подобно тому, как работает вызов по имени (похоже, но не то же самое).

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

Вызов по имени означает, что значение оценивается во время обращения к нему, в то время как при вызове по значению сначала оценивается значение, а затем передается методу.

Чтобы увидеть разницу, рассмотрим этот пример (полностью нефункциональное программирование только с побочными эффектами;)). Допустим, вы хотите создать функцию, которая измеряет, сколько времени занимает какая-то операция. Вы можете сделать это с помощью вызова по имени:

def measure(action: => Unit) = {
    println("Starting to measure time")
    val startTime = System.nanoTime
    action
    val endTime = System.nanoTime
    println("Operation took "+(endTime-startTime)+" ns")
}

measure {
    println("Will now sleep a little")
    Thread.sleep(1000)
}

Вы получите результат (YMMV):

Starting to measure time
Will now sleep a little
Operation took 1000167919 ns

Но если вы измените только подпись measure в measure(action: Unit) поэтому он использует передачу по значению, результат будет:

Will now sleep a little
Starting to measure time
Operation took 1760 ns

Как видите, action оценивается раньше measure даже запускается, а также истекшее время близко к 0 из-за действия, которое уже было выполнено до вызова метода.

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

Простой способ объяснить это

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

Я всегда думал, что эта терминология излишне запутана. Функция может иметь несколько параметров, которые различаются по их названию по сравнению с состоянием по значению. Таким образом, дело не в том, что функция называется call-by-name или call-by-value, а в том, что каждый из ее параметров может быть pass-by-name или pass-by-value. Кроме того, "вызов по имени" не имеет ничего общего с именами. => Int отличается от типа Int; это "функция без аргументов, которая будет генерировать Int" против просто Int. Когда у вас есть первоклассные функции, вам не нужно изобретать терминологию по имени для описания этого.

Вызов по имени оценивается каждый раз, когда он вызывается. Но никогда не оценивается, если он никогда не вызывается. Поэтому, если ваш вариант использования подходит или нуждается в новой оценке каждый раз (например, получение откуда-то свежих/последних данных), это полезно и никогда не оценивается, если никогда не используется. См. код ниже. Удалите "=>" и посмотрите, что произойдет. Перед вызовом метода "m" будет только один вызов.

      object RunMe extends App {

  def m(b: Boolean, m: => String) = {
    if(b) m
  }

  def f = {
    println("hello world")
    "completed"
  }
  println("starting...")
  m(false, f)

}

Хороший вариант для случая использования логирования вызова по имени: https://www.tutorialspoint.com/scala/functions_call_by_name.htm#:%7E:text=For%20this%20circumstance%2C%20Scala%20offers,and%20the%20value%20is%20calculated.

Когда параметр call-by-name используется в функции более одного раза, параметр оценивается более одного раза.

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

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