Skip to content

Conversation

@lihaoyi
Copy link
Contributor

@lihaoyi lihaoyi commented Jan 21, 2026

As discussed in https://contributors.scala-lang.org/t/standard-library-now-open-for-improvements-and-suggestions/7337/66:

Can we merge mutable.ArrayDeque into mutable.Buffer?

As far as I know, ArrayDeque introduced in Scala 2.13 is basically a superset of Buffer’s functionality, and there is no reason that anyone should use Buffer anymore. ArrayDeque supports all the same APIs efficiently, and more due to allowing efficient operations on the other side.

It should be straightforward to implement all the mutable.Buffer operations using ArrayDeque, and add in the other ArrayDeque operations as well. Since the name mutable.Buffer is the “default” mutable buffer semantically, we should make it do what most people would want, rather than forcing them to reach for the more verbose ArrayDeque sometimes but not other times.

This would streamline usage of mutable.Buffer/mutable.ArrayDeque, and remove a stumbling block of having to choose which one to use when there really is one right answer and the obvious answer is wrong

This PR adds the following convenience methods to mutable.Buffer

  /** Optionally removes and returns the first element.
   *
   *  @return  `Some(firstElement)` if the buffer is non-empty, `None` otherwise
   */
  def removeHeadOption(): Option[A]

  /** Removes and returns the first element.
   *
   *  @return  the removed first element
   *  @throws NoSuchElementException if the buffer is empty
   */
  def removeHead(): A

  /** Optionally removes and returns the last element.
   *
   *  @return  `Some(lastElement)` if the buffer is non-empty, `None` otherwise
   */
  def removeLastOption(): Option[A]

  /** Removes and returns the last element.
   *
   *  @return  the removed last element
   *  @throws NoSuchElementException if the buffer is empty
   */
  def removeLast(): A

  /** Removes all elements from this $coll and returns them.
   *
   *  @return a sequence containing all removed elements
   */
  def removeAll(): scala.collection.immutable.Seq[A]
  
  /** Removes all elements from this $coll and returns them in reverse order.
   *
   *  @return a sequence containing all removed elements in reverse order
   */
  def removeAllReverse(): scala.collection.immutable.Seq[A]

  /** Removes and returns elements from the head of this $coll while they satisfy the predicate.
   *
   *  @param f  the predicate used for choosing elements
   *  @return   a sequence containing all removed elements
   */
  def removeHeadWhile(f: A => Boolean): scala.collection.immutable.Seq[A]

  /** Removes and returns elements from the tail of this $coll while they satisfy the predicate.
   *
   *  @param f  the predicate used for choosing elements
   *  @return   a sequence containing all removed elements
   */
  def removeLastWhile(f: A => Boolean): scala.collection.immutable.Seq[A]
  
  /** Finds and removes the first element satisfying a predicate.
   *
   *  @param p    the predicate used for choosing the first element
   *  @param from the start index
   *  @return the first element for which p yields true, or None if no such element exists
   */
  def removeFirst(p: A => Boolean, from: Int = 0): Option[A]
  
  /** Removes and returns all elements satisfying the given predicate.
   *
   *  @param p  the predicate used for choosing elements
   *  @return   a sequence of all removed elements
   */
  def removeAll(p: A => Boolean): scala.collection.immutable.Seq[A]

This PR basically swaps out ArrayBuffer in the Buffer factory objects with ArrayDeque, and copies the unique ArrayDeque convenience methods over while forwarding them to the existing methods like Buffer#remove. You could actually already do all these workflows with Buffer already, just that the API sucked (e.g. buffer.remove(buffer.length - 1)) and the asymptotic complexity of some operations sucked (e.g. buffer.remove(0)).

This PR solves both those problems, so after this for most intents and purposes people reaching for the obvious thing mutable.Buffer() will generally get everything they need without needing to separately reach for mutable.ArrayDeque. There is still some im-precision in the types, since technically a Buffer could be a ListBuffer or some other weird data structure, but that's no worse than where we are today so it's still a strict improvement

Regarding compatibility, we're only adding things to the API, which should be backwards binary and source compatible. And the change from ArrayBuffer to ArrayDeque only improve the asymptotic complexity of some operations while leaving others unchanged

I have updated the Scala.js implementation as well which previously used js.WrappedArray to also use ArrayDeque

@lihaoyi lihaoyi requested a review from a team as a code owner January 21, 2026 15:25
@WojciechMazur WojciechMazur added area:library Standard library stat:feature freeze Issues and PRs waiting for the feature freeze to be lifted. labels Jan 21, 2026
@lihaoyi lihaoyi changed the title Merge mutable.ArrayDeque into mutable.Buffer Merge mutable.ArrayDeque into mutable.Buffer on the JVM Jan 22, 2026
@lihaoyi
Copy link
Contributor Author

lihaoyi commented Jan 22, 2026

the community build failure is benign and expected given the nature of the change

[123-3] X test.pprint.HorizontalTests.Horizontal.collections.Buffer 6ms 
[123-3]   utest.AssertionError: expected.map(_.trim).contains(pprinted)
[123-3]   expected: Seq[String] = ArraySeq(ArrayBuffer("omg", "wtf", "bbq"), WrappedArray("omg", "wtf", "bbq"))
[123-3]   pprinted: String = ArrayDeque("omg", "wtf", "bbq")
[123-3]     utest.asserts.Asserts$.assertImpl(Asserts.scala:30)
[123-3]     test.pprint.Check.apply$$anonfun$1(Check.scala:22)
[123-3]     scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)
[123-3]     scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)
[123-3]     scala.collection.immutable.List.foreach(List.scala:327)
[123-3]     test.pprint.Check.apply(Check.scala:19)
[123-3]     test.pprint.HorizontalTests$.$init$$$anonfun$1$$anonfun$1$$anonfun$3$$anonfun$7(HorizontalTests.scala:115)

@sjrd sjrd added the needs-minor-release This PR cannot be merged until the next minor release label Jan 22, 2026
@sjrd
Copy link
Member

sjrd commented Jan 22, 2026

For now I only change the default on the JVM since that's what I personally care about, and JS is left unchanged. We could conceivably change the default on JS as well to improve the asymptotic complexity there, though we may want benchmarks to check its effect on the performance constant factors

Keeping complexity guarantees in sync between platforms is more important than keeping better constant factors. If we're going to improve complexity guarantees in the JVM, we must also change the JS version.

@lihaoyi
Copy link
Contributor Author

lihaoyi commented Jan 22, 2026

I have updated the Scala.js implementation as well which previously used js.WrappedArray to also use ArrayDeque

The mima changes should go away once we tweak mima to allow standard library evolution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:library Standard library needs-minor-release This PR cannot be merged until the next minor release stat:feature freeze Issues and PRs waiting for the feature freeze to be lifted.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants