Подтвердить что ты не робот

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.

До сих пор мне не удалось выяснить, что это делает и почему это полезно. Может ли кто-нибудь помочь?

4b9b3361

Ответ 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