nonEmpty method in iterables

class Word(str: String) extends Iterable[Char]:
  def iterator(): Iterator[Char] = str.iterator
  override def isEmpty: Boolean = str.isEmpty
  override def nonEmpty: Boolean = !isEmpty

This code generates a warning for overriding a deprecated nonEmpty method in IterableOnceOps. Does that mean I cannot define my own iterable with a nonEmpty method? I have to live with a deprecated, iterator-based nonEmpty method instead of relying on my own isEmpty?

(Scastie link: Scastie - An interactive playground for Scala.)

An Iterable should only provide an iterator out of it, is not really a collection per se.

You should rather implement IterableOnce and some of the Ops traits which are the real collection.

That’s a strange view. The Iterable doc states: “Base trait for generic collections.” Even if an iterable doesn’t have to be a collection, many collections are iterable. How can they indicate that they are? Indeed, IterableOnce is the better trait for things that may not be collections, i.e., that can only be iterated once. What type should a collection, which can be iterated repeatedly, implement, if not Iterable?

The Ops traits.

Since IterableOps extends IterableOnceOps, I don’t think this is going to solve my nonEmpty problem. Also, I’m not implementing a generic collection but only a sequence of Char. I’m looking for a way to say “I have an iterator”, like java.lang.Iterable does. Maybe there isn’t such a type in Scala.

I think you have to do something along these lines:

class Word(str: String) extends Iterable[Char] with IterableOps[Char, Iterable, Word]:
  override def fromSpecific(coll: IterableOnce[Char]): Word = Word(coll.iterator.mkString)
  override def newSpecificBuilder: Builder[Char, Word] = super.newSpecificBuilder.mapResult(chars => Word(chars.mkString))
  override def empty: Word = Word("")
  def iterator(): Iterator[Char] = str.iterator
  override def isEmpty: Boolean = str.isEmpty
  override def toString() = s"Word($str)"

Overriding nonEmpty is still deprecated, but that’s because it is implemented as !isEmpty in IterableOnceOps.

If that’s really all you want, I think that’s more or less the definition of IterableOnce. But then of course almost all methods except iterator will be deprecated by design.

As I mentioned above the nonEmpty that you get through IterableOnceOps (which Iterable and IterableOps extend) actually has an efficient !isEmpty implementation. That method should actually be final and all those methods on IterableOnce should not exist. But that’s not the case because of some migration philosophy for people coming from Scala 2.12, from back when they thought they could still clean up the std library way before Scala 4.0.

2 Likes

In Scala 2, the warning will not be issued if the override is itself deprecated:

scala 2.13.10> class Word(str: String) extends Iterable[Char] {
             |   def iterator: Iterator[Char] = str.iterator
             |   override def isEmpty: Boolean = str.isEmpty
             |   override def nonEmpty: Boolean = !isEmpty
             | }
                 override def nonEmpty: Boolean = !isEmpty
                              ^
On line 4: warning: overriding method nonEmpty in trait IterableOnceOps is deprecated (since 2.13.0): nonEmpty is defined as !isEmpty; override isEmpty instead
class Word

scala 2.13.10> class Word(str: String) extends Iterable[Char] {
             |   def iterator: Iterator[Char] = str.iterator
             |   override def isEmpty: Boolean = str.isEmpty
             |   @deprecated("...","...") override def nonEmpty: Boolean = !isEmpty
             | }
class Word

In Scala 3, that doesn’t make the warning go away. I believe that’s a compiler bug you should report at Issues · lampepfl/dotty · GitHub.

As a workaround in the meantime, I expect it would work to silence the warning with @nowarn.

1 Like

I think if all I need is to say “I have an iterator”, I should simply implement an iterator method and forget about Iterable. In Java, expressing “I have an iterator” using the Iterable interface enables the foreach loop, but in Scala, there is not such benefit.

I also thought that, if I really need the Iterable type, I can get it using a lambda, i.e.:

val w = Word("foo")
val t: Iterable[Char] = () => w.iterator

but that doesn’t work. I have to write the more cumbersome:

val t: Iterable[Char] = new Iterable[Char] { def iterator = w.iterator }

Anyway, I’m all set. Thanks.

Extending IterableOnce does enable some interop with the scala collections. It’s why Option extends IterableOnce since Scala 2.13.

2 Likes

Someone has to intervene with a collections “dad joke”, but that person won’t be me.

Thanks for the “user story”, as I just realized that my recent tweak to IterableOnceOps to “prefer knownSize to isEmpty” should actually include using knownSize from isEmpty.

For now, I will say, based on that PR experience, the best simple thing is:

object Test extends App {
  val s = "hello, world"
  val chars = new collection.AbstractIterable[Char] {
    def iterator: Iterator[Char] = s.iterator
    override def knownSize = s.knownSize    // s.length
    override def isEmpty = s.isEmpty   // knownSize == 0 || super.isEmpty  // push upstream
  }
}

That ensures your trivial implementation is maximally trivial and free of mixin.

I’ll contribute that obvious [sic?] improvement to isEmpty.

SAM conversion was broken when they stripped all the parens from iterator.

You can restore them in a custom subclass, which I named LIterable because it lets you use your function literal for an Iterable. Let the puns begin.

There is a Scala 2 bug for which I just created a ticket: adding parens is allowed in Scala 3 but not Scala 2. I don’t recall if there is a reason.

As an additional PSA, the Scala 3 import syntax is supported under Scala 2 -Xsource:3, which I always use unless other conditions don’t permit.

As another PSA, missing backtick and hitting escape in this UI is unnerving.

import collection.AbstractIterable
abstract class LIterable[+A] extends AbstractIterable[A] {
  def iterator(): Iterator[A]
}

object Test extends App {
  val s = "hello, world"
  val chars = new AbstractIterable[Char] {
    def iterator: Iterator[Char] = s.iterator
    override def knownSize = s.knownSize
  }
  import java.lang.{Iterable as JIterable}
  import scala.jdk.CollectionConverters.*
  val schars: LIterable[Char] = () => s.iterator
  val jchars: JIterable[Char] = () => s.iterator.asJava
  val easy = jchars.asScala
  println(chars.partition('o'.==))
  println(schars.partition('o'.==))
  println(easy.partition('o'.==))
}

'o'.== is a tribute to Orion, the craft currently visiting the moon. That is a solar array sticking out.

1 Like

Unrelated topic, but that’s something I was wondering while writing my example (and I ended up being inconsistent). In terms of uniform access principle, it doesn’t make sense to me. iterator must involve the construction of a fresh object and cannot possibly be a field. What’s the rationale for not having parentheses? Simply that x.iterator.find(...) looks better than x.iterator().find(...)? More generally, is there a document somewhere that describes a consistent principle/philosophy to follow?

(I’ve always disliked new Foo, but now it’s just Foo() without new, so I’m happy. I also struggle with methods like Future.get in Java, which may block a thread. And something like iterator is another situation where I find it hard to remain consistent.)

see design discussion at Remove empty parameter list from `iterator()` · Issue #520 · scala/collection-strawman · GitHub

2 Likes

Thanks, I also enjoyed the link there to the ancient mailing list thread.

On the morning of the third day, Seth changed his mind and reversed his opinion. Luckily, by referential transparency, all he needed to do was seth.opinion.reverse.

It’s nice to see that Odersky was unwavering on this point throughout the decade. I can’t tell if he was overruled or ignored.

I’ve often wondered if I would understand today those old Tony Morris adages:

I think a placebo has a relative non-zero adverse impact compared to null.

I guess not. But if null means nullary as in f(), then instead of “parameterless” for f (as the Dotty docs have it), we can now say, “placebary” or perhaps “placatary” (alt for placatory). I still prefer Scala 2’s nullary, nilary, nelary, to indicate the length of paramss.head.

The current style guide describes its policy for method invocation rather than definition. That is, all methods could be defined with parens, but coder discipline would demand their omission when the method usage looks like a variable. (The compiler would supply invisible parens via “empty application”.)

EDIT: definition site paren policy is under Naming Conventions / Methods / Parentheses

Also interesting is the objection to infix alphabetic xs addOne item, which is not that it is hard to parse visually, but that it is side-effecting and therefore should have parens.

Resist the urge to omit parentheses simply to save two characters!

Especially if they are bad characters, in which case they are most likely beyond saving.