7
votes

I would like to have the automatic companion class apply constructors of a case class to perform implicit conversions for me, but cannot figure out how to do so. I've searched all over and the closest answer I could find was for this question (I'll explain why it isn't what I'm looking for below).

I have a case class that looks something like this:

case class Container(a: Long, b: Long, c: Long)

I'm using the container to count instances where certain conditions apply, so I'd like to be able to have the constructor automatically convert boolean parameters to longs (if (boolean) 1L else 0L).

The real case class, of course, has many parameters, so it would be tedious and terribly repetitive to make my own companion object and overload apply to accept Boolean parameters. Additionally, something like the code below isn't ideal (if it were implemented correctly somehow) as it only accepts boolean arguments:

object Container {
  def apply(args: Boolean*) = {
    // doesn't REALLY work since number of arguments not enforced
    Container(args map { if (_) 1L else 0L } toArray: _*)
  }
}
val c1 = Container(1, 0, 1) // works
val c2 = Container(true, false, true) // might be workable if done correctly
val c3 = Container(true, 0, 1) // won't work

I tried adding an implicit conversion in the companion object (below), hoping that it would automatically be used in Container.apply, but it appears that this does not actually put the implicit conversion into the namespace of the code that calls apply.

object Container {
  implicit def booleanToLong(x: Boolean): Long = if (x) 1L else 0L
}

I'm able to get things working using this hackish workaround:

{
  import Container.booleanToLong
  // all of these now work
  val c1 = Container(1, 0, 1)
  val c2 = Container(true, false, true)
  val c3 = Container(true, 0, 1) // works!!!
}

The biggest problem is that I have to import booleanToLong into the code that wants to create a Container and thus must put it in its own block for safety (booleanToLong is generally undesirable).

Finally, the solution of using an implicit parameter that itself includes an implicit conversion doesn't work because it would require an explicit overriding of apply, defeating the goal of not repeating a long parameter list and marshaling types.

Is there a way to do this such that I get implicit conversions for free every time I make a Container, but not otherwise? Or is this impossible due to some sort of technical constraint?

1

1 Answers

6
votes

You can use a kind of variant of the magnet pattern to make this a little safer. First for a type class:

trait ToLong[A] {
  def apply(a: A): Long
}

implicit object longToLong extends ToLong[Long] {
  def apply(l: Long) = l
}

implicit object booleanToLong extends ToLong[Boolean] {
  def apply(b: Boolean) = if (b) 1L else 0L
}

And now we just need one extra constructor:

case class Container(a: Long, b: Long, c: Long)

object Container {
  def apply[A: ToLong, B: ToLong, C: ToLong](a: A, b: B, c: C) = new Container(
    implicitly[ToLong[A]].apply(a),
    implicitly[ToLong[B]].apply(b),
    implicitly[ToLong[C]].apply(c)
  )
}

And we can write the following:

val c1 = Container(1, 0, 1)
val c2 = Container(true, false, true)
val c3 = Container(true, 0L, 1L)

Without having to introduce the rather scary general conversion from Boolean to Long.