Something I just noticed:
val v = List(1,2,3).view.map(f)
v.last // evaluates f 3 times
v(2) // evaluates f once
Since views are often use to avoid evaluating unnecessarily, would it be better to have v.last
behave like v(2)
, even at the cost of a second traversal? Or is this by design, to leave users with a choice (and maybe some confusion)? Right now, last
seems to rely on the default, iterator-based implementation.
2 Likes
The inconsistency surprises me too. Let me test my understanding…
I’m guessing that as List
is a cons-cell implementation, and doesn’t carry any fancy direct-link-through-mutable-private reference to the last element around, then any access to the underlying element by the view has to involve iteration via recursive traversal (or direct recursive traversal).
Or do you expect .view
to cache?
If I’m right so far, then it’s a choice as to whether you want the iteration do be done down in the underlying list prior to applying f
to the last element, or as a ‘pull-through’ iterator that delegates to the underlying list iterator.
You would want a general mapped iterator to do pull-through so that .filter
, .take
etc don’t have to strictly evaluate the mapped elements. That doesn’t stop .last
in the view implementation from being specialized to punch through to the underlying list, execute .last
there and then apply f
- and that is how .apply
works already.
Looking at the library code briefly, it seems that .apply
and .iterator
are implemented as I imagined, and that .last
just comes from the last-resort implementation up in Iterable
, hence the inconsistency in applications of f
.
You knew this already, of course.
Personally, I’d just call f(v.last)
and have done with it if f
is some monstrous performance drain. Or run f
through a caching wrapper like Scalacache / Caffeine. Or use a Vector
as the underlying collection, in which case it will evaluate f
just once for .last
and give near constant-time access for both .last
and .apply(2)
.
It’s true that last
never makes much sense on views anyway.
list.view.map(f).filter(p).headOption
is a more useful pattern than
list.view.map(f).filter(p).lastOption
for which everything has to be computed anyway.
Some behavior can still be hard to guess, though:
val v = List(1, 2, 3).view.map(f)
v.last // evaluates f 3 times
v.reverse.head // evaluates f 3 times
v.reverse(0) // evaluates f once!
(Which means that the documentation is incorrect. For SeqView.reverse
, it states: “Even when applied to a view or a lazy collection it will always force the elements.”)
I guess none of this matters much as long as views are used in typical, useful patterns. It’s just there to trip teachers doing in-class demos
I feel that someone, somewhere will be composing a Scala technical interview question based on this discussion. Groan!
2 Likes