Skip to content

Seq/Array atOrElse throws IndexOutOfBoundsException for missing indices instead of using default #301

@haskiindahouse

Description

@haskiindahouse

atOrElse on a Seq (and on Array) is supposed to return the default for indices outside the collection. The implementation uses fa.applyOrElse(idx, ...) to produce the value to update — which correctly returns the default — but then writes the result back with fa.updated(idx, ...), and Seq.updated requires 0 <= idx < length, so it throws.

Reproducer (scala-cli):

//> using scala 3.8.3
//> using dep com.softwaremill.quicklens::quicklens:1.9.12

import com.softwaremill.quicklens.*

case class Item(name: String)

@main def repro(): Unit =
  val items: List[Item] = List(Item("a"), Item("b"))
  val result = modify(items)(_.atOrElse(5, Item("default")).name).using(_.toUpperCase)
  // throws IndexOutOfBoundsException: 5

Output:

java.lang.IndexOutOfBoundsException: 5 is out of bounds (min 0, max 1)
  at quicklens... atOrElse(...)

Source pointer (Seq, Array):

def atOrElse[A](fa: S[A], f: A => A, idx: Int, default: => A): S[A] =
fa.updated(idx, f(fa.applyOrElse(idx, Function.const(default)))).asInstanceOf[S[A]]
def index[A](fa: S[A], f: A => A, idx: Int): S[A] =
if fa.isDefinedAt(idx) then fa.updated(idx, f(fa(idx))).asInstanceOf[S[A]] else fa
}
given QuicklensIndexedFunctor[Array, Int] with {
def at[A](fa: Array[A], f: A => A, idx: Int): Array[A] =
implicit val aClassTag: ClassTag[A] = fa.elemTag.asInstanceOf[ClassTag[A]]
fa.updated(idx, f(fa(idx)))
def atOrElse[A](fa: Array[A], f: A => A, idx: Int, default: => A): Array[A] =
implicit val aClassTag: ClassTag[A] = fa.elemTag.asInstanceOf[ClassTag[A]]
fa.updated(idx, f(fa.applyOrElse(idx, Function.const(default))))

The Map overload doesn't have this problem because Map.updated(k, v) inserts when the key is absent. For Seq the simplest fix is to skip the write when the index doesn't exist (treat atOrElse as a no-op for missing indices) or to append, depending on intended semantics:

def atOrElse[A](fa: S[A], f: A => A, idx: Int, default: => A): S[A] =
  if fa.isDefinedAt(idx) then
    fa.updated(idx, f(fa(idx))).asInstanceOf[S[A]]
  else
    fa.appended(f(default)).asInstanceOf[S[A]]   // or: pad with default up to idx

Whichever semantics you'd prefer, the current "use default to compute, then crash on write" path is clearly not it. Happy to PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions