Description
Background
(quoted from #11105)
Currently,
LazyList
has two forms of laziness:
- laziness in determining whether or not it is empty (and by extension, how many elements it has)
- independent laziness of elements - that is, evaluating a subsequent
head
ortail
of theLazyList
does not cause the evaluation of previoushead
s.
Form 1 is one of the primary reasons for the creation of LazyList
, is generally maintained throughout operations on it, and I think is is generally agreed to be of very high value.
Form 2 is less obviously valuable. Historically, it exists because the original implementation of LazyList
had a strict state (empty or cons), and lazy head
and tail
(like Stream
, but with a lazy head
as well); when the design of LazyList
was changed to have a lazy state and strict tail
(because lazy tail
doesn't add anything once the state is lazy), the lazy head
was still kept.
During various discussions on various issues, it has been noted that the fact that LazyList
's supports form 2 of laziness
- makes some implementations more complex (though not overly so)
- often adds no value, because the
LazyList
is created in a fashion (e.g. from anIterator
) that does not have form 2 of laziness - is subverted by many common operations (such as
filter
,dropWhile
) which by their nature must evaluate elements sequentially in order to determine the next element
This yields the question (quoted from @sjrd at scala/scala#6880 (comment)):
As added food for thought: I wonder whether it is still useful that the head be lazy. Are there use cases where one already knows that the lazy list is non-empty, but doesn't know yet what its
head
will be (and cannot synchronously compute it)? My experience from programming in Oz (where this lazierLazyList
is basically the base data type used everywhere--and optimized by the VM) doesn't support any such use case. [...]
(see also #11089 (comment))
Issue and Investigation Areas
Should LazyList
support form 2 of laziness by having a lazy head
? There are a few points to think about in this regard:
- How significant is the implementation cost of supporting form 2 of laziness? Keeping in mind that the implementation supporting form 2 of laziness already exists, but must be maintained.
- How significant is the performance cost of having a lazy
head
? - Are there use cases where, despite the fact that the creation of the
LazyList
does not support form 2 of laziness, the fact that some methods (such asmap
) support form 2 of laziness is valuable because it allows performing independently lazy expensive operations? - Are there use cases for which form 2 of laziness is valuable in the creation of the
LazyList
(in contrast to what is discussed in the previous question)? I and several others have yet to come up with one.
I think that if we cannot come up any compelling use cases for form 2 of laziness, taking into account how frequently it is not present anyway, then LazyList
should be changed to have a strict head
and not support form 2 of laziness. I say this despite the fact that I have invested significant effort into writing comprehensive tests which ensure that all methods which can preserve/support form 2 of laziness do so. Once 2.13 is out, it will be difficult to change LazyList
's behaviour even in subsequent releases of Scala because it would be a significant change in semantics (much like the way Stream
cannot be a deprecated type alias for LazyList
).
Consensus
Form 2 of laziness should be removed, and head
made strict.
I don't know if this issue actually needs to be resolved by 2.13.0-RC1, but it definitely needs to be resolved by the final release of 2.13.0, and it would probably be better to resolve it at least a few release candidates beforehand.