Vedanshu
Vedanshu

Reputation: 2296

Why value is not a member of class in ArrayBuffer[Any]

I've just started learning scala. In classes primary constructor parameters with val and var are public whereas parameters without val or var are private values. So, when I'm trying to execute following code, everything should work fine.

import scala.collection.mutable.ArrayBuffer

class Cat(val name: String)
class Dog(val name: String)

val dog = new Dog("Harry")
val cat = new Cat("Sally")

val animals = ArrayBuffer.empty[Any]
animals.append(dog)
animals.append(cat)
animals.foreach(pet => println(pet.name))

But I'm getting following error:

ScalaFiddle.scala:12: error: value name is not a member of scala.this.Any
  animals.foreach(pet => println(pet.name))  // Prints Harry Sally

Why is this happening ?

Upvotes: 3

Views: 1118

Answers (2)

Jörg W Mittag
Jörg W Mittag

Reputation: 369430

In this line:

val animals = ArrayBuffer.empty[Any]
//                              ↑↑↑

You explicitly tell Scala that your animals ArrayBuffer can contain Any object. As a result, you can only use methods of Any which are not that many.

It is, for example, perfectly legal to put an Int into animals, since you told Scala that the contents of animals can be anything. However, Int doesn't have a name method, and thus your code would blow up at runtime if you tried to get the name of an Int. In order to prevent your code from blowing up at runtime, Scala rather decides to not allow you to write that code at all in the first place.

So, you need to tell Scala what kinds of things you want to put into the animals. Unfortunately, we only have two explicit nominal types at our disposal here: Dog and Cat. However, when you type animals as ArrayBuffer[Dog], then you can't put Sally in there, and if you type it as ArrayBuffer[Cat], then you can't put Harry in there.

So, we need to type animals as something that allows both Dogs and Cats.

In Scala 3, you could use a Union Type:

val animals = ArrayBuffer.empty[Dog | Cat]

Alas, Scala 3 is still quite far away.

Another way would be to use a Compound type with structural refinement:

val animals = ArrayBuffer.empty[{val name: String}]

This allows your code to work, but it may not necessarily do what you want: this allows any object that has a val named name of type String, not just Dogs and Cats. In particular, you could put something in animals which is not an animal, as long as it has a name.

The best way would probably be to introduce an abstract supertype (let's call it Pet) that defines an abstract val name that gets overridden by Cat and Dog, and then type animals as ArrayBuffer[Pet]:

trait Pet {
  val name: String
}

class Dog(val name: String) extends Pet
class Cat(val name: String) extends Pet

val animals = ArrayBuffer.empty[Pet]

Upvotes: 4

Mario Galic
Mario Galic

Reputation: 48400

ArrayBuffer.empty[Any] specifies array buffer is storing Any values, not Cats and Dogs, and Any does not know about name. Try relating Cat and Dog via Animal trait like so

trait Animal {
  val name: String
}
class Cat(val name: String) extends Animal
class Dog(val name: String) extends Animal

and then specify the array buffer to store Animal like so

val animals = ArrayBuffer.empty[Animal]
animals.append(dog)
animals.append(cat)
animals.foreach(pet => println(pet.name))

which outputs

Harry
Sally

Upvotes: 4

Related Questions