0

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)

Michael Zajac
  • 55,144
  • 7
  • 113
  • 138

2 Answers2

1

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"))
Eugene Zhulenev
  • 9,714
  • 2
  • 30
  • 40
1

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.

Shiva Wu
  • 1,074
  • 1
  • 8
  • 20
  • You're correct in that I don't need a `listable` instance for the `grid` method at all. It's meant to rely on the companion object's `list` method in order to fetch a list (and not in static way). The `listable: A` parameter was more or less a way of trying to grab a reference to the companion object (which clearly doesn't work). This is an interesting variation, but it doesn't work outside the REPL, because `Test` is a top-level object and cannot be `implicit`. – Michael Zajac Nov 13 '14 at 03:18
  • I updated the code so that it will work in the repl. As long as you get the idea, I think the rest is just impl details. – Shiva Wu Nov 13 '14 at 18:32
  • That's a nice trick. It feels a little hacky, but also allows me to define the API the way I want. – Michael Zajac Nov 15 '14 at 03:25