Bad IArray opaque type interop

This code fails to compile:

object Holder {
  opaque type Id <: Int = Int
  def apply(a: Int): Id = a
}

val arr: IArray[Holder.Id] = IArray.tabulate(10)(Holder.apply)
val x: Holder.Id = arr(1) // this line does not compile

The error is:

Found:    Int
Required: worksheet.Holder.Id

The code starts working if I change IArray back to normal Array.

1 Like

Strangely it also works with arr.head. Something about arr(1) access…

1 Like

I probably caused by the fact IArray is overloaded: scala3/library/src/scala/IArray.scala at d61508a90a9edda1f8c060d72ed327e58e099537 · eejbyfeldt/scala3 · GitHub

And the bound Id <: Int means that IArray[Holder.Id] <:< IArray[Int] and therefore the Int overload is picked.

One work around could be to add another version of apply in the Holder object with will be chosen e.g

  extension (arr: IArray[Id]) def apply(n: Int): Id = arr.apply(n)
1 Like

A better workaround is re-wrapping in Id.apply after array access.

But this is not about finding a wokraround, fixing the root cause is preferred.

Why is apply overloaded, but not head and other methods?

I looked at the git history and it seems like the overloads where add in Disallow inline methods and opaque types in same scope by odersky · Pull Request #6853 · scala/scala3 · GitHub
and the implementation restriction was later removed in
Drop "no inlines with opaques" implementation restriction by odersky · Pull Request #12815 · scala/scala3 · GitHub

toSet also does not play well (but it does with normal Array). So in current form, IArray is unusable. Too many quirks.

object Holder {
  opaque type Id <: Int = Int
  def apply(a: Int): Id = a
}

val arr: IArray[Holder.Id] = IArray.tabulate(10)(Holder.apply)
val x: Set[Holder.Id] = arr.toSet // does not compile

you can fix this with opaque type Id >: Int <: Int = Int,

what’s happening is that because Array is invariant, then Holder.Id is not exactly Int so it wraps in Predef.genericWrapArray.

because IArray is covariant, then IArray[Holder.Id] counts as IArray[Int] so applies IArray.wrapIntArray, and ArraySeq.ofInt requires a lower bound of Int for the type argument to toSet

object Holder {
  opaque type Id >: Int <: Int = Int
  def apply(a: Int): Id = a
}

val arr: IArray[Holder.Id] = IArray.tabulate(10)(Holder.apply)
val x: Set[Holder.Id] = arr.toSet // ok

Perhaps we should look into downcasting as an improvement, so the conversion will convert IArray[Holder.Id] to (new ArraySeq.OfInt(...)).asInstanceOf[ArraySeq[Holder.Id]].

something like:

implicit def wrapIntArrayV2[T <: Int](arr: IArray[T]): ArraySeq[T] =
  mapNull(arr, new ArraySeq.ofInt(arr.asInstanceOf[Array[Int]]).asInstanceOf[ArraySeq[T]])
1 Like

a slight wrinkle - there could be code out there relying on the result of conversion being exactly ArraySeq.ofInt, but we could bridge that by keeping the present conversions as non-implicit

I appreciate the suggestion, but defining the type like this defeats the purpose of an opaque type, making Id always typecheck equivalently to Int.

For example into f(id: Holder.Id) you can pass f(10) and it will typecheck, therefore bypassing the safeguards in Id.apply()

Why cannot IArray have the same behavior as a normal Array?

its covariant (i.e. IArray[+T] ) vs Array[T].

I guess the choice could have been made to keep it invariant, but that can’t change now.

I would basically make the specialised conversions non-implicit so then it will always use the generic conversion (with generic return type), perhaps we can still shortcut the runtime type test within the conversion method.