Skip to content

Determine how lazy LazyList should be #11307

Closed
@NthPortal

Description

@NthPortal

Background

(quoted from #11105)

Currently, LazyList has two forms of laziness:

  1. laziness in determining whether or not it is empty (and by extension, how many elements it has)
  2. independent laziness of elements - that is, evaluating a subsequent head or tail of the LazyList does not cause the evaluation of previous heads.

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 an Iterator) 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 lazier LazyList 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:

  1. 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.
  2. How significant is the performance cost of having a lazy head?
  3. 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 as map) support form 2 of laziness is valuable because it allows performing independently lazy expensive operations?
  4. 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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions