chrsan
chrsan

Reputation: 3400

Folding on case classes

I have a situation where I have a couple of case classes where all of their variables are optional.

Let's say I have:

case class Size(width: Option[Int], height: Option[Int])
case class Foo(a: Option[String], b: Option[Boolean], c: Option[Char])

Given a collection of the same type of case class I would like to fold over them comparing the option values and keep the values which are defined. I.e. for Size:

values.foldLeft(x) { (a, b) =>
  Size(a.width.orElse(b.width), a.height.orElse(b.height))
}

I would like to do this in a more general way for any of the case classes like the ones above. I'm thinking about doing something with unapply(_).get etc. Does anyone know a smart way to solve this?

Upvotes: 3

Views: 1313

Answers (3)

Landei
Landei

Reputation: 54584

[Code updated]

Here is an solution which requires only one abstract class per "arity":

abstract class Foldable2[A,B](val a:Option[A], val b:Option[B]) {
  def orElse[F <: Foldable2[A,B]](that: F)(implicit ev: this.type <:< F) = 
    getClass.getConstructor(classOf[Option[A]], classOf[Option[B]]).newInstance(
      this.a.orElse(that.a), this.b.orElse(that.b)
    )
}

case class Size(w: Option[Int], h: Option[Int]) extends Foldable2(w, h) 

println(Size(Some(1),None).orElse(Size(Some(2),Some(42))))
//--> Size(Some(1),Some(42))

Note that the implicit <:< argument will give a compile time error when other case classes with the same constructor arguments are passed to the method.

However, a "well formed" constructor is required, else the reflection code will blow up.

Upvotes: 2

Daniel C. Sobral
Daniel C. Sobral

Reputation: 297195

Ok, consider this:

def foldCase[C,T1](unapply: C => Option[Option[T1]], apply: Option[T1] => C)
  (coll: Seq[C]): C = {
  coll.tail.foldLeft(coll.head) { case (current, next) =>
    apply(unapply(current).get orElse unapply(next).get)
  }
}

case class Person(name: Option[String])

foldCase(Person.unapply, Person.apply)(List(Person(None), Person(Some("Joe")), Person(Some("Mary"))))

One could overload foldCase to accept two, three, or more parameters, one version of f for each arity. It could then be used with any case class. Since there's the tuple-thing to worry about, below's one way to make it work with case classes or two parameters. Expanding it to more parameters is then trivial, though a bit tiresome.

def foldCase[C,T1,T2](unapply: C => Option[(Option[T1], Option[T2])], apply: (Option[T1], Option[T2]) => C)
  (coll: Seq[C]): C = {
  def thisOrElse(current: (Option[T1], Option[T2]), next: (Option[T1], Option[T2])) =
    apply(current._1 orElse next._1, current._2 orElse next._2)
  coll.tail.foldLeft(coll.head) { case (current, next) =>
    thisOrElse(unapply(current).get, unapply(next).get)
  }
}

val list = Person(None, None) :: Person(Some("Joe"), None) :: Person(None, Some(20)) :: Person(Some("Mary"), Some(25)) :: Nil

def foldPerson = foldCase(Person.unapply, Person.apply) _

foldPerson(list)

To use it overloaded, just put all definitions inside one object:

object Folder {
  def foldCase[C,T1](unapply: C => Option[Option[T1]], apply: Option[T1] => C)
    (coll: Seq[C]): C = {
    coll.tail.foldLeft(coll.head) { case (current, next) =>
      apply(unapply(current).get orElse unapply(next).get)
    }
  }

  def foldCase[C,T1,T2](unapply: C => Option[(Option[T1], Option[T2])], apply: (Option[T1], Option[T2]) => C)
    (coll: Seq[C]): C = {
    def thisOrElse(current: (Option[T1], Option[T2]), next: (Option[T1], Option[T2])) =
      apply(current._1 orElse next._1, current._2 orElse next._2)
    coll.tail.foldLeft(coll.head) { case (current, next) =>
      thisOrElse(unapply(current).get, unapply(next).get)
    }
  }
}

When you do this, however, you'll have to explicitly turn apply and unapply into functions:

case class Question(answer: Option[Boolean])
val list2 = List(Question(None), Question(Some(true)), Question(Some(false)))
Folder.foldCase(Question.unapply _, Question.apply _)(list2)

It might be possible to turn it into a structural type, so that you only need to pass the companion object, but I couldn't do it. On #scala, I was told the answer is a definitive no, at least to how I approached the problem.

Upvotes: 2

Alex Cruise
Alex Cruise

Reputation: 7979

You can use productElement or productIterator (on scala.Product) to generically retrieve/iterate the elements of case classes (and tuples), but they're typed as Any, so there will be some pain.

Upvotes: 1

Related Questions