Why both "::" and "#::" are needed?

Why do we need :: for List and #:: for LazyList? Why can not we get along with just ::?

I mean, they are just names for two different methods.
Just like 1 + 2 and 3.0 + 5.0 and "foo" + "bar" are all different methods with the same name.

So, yeah, we need two different methods since those two are two different data structures; but they didn’t need to be named differently.
However, I would guess the rationale for having a slightly different but similar name was to make it clear at simple eyesight that at the end this is just a cons operation, but on a LazyList instead of a normal List

But let me ask more precise, is there even a single case, where :: and #:: would be used differently?

I mean, what do you mean with differently?

Both :: & #:: mean cons; or in plain English, prepend an element to a “list”.
Is just that :: is defined inside immutable.List whereas #:: inside immutable.LazyList; two different classes / datastructures.

So again, is like asking if + on Int is different to + on Double

Ok, thank you!

I honestly cannot see how this is an answer, maybe I just don’t understand it. If it was the case that we had + for Int and #+ for Double then your analogy would, AFAICS, be valid. Given that + works for all numbers, why doesn’t :: work for all kinds of lists? I too find it strange that another type of List needs a separate/different prepend-operator.

1 Like

So, since I have seen this question a couple of times it has two sides; either people think they could just use :: for LazyList or they wonder why the different name.

In this case, I wasn’t sure what was OP case so I tried to answer both.
I first mention that since List and LazyList are different data structure / classes then they both need two different methods to prepend; whenever or not they are named the same, they are still two different ones. Then, I also mention that the method could be named the same, there is no problem with that (I just noticed that I actually wrote this wrong on my original first reply, I have edited it for clarity).
Finally, I just give my honest impression that the rationale for having two different names is probably only for visual clarity, since laziness is something that is deliberately chosen with good motives it may be good to try to make it as explicit as possible.

However, being honest, we can’t really answer that question.
Since the answer may as well be: “the maintainers liked that symbol” or even “just a typo that was merged lol” (of course not, but I guess I made my point)
Only the maintainers may provide the real answer to why the distinct name was chosen.

It seems to me that the reason for different names is that they have very different semantics. In particular, one is eager and one is lazy. That makes a big difference at runtime and my guess is that the people who named them thought that was enough of a difference to warrant different names. Expecting an operation to be eager, when it is actually lazy, can cause a lot of challenging and subtle bugs.

1 Like

@MarkCLewis Actually, that is the answer I was expecting, but can you please provide a short illustration of the eagerness of ::. Is it that :: is eager just because List is not lazy, or there is more to it?

Is just because List.:: is implemented in a strict way, and LazyList.#:: is implemented in a lazy way.
Again, that last one can easily be named :: instead, the code would be the same.

@yarchik, I’m not certain what answer would help here, but I’d first look at the signatures of the two.

def#::[B >: A](elem: => B): LazyList[B]
def::[B >: A](elem: B): List[B]

The => in the type of the element says that it is a by-name argument. (I apologize if these are things you know, I don’t know how much experience you have with Scala and perhaps the details might help others even if you know them.) So the argument isn’t evaluated then passed. Instead, a “thunk” is passed that wraps up the computation so it can be performed later.

The impact of this is illustrated by all the examples at the top of Scala Standard Library 2.13.8 - scala.collection.immutable.LazyList. I’ll pick one specifically though.

val lazylist1: LazyList[Int] = {
  def loop(v: Int): LazyList[Int] = v #:: loop(v + 1)
  loop(0)
}

Try converting that to a List using :: and you will get a stack overflow.

val list1: List[Int] = {
   def loop(v: Int): List[Int] = v :: loop(v+1)
   loop(0)
}

Just looking at the definition of loop in lazyList1 it looks like infinite recursion in an eager language because there is no conditional for the recursion. It’s only safe because it isn’t eager. The things is, most things in Scala are eager, including collections. So it is helpful to have something that makes it clear that an operation like this isn’t going to be evaluated eagerly.

At least, that is my take on why this name was selected.

2 Likes

Just to make the whole thing more confusing, there’s also +:, which works on all Seqs:

scala 2.13.8> 1 +: List(2)
val res2: List[Int] = List(1, 2)

scala 2.13.8> 1 +: LazyList(2)
val res3: scala.collection.immutable.LazyList[Int] = LazyList(<not computed>)

And note that :: isn’t only a method, but also a class:

scala 2.13.8> new collection.immutable.::(1, Nil)
val res5: scala.collection.immutable.::[Int] = List(1)
1 Like

I would also be interested in hearing where the # prefix came from.

The original scala.Stream had only a lazy_:: extractor which was aliased #:: with the so-called massive new collections when #:: was added as an enhanced cons.

The sys.process API from sbt also uses #op to mean “not your parents’ op”.

The syntax broke down a bit, as #:: extracts both lazy collections.

What is the lore by which # acquired this meaning?

1 Like

One thing I don’t know is whether +: and ++ have the expected laziness semantics on LazyList, or whether you sometimes actually need to use #:: or #::: instead in order to get the right laziness.

(And when digging into the history, it’s possible that the answer might be different for the old Stream and the new LazyList…)

2 Likes