11

I have a class with a private field that is a mutable collection. The field in this particular instance is an ArrayBuffer, although my question extends to any finite, ordered, random-access collection type. I want to expose this field without permitting others to modify it. In Java I would add a method like:

private List<T> theList;

public List<T> getList() {
    return Collections.unmodifiableList(theList);
}

In Java we just accept that the result is a List that doesn't fully implement the List interface because #add and friends throw UnsupportedOperationException.

In Scala, I would expect to find an appropriate trait with accessors like iterator, size, and apply (for retrieving values by index) but no mutators. Does such a type exist that I just haven't found yet?

Chris Martin
  • 30,334
  • 10
  • 78
  • 137
  • Is something like the `.toVector` method what you want? It will return a copy of the values in the `ArrayBuffer` as a `Vector` which is immutable and supports fast random access. (`.toVector` is only in Scala 2.10, for 2.9.2 you can do something like `Vector() ++ myArrayBuffer`) – adelbertc Nov 21 '12 at 06:11
  • The collection size may be in the tens of thousands. Is this large enough to justify my aversion to immutable collection copying? – Chris Martin Nov 21 '12 at 06:32
  • Your Java example does not produce an immutable List. It just hides a mutable lists in an immutabl-ish wrapper. Any object keeping a reference to the underlying list can still mutate it... If it's enough for you, you can implement easily the same approach... – paradigmatic Nov 21 '12 at 07:50
  • 4
    Yes, I said "unmodifiable" instead of "immutable", and explicitly referenced a method that does that sort of thing on a java.util.List, because that is precisely the behavior I'm looking for. – Chris Martin Nov 21 '12 at 07:56

4 Answers4

9

The Scala collection library is oriented to immutability, and immutability doesn't refer only to the fact that you are not allowed to modify a given collection, but also that you are guaranteed that the collection won't be ever modified by anyone.

So you cannot and you shouldn't get a collection like immutable.Seq as a view from a mutable buffer in Scala since it breaks that guarantee.

But you can implement the concept of unmodifiable mutable Seq easy enough like so:

class UnmodifiableSeq[A](buffer: mutable.Seq[A]) extends mutable.Seq[A]{
    def update(idx: Int, elem: A) {throw new UnsupportedOperationException()}

    def length = buffer.length

    def apply(idx: Int) = buffer(idx)

    def iterator = buffer.iterator
}

Usage:

val xs = Array(1, 2, 3, 4)
val view = new UnmodifiableSeq(xs)
println(view(2)) >> 3
view(2) = 10 >> Exception in thread "main" java.lang.UnsupportedOperationException

EDIT :

A probably better way of obtaining an unmodifiable view of the collection is by downcasting to collection.Seq which provides no mutable update operations:

val xs = Array(1, 2, 3)
val view: Seq[Int] = xs //this is unmodifiable

or creating a wrapper which extends Seq if you have your own custom mutable class.

class UnmodifiableView[A](col: MutableCollection[A]) extends collection.Seq[A]{
    def length = col.length

    def apply(idx: Int) = col(idx)

    def iterator = col.iterator
}

The scala.collection.Seq trait does not make any guarantees of immutability but it also does not allow any modifying operations so it seems the perfect fit.

Marius Danila
  • 10,311
  • 2
  • 34
  • 37
  • 1
    I agree it would be wrong to break the immutability guarantee of immutable.Seq, but isn't it also a bad idea to break the contract of mutable.Seq#update(Int,A)? – Chris Martin Nov 21 '12 at 16:34
  • 1
    @ChristopherMartin - Yes, I agree. I must confess that I made a mistake in my answer since I assumed that collection.Seq is an alias for collection.immutable.Seq. As it turns out, it isn't. So you can extend collection.Seq which doesn't offer the guarantee of immutability. I'll add the fix in my answer. – Marius Danila Nov 21 '12 at 18:26
  • That's perfect, thanks. I also hadn't expected that there would exist three Seq traits (collection, collection.immutable, collection.mutable). Still a little disappointing that Scala doesn't already have the wrapper class, though. I suppose I'll just cast to collection.Seq and stop being so paranoid about the potential for misuse. – Chris Martin Nov 21 '12 at 19:12
2

Convert your buffer to a Seq, or one of the other unmodifiable collections that are part of the scala.collection package.

myBuffer.toSeq
cheeken
  • 33,663
  • 4
  • 35
  • 42
  • Good (perhaps good enough) suggestion - But it doesn't truly ensure unmodifiability of the returned value. A really dumb coworker could just cast the Seq back to ArrayBuffer and make changes to it. – Chris Martin Nov 21 '12 at 06:29
  • 2
    I just tried it, and it works... scala> import collection.mutable.ArrayBuffer; val a = new ArrayBuffer[Int]; a.append(1); val b:Seq[Int] = a.toSeq; b.asInstanceOf[ArrayBuffer[Int]].append(2); – Chris Martin Nov 21 '12 at 07:51
  • 1
    Well, a stupid enough coworker could also get the underlying collection of `Collections.unmodifiableList` via reflection and modify it. – Alexey Romanov Nov 21 '12 at 08:28
  • 2
    That's far less likely, don't you think? – Chris Martin Nov 21 '12 at 16:03
1

If you want to:

  • use only existing library functions (not write your own wrapper)
  • make sure to avoid copying
  • make sure that the returned value cannot be cast to something modifiable then one possibility is to simply return an iterator

    def getStuff = array.iterator
    

This is not an option, of course, if your specification specifically requires you to return a Seq, but it

  • allows the caller to iterate over it using the same syntax as for Seq's (for (x <- obj.getStuff))
  • allows the caller to easily convert to a Seq or List by using obj.getStuff.toSeq or obj.getStuff.toList.

(Note that the documentation of .iterator does not explicitly say that the returned object cannot be cast to something modifiable, but the current implementation of ArrayBuffer.iterator indeed gives an unmodifiable iterator.)

Dominique Unruh
  • 1,248
  • 8
  • 23
0

If myBuffer.toSeq isn't good enough, you can write your own class extending scala.collection.IndexedSeqOptimized delegating the calls to an underlying collection.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487