Scala ленивые аргументы: как они работают?
В файле Parsers.scala(Scala 2.9.1) из библиотеки комбинаторов парсеров я, кажется, столкнулся с менее известной функцией Scala, называемой "ленивыми аргументами". Вот пример:
def ~ [U](q: => Parser[U]): Parser[~[T, U]] = { lazy val p = q // lazy argument
(for(a <- this; b <- p) yield new ~(a,b)).named("~")
}
По-видимому, здесь происходит кое-что с назначением аргумента call-by-name q
для lazy val p
.
До сих пор мне не удалось выяснить, что это делает и почему это полезно. Может ли кто-нибудь помочь?
Ответы
Ответ 1
Вызовы по имени вызываются каждый раз, когда вы их просите. Lazy vals называются в первый раз, а затем значение сохраняется. Если вы попросите его снова, вы получите сохраненное значение.
Таким образом, такой шаблон, как
def foo(x: => Expensive) = {
lazy val cache = x
/* do lots of stuff with cache */
}
- это окончательный шаблон "отключение-работа-как-долго-как-возможно-и-только-сделай-один раз". Если ваш код никогда не потребует от вас x
вообще, он никогда не будет оценен. Если вам это нужно несколько раз, он будет оцениваться только один раз и храниться для дальнейшего использования. Таким образом, вы делаете дорогостоящий вызов либо нулевым (если возможно), либо одним (если нет) временем, гарантированным.
Ответ 2
Статья в wikipedia для Scala даже отвечает на то, что делает ключевое слово lazy
:
Использование ключевого слова lazy отменяет инициализацию значения до тех пор, пока это значение не будет использовано.
Кроме того, то, что у вас есть в этом примере кода с q : => Parser[U]
, является параметром "вызывать по имени". Параметр, объявленный таким образом, остается неоцененным, пока вы явно не оцените его где-нибудь в своем методе.
Вот пример из scala REPL о том, как работают параметры по имени:
scala> def f(p: => Int, eval : Boolean) = if (eval) println(p)
f: (p: => Int, eval: Boolean)Unit
scala> f(3, true)
3
scala> f(3/0, false)
scala> f(3/0, true)
java.lang.ArithmeticException: / by zero
at $anonfun$1.apply$mcI$sp(<console>:9)
...
Как вы можете видеть, 3/0
вообще не оценивается во втором вызове. Объединение ленивого значения с параметром "вызывать по имени", как показано выше, приводит к следующему значению: параметр q
не оценивается сразу при вызове метода. Вместо этого оно назначается ленивому значению p
, которое также не оценивается немедленно. Только в случае использования p
это приводит к оценке q
. Но, поскольку p
является val
, параметр q
будет оцениваться только один раз, а результат сохраняется в p
для последующего повторного использования в цикле.
Вы можете легко увидеть в repl, что в противном случае может произойти несколько оценок:
scala> def g(p: => Int) = println(p + p)
g: (p: => Int)Unit
scala> def calc = { println("evaluating") ; 10 }
calc: Int
scala> g(calc)
evaluating
evaluating
20