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.
atOrElseon aSeq(and onArray) is supposed to return the default for indices outside the collection. The implementation usesfa.applyOrElse(idx, ...)to produce the value to update — which correctly returns the default — but then writes the result back withfa.updated(idx, ...), andSeq.updatedrequires0 <= idx < length, so it throws.Reproducer (scala-cli):
Output:
Source pointer (Seq, Array):
quicklens/quicklens/src/main/scala-3/com/softwaremill/quicklens/package.scala
Lines 174 to 186 in 0b5935c
The
Mapoverload doesn't have this problem becauseMap.updated(k, v)inserts when the key is absent. ForSeqthe simplest fix is to skip the write when the index doesn't exist (treatatOrElseas a no-op for missing indices) or to append, depending on intended semantics:Whichever semantics you'd prefer, the current "use default to compute, then crash on write" path is clearly not it. Happy to PR.