7

Say you have a class Person, and create a collection class for it by extending e.g. ArrayBuffer:

class Persons extends ArrayBuffer[Person] {
// methods operation on the collection
}

Now, with ArrayBuffer, can create a collection with the apply() method on the companion object, e.g.:

ArrayBuffer(1, 2, 3)

You want to be able to do the same with Persons, e.g.:

Persons(new Person("John", 32), new Person("Bob", 43))

My first intuition here was to extend the ArrayBuffer companion object and getting the apply() method for free. But it seems that you can't extend objects. (I'm not quite sure why.)

The next idea was to create a Persons object with an apply() method that calls the apply method of ArrayBuffer:

object Persons {
    def apply(ps: Person*) = ArrayBuffer(ps: _*) 
}

However, this returns an ArrayBuffer[Person] and not a Persons.

After some digging in the scaladoc and source for ArrayBuffer, I came up with the following, which I thought would make the Persons object inherit apply() from GenericCompanion:

EDIT:

object Persons extends SeqFactory[ArrayBuffer] {
    def fromArrayBuffer(ps: ArrayBuffer[Person]) = {
        val persons = new Persons
        persons appendAll ps
        persons
    }

    def newBuilder[Person]: Builder[Person, Persons] = new ArrayBuffer[Person] mapResult fromArrayBuffer
}

However, it gives the following error message:

<console>:24: error: type mismatch;
 found   : (scala.collection.mutable.ArrayBuffer[Person]) => Persons
 required: (scala.collection.mutable.ArrayBuffer[Person(in method newBuilder)])
=> Persons
        def newBuilder[Person]: Builder[Person, Persons] = new ArrayBuffer[Perso
n] mapResult fromArrayBuffer
             ^

Perhaps this should disencourage me from going further, but I'm having a great time learning Scala and I'd really like to get this working. Please tell me if I'm on the wrong track. :)

Knut Arne Vedaa
  • 15,372
  • 11
  • 48
  • 59
  • 2
    You can't extend an object because it defines an instance rather than a type. This analogy might help -- you can have sub-categories of SoftwareEngineer (say, JavaHacker), but it doesn't make sense to talk about sub-categories of Martin Fowler. In the same way, it doesn't make sense to extend an instance. – Aaron Novstrup Jul 31 '10 at 01:18
  • 1
    I think extending here is a really bad idea. *Are* persons a special kind of ArrayBuffer? No. So you should clearly prefer composition here. – Landei Jul 31 '10 at 12:08
  • That's an interesting point, and one that could warrant it's own discussion, but I think it's a more philosophical that practical one. (I.e., why is it a bad idea in practice.) You could say that Persons is a sequence of persons, however Seq is abstract so you inherit from the concrete implemention ArrayBuffer instead. – Knut Arne Vedaa Jul 31 '10 at 13:06
  • 1
    Inheritance is a bad idea in practice in this case because it ties you to a particular collection implementation. Suppose you find out that for a particular group of Persons, you need to be able to quickly (i.e. in constant time) determine whether a Person is in that group. Clearly, ArrayBuffer would be a horrible choice for this use case. If you use composition, on the other hand, the methods in your Persons class can apply equally well whether the underlying collection is a Set or an ArrayBuffer. – Aaron Novstrup Jul 31 '10 at 20:26
  • Don't know whether you're still interested in this, but... The reason for that compiler error is that the newBuilder method is generic -- saying `def newBuilder[Person]: Builder[Person, Persons] ...` is no different from `def newBuilder[X]: Builder[X, Persons] ...`. On a separate note, I added to my solution to show how you can abstract away the underlying collection implementation (trying to learn higher-kinded types). – Aaron Novstrup Aug 01 '10 at 07:55
  • But if you use composition, and you want to use collection methods like filter, map etc. on Persons, wouldn't you have to implement them as def filter = self.filter, def map = self.map...? I can see why you don't have to do that with the "pimp" pattern, but in general? – Knut Arne Vedaa Aug 01 '10 at 08:43
  • 1
    No, you can use the SeqProxy trait to get those methods for free: class Persons (val self: Seq[Person]) extends SeqProxy[Person] – Aaron Novstrup Aug 01 '10 at 16:15
  • Very interesting. Looking at the source for the Proxy classes, however, that's exactly (or maybe roughly) what they do. :) – Knut Arne Vedaa Aug 01 '10 at 17:21
  • The plural of Person is People ;) – nicodemus13 Aug 17 '11 at 22:53

2 Answers2

3

Rather than extending ArrayBuffer[Person] directly, you can use the pimp my library pattern. The idea is to make Persons and ArrayBuffer[Person] completely interchangeable.

class Persons(val self: ArrayBuffer[Person]) extends Proxy {
   def names = self map { _.name }

   // ... other methods ...
}

object Persons {
   def apply(ps: Person*): Persons = ArrayBuffer(ps: _*)

   implicit def toPersons(b: ArrayBuffer[Person]): Persons = new Persons(b)

   implicit def toBuffer(ps: Persons): ArrayBuffer[Person] = ps.self
}

The implicit conversion in the Persons companion object allows you to use any ArrayBuffer method whenever you have a Persons reference and vice-versa.

For example, you can do

val l = Persons(new Person("Joe"))
(l += new Person("Bob")).names

Note that l is a Persons, but you can call the ArrayBuffer.+= method on it because the compiler will automatically add in a call to Persons.toBuffer(l). The result of the += method is an ArrayBuffer, but you can call Person.names on it because the compiler inserts a call to Persons.toPersons.

Edit:

You can generalize this solution with higher-kinded types:

class Persons[CC[X] <: Seq[X]](self: CC[Person]) extends Proxy {
   def names = self map (_.name)
   def averageAge = {
      self map (_.age) reduceLeft { _ + _ } / 
            (self.length toDouble)
   }
   // other methods
}

object Persons {
   def apply(ps: Person*): Persons[ArrayBuffer] = ArrayBuffer(ps: _*)

   implicit def toPersons[CC[X] <: Seq[X]](c: CC[Person]): Persons[CC] =
         new Persons[CC](c)

   implicit def toColl[CC[X] <: Seq[X]](ps: Persons[CC]): CC[Person] = 
         ps.self
}

This allows you to do things like

List(new Person("Joe", 38), new Person("Bob", 52)).names

or

val p = Persons(new Person("Jeff", 23))
p += new Person("Sam", 20)

Note that in the latter example, we're calling += on a Persons. This is possible because Persons "remembers" the underlying collection type and allows you to call any method defined in that type (ArrayBuffer in this case, due to the definition of Persons.apply).

Aaron Novstrup
  • 20,967
  • 7
  • 70
  • 108
1

Apart from anovstrup's solution, won't the example below do what you want?


case class Person(name: String, age: Int)

class Persons extends ArrayBuffer[Person]

object Persons {
    def apply(ps: Person*) = {
      val persons = new Persons
      persons appendAll(ps)
      persons
    }
}

scala> val ps = Persons(new Person("John", 32), new Person("Bob", 43))
ps: Persons = ArrayBuffer(Person(John,32), Person(Bob,43))

scala> ps.append(new Person("Bill", 50))   

scala> ps
res0: Persons = ArrayBuffer(Person(John,32), Person(Bob,43), Person(Bill,50))
Arjan Blokzijl
  • 6,878
  • 2
  • 31
  • 27
  • I'm aware of this solution, but I thought it would be more elegant to inherit the apply method. object ArrayBuffer itself does not define it's apply method, but inherits it from GenericCompanion, or so it seems, correct me if I'm wrong. This seems to be the simplest practical solution to the original problem though. – Knut Arne Vedaa Jul 31 '10 at 09:01