1. Introduction
In this tutorial, we’ll examine the new IArray immutable construct offered by Scala 3 and how it differs from the Array and Vector collections.
2. IArray: Description and Comparisons
Before Scala 3, the Array class was the most natural and direct way to represent arrays. But, the Array class is not fully implemented in Scala – it’s a wrapping class around Java’s array. As a result, it suffers from two problems:
- Arrays are mutable. Hence, it doesn’t enforce the use of functional algorithms as much as an immutable collection would.
- Resulting from the above, arrays are invariant on their type. That’s the safest decision as regards inheritance in type systems, but it also leads to inflexible and convoluted algorithms.
2.1. What Is IArray?
IArray is an object in the scala package, so we don’t need to import it in our programs.
It has several methods to produce immutable arrays and some other utility methods to help us deal with them, such as equals() to compare two of them.
2.2. Why Not Use Vector?
Like IArray, Vector is an immutable, indexed sequence collection providing fast access to elements. However, IArray is specialized for primitive types (e.g., Int, Double, etc.), and it uses a more memory-efficient representation for these types when compared to Vector. This can lead to better performance when working with primitive data types.
Due to its generality, Vector must store each element as an individual object and some extra metadata for efficient indexing and updates. IArray is specialized for primitive types and uses a more memory-efficient representation, typically storing elements more compactly than a generic Vector.
2.3. Covariance in IArray
IArray immutability has allowed designers to make it covariant on its type parameter, opening the door to abstractions previously unavailable. Here’s an example:
class ImmutableArraySpec extends AnyWordSpec {
trait Pet(val name: String, val age: Int)
case class Dog(override val name: String, override val age: Int)
extends Pet(name = name, age = age)
case class Cat(override val name: String, override val age: Int)
extends Pet(name = name, age = age)
val dogs = IArray(Dog("champ", 2), Dog("barky", 3))
val cats = IArray(Cat("overlord", 3), Cat("silky", 5))
"dogs and cats" should {
"get along together" in {
val myPets = cats ++ dogs // myPets is an IArray[Pet]
// is the size of the array correct?
assertResult(4)(myPets.length)
// check contents by full comparison
assert(myPets.indexOf(Dog("barky", 3)) == 3)
assert(myPets.indexOf(Cat("silky", 5)) == 1)
assert(myPets.indexOf(Dog("champ", 2)) == 2)
assert(myPets.indexOf(Cat("overlord", 3)) == 0)
// check contents by predicate
assert(myPets.exists(_.name == "barky"))
assert(myPets.filter(_.age == 3).size == 2)
// check type is respected by closure
assert(myPets(1).isInstanceOf[Cat])
assert(myPets(2).isInstanceOf[Dog])
}
}
// ...
}
In this test, we declare an IArray[Dog] and an IArray[Cat]. Of course, the former can only contain instances of Dog, and the latter can only contain instances of Cat, no surprises here. The magic happens when we declare an IArray[Pet] because, thanks to covariance, IArray[Dog] is an IArray[Pet], as is IArray[Cat], and we can mix and match both arrays!
2.4. Testing Immutability
In this second test, we assert that trying to modify an element of an IArray produces a compilation error:
class ImmutableArraySpec extends AnyWordSpec {
trait Pet(val name: String, val age: Int)
case class Dog(override val name: String, override val age: Int)
extends Pet(name = name, age = age)
case class Cat(override val name: String, override val age: Int)
extends Pet(name = name, age = age)
val dogs = IArray(Dog("champ", 2), Dog("barky", 3))
val cats = IArray(Cat("overlord", 3), Cat("silky", 5))
// ...
"a dogs array" should {
"be immutable" in {
assertDoesNotCompile("dogs(0) = Dog(\"unwanted\", 8)")
}
}
}
Indeed, without using the assertDoesNotCompile() method, let’s try to compile this statement:
dogs(0) = Dog("unwanted", 8)
The code won’t compile, and we’ll receive an error:
value update is not a member of IArray[ImmutableArraySpec.this.Dog]
3. Conclusion
In this article, we learned about the Scala 3 IArray class, which allows an efficient representation of indexed sequences, especially of elements of basic types.
We also learned that IArray, being immutable, lends itself beautifully to functional algorithms and that its covariant nature facilitates abstracting over types, something more challenging to do with invariant collections like Array.
As usual, the full code for this article is available over on GitHub.