Haque
Haque

Reputation: 11

Scala : How to pass a class field into a method

I'm new to Scala and attempting to do some data analysis.

I have a CSV files with a few headers - lets say item no., item type, month, items sold.

I have made an Item class with the fields of the headers. I split the CSV into a list with each iteration of the list being a row of the CSV file being represented by the Item class.

I am attempting to make a method that will create maps based off of the parameter I send in. For example if I want to group the items sold by month, or by item type. However I am struggling to send the Item.field into a method.

F.e what I am attempting is something like:

makemaps(Item.month);
makemaps(Item.itemtype);

def makemaps(Item.field):
   if (item.field==Item.month){}
   else (if item.field==Item.itemType){}

However my logic for this appears to be wrong. Any ideas?

Upvotes: 1

Views: 60

Answers (1)

Levi Ramsey
Levi Ramsey

Reputation: 20561

def makeMap[T](items: Iterable[Item])(extractKey: Item => T): Map[T, Iterable[Item]] =
  items.groupBy(extractKey)

So given this example Item class:

case class Item(month: String, itemType: String, quantity: Int, description: String)

You could have (I believe the type ascriptions are mandatory):

val byMonth = makeMap[String](items)(_.month)
val byType = makeMap[String](items)(_.itemType)
val byQuantity = makeMap[Int](items)(_.quantity)
val byDescription = makeMap[String](items)(_.description)

Note that _.month, for instance, creates a function taking an Item which results in the String contained in the month field (simplifying a little).

You could, if so inclined, save the functions used for extracting keys in the companion object:

object Item {
  val month: Item => String = _.month
  val itemType: Item => String = _.itemType
  val quantity: Item => Int = _.quantity
  val description: Item => String = _.description

  // Allows us to determine if using a predefined extractor or using an ad hoc one
  val extractors: Set[Item => Any] = Set(month, itemType, quantity, description)
}

Then you can pass those around like so:

val byMonth = makeMap[String](items)(Item.month)

The only real change semantically is that you explicitly avoid possible extra construction of lambdas at runtime, at the cost of having the lambdas stick around in memory the whole time. A fringe benefit is that you might be able to cache the maps by extractor if you're sure that the source Items never change: for lambdas, equality is reference equality. This might be particularly useful if you have some class representing the collection of Items as opposed to just using a standard collection, like so:

object Items {
  def makeMap[T](items: Iterable[Item])(extractKey: Item => T): Map[T, 
Iterable[Item]] =
    items.groupBy(extractKey)
}

class Items(val underlying: immutable.Seq[Item]) {
  def makeMap[T](extractKey: Item => T): Map[T, Iterable[Item]] =
    if (Item.extractors.contains(extractKey)) {
      if (extractKey == Item.month) groupedByMonth.asInstanceOf[Map[T, Iterable[Item]]]
      else if (extractKey == Item.itemType) groupedByItemType.asInstanceOf[Map[T, Iterable[Item]]]
      else if (extractKey == Item.quantity) groupedByQuantity.asInstanceOf[Map[T, Iterable[Item]]]
      else if (extractKey == Item.description) groupedByDescription.asInstanceOf[Map[T, Iterable[Item]]]
      else throw new AssertionError("Shouldn't happen!")
    } else {
      Items.makeMap(underlying)(extractKey)
    }

  lazy val groupedByMonth = Items.makeMap[String](underlying)(Item.month)
  lazy val groupedByItemType = Items.makeMap[String](underlying)(Item.itemType)
  lazy val groupedByQuantity = Items.makeMap[Int](underlying)(Item.quantity)
  lazy val groupedByDescription = Items.makeMap[String](underlying)(Item.description)
}

(that is almost certainly a personal record for asInstanceOfs in a small block of code... I'm not sure if I should be proud or ashamed of this snippet)

Upvotes: 2

Related Questions