How can I guarantee the existence of a method in a companion object and reference it?

102 views Asked by At

Consider this example, where Listable is intended to mixed into the companion object of a case class. Therefore, in order to call Writer.grid, one must have a companion object A that extends Listable[A], with an implicit Writer[A] defined within. (Say for example, to convert a list of an arbitrary Listable to a CSV format.)

trait Listable[A] {
    def list: List[A]
}

object Writer {
    def grid[A <: Listable[A]](listable: A)(implicit w: Writer[A]): String = {  
        listable.list.map(w.write(_).mkString(",")).mkString("\n")
    }
}

trait Writer[A] {
    def write(a: A): List[String]
}

And here's a naive implementation:

case class Test(id: Int, text: String)

object Test extends Listable[Test] {
    def list = List(Test(1, "test"))

    implicit val wrt = new Writer[Test] {
        def write(t: Test) = List(t.id.toString, t.text)
    }
}

This compiles, but cannot work because listable: A really refers to the object Test, and A in w: Writer[A] refers to the case class Test, so calling Writer.grid(Test) fails to conform to the type bounds.

I can work around this problem somewhat by ditching Listable and requiring an implicit List[A] in the signature of grid:

def grid[A](implicit w: Writer[A], list: List[A]): String = ...

But I'd prefer to:

  1. Not require such an implicit function that could produce unexpected results.
  2. Not use a special type to wrap list, as it will also be used elsewhere.
  3. Keep the definition of the grid method outside of Listable.

Is it possible to re-work the signature of Writer.grid to make this work? (or other structural changes)

2

There are 2 answers

3
Shiva Wu On BEST ANSWER

Basically Engene pointed out a working solution, I just want to say that, what's really weird about your situation is that, you don't need the listable instance in the grid function. This fact is not that obvious from your original code, but it's pretty on the table if you see from Eugene's code, the argument is not used at all.

So I'm guessing what you're trying to do is to have the companion object to have an instance of special instances for special purposes, maybe I'm wrong. But the name Listable[A] is really confusing, if you're trying to describe some trait of the companion object, you can name sth like ListableMeta[A], which makes more sense.

trait ListableMeta[A] {
    def list: List[A]
}

object Writer {
    def grid[A : ListableMeta](implicit w: Writer[A]): String = {
        implicitly[ListableMeta[A]].list.map(w.write(_).mkString(",")).mkString("\n")
    }
}

trait Writer[A] {
    def write(a: A): List[String]
}

case class Test(id: Int, text: String)

trait InImplicit {
    implicit val injectThisToImplicits: this.type = this
}

object Test extends ListableMeta[Test] with Writer[Test] with InImplicit {
    def list = List(Test(1, "test"))

    def write(t: Test) = List(t.id.toString, t.text)
}

What you need to do is basically Writer.grid[Test] instead of passing a concrete instance, so this should work. But the clear point here is to use typeclasses on all things, mix and match typeclasses and type bounds can cause confusion.

0
Eugene Zhulenev On

You want something strange. Case class Test has type Test obviously, but companion object Test doesn't have any relationship to type 'Test', it has it's one type 'Test.type'.

I suggest you to make 2 proper type classes: Listable and Writer

  trait Listable[A] {
    def list: List[A]
  }

  object Writer {
    def grid[A : Listable : Writer](listable: A): String = {
      implicitly[Listable[A]].list.map(implicitly[Writer[A]].write(_).mkString(",")).mkString("\n")
    }
  }

  trait Writer[A] {
    def write(a: A): List[String]
  }

  case class Test(id: Int, text: String)

  object Test {
    implicit val listable: Listable[Test] = new Listable[Test] {
       def list: List[Test] = List(Test(1, "test"))
    }

    implicit val wrt: Writer[Test] = new Writer[Test] {
      def write(t: Test) = List(t.id.toString, t.text)
    }
  }

  Writer.grid(Test(123, "abc"))