12

I've noticed that lazy val repeats computation several times (in case of exception):

scala> lazy val aaa = {println("calc"); sys.error("aaaa")}
aaa: Nothing = <lazy>

scala> aaa
calc
java.lang.RuntimeException: aaaa
  at scala.sys.package$.error(package.scala:27)
  at .aaa$lzycompute(<console>:7)
  at .aaa(<console>:7)
  ... 33 elided

scala> aaa
calc
java.lang.RuntimeException: aaaa
  at scala.sys.package$.error(package.scala:27)
  at .aaa$lzycompute(<console>:7)
  at .aaa(<console>:7)
  ... 33 elided

Shouldn't it be like:

scala> aaa
calc
java.lang.RuntimeException: Not Initialized! 
caused by
java.lang.RuntimeException: aaaa

scala> aaa
java.lang.RuntimeException: Not Initialized! 
caused by
java.lang.RuntimeException: aaaa  
dk14
  • 22,206
  • 4
  • 51
  • 88

1 Answers1

6

In this post they explain very well how lazy val is compiled by the Scala compiler. Basically, if the evaluation of the expression fails, then the indicator-bit signaling that the lazy val contains its data won't be set.

update1:

I think one reason going with the first approach could be that the second one may be emulated by using two lazy vals, without burdening the underlying implementation with multiple volatile variables:

scala> lazy val _aaa = Try {println("calc"); sys.error("aaaa")}
_aaa: scala.util.Try[Nothing] = <lazy>

scala> lazy val aaa = _aaa.get
aaa: Nothing = <lazy>

scala> aaa
calc
java.lang.RuntimeException: aaaa
  at scala.sys.package$.error(package.scala:27)
  at $anonfun$_aaa$1.apply(<console>:10)
  at $anonfun$_aaa$1.apply(<console>:10)
  at scala.util.Try$.apply(Try.scala:191)
  at ._aaa$lzycompute(<console>:10)
  at ._aaa(<console>:10)
  at .aaa$lzycompute(<console>:11)
  at .aaa(<console>:11)
  ... 33 elided

scala> aaa
java.lang.RuntimeException: aaaa
  at scala.sys.package$.error(package.scala:27)
  at $anonfun$_aaa$1.apply(<console>:10)
  at $anonfun$_aaa$1.apply(<console>:10)
  at scala.util.Try$.apply(Try.scala:191)
  at ._aaa$lzycompute(<console>:10)
  at ._aaa(<console>:10)
  at .aaa$lzycompute(<console>:11)
  at .aaa(<console>:11)
  ... 33 elided

update2:

As @Silly Freak has proposed in his comment,

scala> lazy val _aaa = Try {println("calc"); sys.error("aaaa")}
_aaa: scala.util.Try[Nothing] = <lazy>

scala> def aaa = _aaa.get
aaa: Nothing

may work even better, as we can avoid having two lazy vals.

Community
  • 1
  • 1
mutantacule
  • 6,913
  • 1
  • 25
  • 39
  • Already know about how it works. My question is about correctness of approach or maybe better approach? As I believe, nothing stops them (as I see it) from implementing of correct exception handling inside lazy-compute. – dk14 Jul 31 '15 at 10:31
  • I mean, they could have another bit for isFailure (at least) or remember the exception (at most, but more expensive). For now, `0` means both failure/not-initialized, so you can't distinguish them. – dk14 Jul 31 '15 at 10:38
  • 1
    I can't really tell that, but probably for performance reasons (https://twitter.com/djspiewak/status/302489756552536064). For me both approach seems reasonable. Although it's not exactly the same, but depending on your use case, you may emulate your approach by doing `lazy val aaa = Try { println("calc"); sys.error("aaaa") }` – mutantacule Jul 31 '15 at 11:29
  • or even better: `lazy val _aaa = Try { println("calc"); sys.error("aaaa") }; lazy val aaa = _aaa.get` – mutantacule Jul 31 '15 at 11:41
  • I wouldn't agree about performance, but I like the workaround – dk14 Jul 31 '15 at 12:19
  • @SillyFreak it can be, but if _aaa contains a value, you may want to avoid calling `_aaa.get` each time – mutantacule Jul 31 '15 at 12:34
  • 2
    @kosii `_aaa` is either a `Success` or a `Failure`. If it's a success, then `get` has no side effects and it's ok to call it multiple times. If it's a failure, then `get` throws an exception, so the `lazy` won't cache it anyways. As I see it, making `aaa` a `lazy val` does not add anything semantically, no? And `lazy val` is more expensive than `def`. – Silly Freak Jul 31 '15 at 14:39