2
votes

I'm thinking of a following Scala class layout. I have a basic trait that represents an Item - an interface of what ought to be an immutable object that we can query for name, weight and do some object-specific stuff by invoking methods like equip:

trait Item {
  def name: String
  def weight: Int
  def equip // ... more abstract methods
}

I can create Item implementations, creating case classes by hand, but I'd like to have some sort of "dictionary"-based items - i.e. a static map holds mapping from type ID to values, name and weight methods just query the dictionary with a stored type ID:

object Weapon {
  final val NameDict = Map(1 -> "short sword", 2 -> "long sword")
  final val WeightDict = Map(1 -> 15, 2 -> 30)
}
case class Weapon(typ: Int) extends Item {
  import Weapon._
  def name = NameDict(typ)
  def weight = WeightDict(typ)
  def equip = ... // implementation for weapon
}

So far, so good. I can use Weapon(1) to reference an item object for "short sword". However, the same basic principle applies to any other item types, such as Armor, which uses exactly the same name and weight implementation, but completely different equip and other abstract methods implementation, for example:

object Armor {
  final val NameDict = Map(3 -> "bronze armor", 4 -> "iron armor")
  final val WeightDict = Map(3 -> 100, 4 -> 200)
}
case class Armor(typ: Int) extends Item {
  import Armor._
  def name = NameDict(typ)
  def weight = WeightDict(typ)
  def equip = ... // implementation for armor
}

Looks pretty similar to Weapon, isn't it? I'd like to factor out the common pattern (i.e. implementation of lookups in companion object dictionaries and common typ value) in something like that:

abstract class DictionaryItem(typ: Int) extends Item {
  def name = ???.NameDict(typ)
  def weight = ???.WeightDict(typ)
}

object Weapon {
  final val NameDict = Map(1 -> "sword", 2 -> "bow")
  final val WeightDict = Map(1 -> 15, 2 -> 30)
}
case class Weapon(typ: Int) extends DictionaryItem(typ) {
  def equip = ... // implementation for weapon
}

But how do I reference a children's companion object (like Weapon.NameDict) from parent class - i.e. what should I use instead of ???.NameDict(typ) and how do I explain to the compiler that children's companion object must include these dictionaries? Is there a better, more Scala-esque approach to such a problem?

3
Perhaps this is a related question you want to check out stackoverflow.com/questions/12179092/…Kane
Thanks, Kane! It is related, yes.GreyCat

3 Answers

4
votes

Instead of looking up the names and weights of items in maps, you could make use of instances of your case classes. In fact there are some good reasons you might want to take this approach:

  1. Avoid Complexity: It's simple enough, you might not have needed to post this question about how inheritance works with companion classes.
  2. Avoid Repetition: Using case classes, you would only have to define your items once.
  3. Avoid Errors: Using synthetic keys, would require you to use the correct key in both the name and weight dictionaries. See item 2.
  4. Write Idiomatic Code: Scala is both object-oriented and functional. In this case, orient yourself around the Item.
  5. Avoid Overhead: Get all the item's properties with a single lookup.

Here's a different take in which Item plays the starring role:

package object items {
  val weapons = Weapon("halbred", 25) :: Weapon("pike", 35) :: Nil
  val armor = Armor("plate", 65) :: Armor("chainmail", 40) :: Nil
  val dictionary = (weapons ::: armor) map(item => item.name -> item) toMap
}

Notice that the names and weights are packaged in the instances instead of packaged as values in a map. And there are no indexes to manage. However, if you needed an index for some reason you could easily use the item's position in the list of equipment or add a property to the Item trait.

And, you might use it like this:

> items dictionary("halbred")        // Weapon(halbred, 25)
> items dictionary("plate") weight   // 65

One final note, the final keyword in the Scala word is kind of anachronistic. When you define a val in Scala, it's already immutable: Why are `private val` and `private final val` different?

2
votes

som-snytt is right - just make the Armor and Weapon objects extend a trait, say, Dictionary which has NameDict and WeightDict members, then have DictionaryItem(typ: Dictionary).

It seems like a pretty convoluted way of doing things though, and it's easy to make mistakes and hard to refactor if you make all your properties lookups by number. The more natural way would be to put the actual objects in the map, something like:

case class Weapon(name: String, weight: Int, equip: ...) extends Item
case class Armor(name: String, weight: Int, equip: ...) extends Item

val ShortSword = Weapon(name = "short sword",
                        weight = 15,
                        equip = ...)

val LongSword = Weapon("long sword", 20, ...)

val Weapons = Map(
  1 -> ShortSword,
  2 -> LongSword
)

so you'd write for example Weapons(1).name rather than Weapon.NameDict(1). Or, you could just write ShortSword.name. Or, if you only need to refer to them via their index, you could define the items directly in the Map.

1
votes

I think you want object Weapon extends Dictionary. I once called it an Armory, to mean a factory for arms. But any -ory or -ary will do. I would also change the integral typ to an Enumeration. Then define Weapon.apply(kind: Kind) to return a Sword when you Weapon(Kinds.Sword).

This pattern of factory methods on traits to be implemented by companion objects is used by the collections framework, which is educational.