Search code examples
scalapolymorphismtypeclassimplicitscala-cats

Achieving Ad hoc polymorphism at function parameter level (mixing parameters of different type)


When I have a function in Scala:

def toString[T: Show](xs: T*): String = paths.map(_.show).mkString

And the following type class instances in scope:

implicit val showA: Show[MyTypeA]
implicit val showB: Show[MyTypeB]

I can use function toString in the following ways:

val a1: MyTypeA
val a2: MyTypeA
val stringA = toString(a1, a2)

val b1: MyTypeB
val b2: MyTypeB
val stringB = toString(b1, b2)

But I cannot call toString mixing parameters of type MyTypeA and MyTypeB:

// doesn't compile, T is inferred to be of type Any
toString(a1, b1)

Is it possible to redefine toString in such a way that it becomes possible to mix parameters of different types (but only for which a Show typeclass is available)?

Note that I am aware of the cats show interpolator which solves this specific example, but I'm looking for a solution which can be applied to different cases as well (e.g. toNumber).

I am also aware of circumventing the problem by calling .show on the parameters before passing them to the toString function, but I'm looking for a way to avoid this as it results in code duplication.


Solution

  • Example with shapeless:

    object myToString extends ProductArgs { //ProductArgs allows changing variable number of arguments to HList
    
        //polymorphic function to iterate over values of HList and change to a string using Show instances
        object showMapper extends Poly1 {
    
          implicit def caseShow[V](implicit show: Show[V]): Case.Aux[V, String] = {
            at[V](v => show.show(v))
          }
    
        }
    
        def applyProduct[ARepr <: HList](
            l: ARepr
        )(
            implicit mapper: Mapper[showMapper.type, ARepr]
        ): String = l.map(showMapper).mkString("", "", "")
    }
    

    Now let's test it:

    case class Test1(value: String)
    case class Test2(value: String)
    case class Test3(value: String)
    
    implicit val show1: Show[Test1] = Show.show(_.value)
    implicit val show2: Show[Test2] = Show.show(_.value)
    
    println(myToString(Test1("a"), Test2("b"))) //"ab"
    
    println(myToString(Test1("a"), Test2("b"), Test3("c"))) //won't compile since there's no instance of Show for Test3
    
    

    By the way, I think toString is not the best name, because probably it can cause weird conflicts with toString from java.lang.Object.


    If you don't want to mess with shapeless, another solution that comes to my mind is to just create functions with different arity:

    def toString[A: Show](a: A): String = ???
    def toString[A: Show, B: Show](a: A, b: B): String = ???
    //etc
    

    It's definitely cumbersome, but it might be the easiest way to solve your problem.