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

3

5

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.

Tc answered 12/8, 2020 at 16:3 Comment(3)
Sadly no an easy one. I know that is possible using Shapeless, but it is quite complex. The other solution is the magnet pattern.Geotaxis
Related: How can I make a function for unspecified number of arguments function In Scala?Butterflies
Indeed not easy it seems, though the answers show it is possible using shapeless. Ultimately I chose for calling .show on every argument before passing it, as it was least intrusive.Tc
I
6

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.

Individuality answered 12/8, 2020 at 18:22 Comment(0)
B
5

Here's one way to do it in Dotty (note that most of the Dotty-specific features used here are not necessary; they're just to make life easier, but being able to abstract over tuples of different arities is something you can't do (easily) in Scala 2):

opaque type Show[T] = T => String
opaque type ShowTuple[T <: Tuple] = T => String
object ShowTuple {
  given ShowTuple[EmptyTuple] = _ => ""
  given showTuple[H, T <: Tuple](using show: Show[H], showTail: ShowTuple[T]) as ShowTuple[H *: T] = 
    { case h *: t => show(h) + "," + showTail(t) }
}

def multiToString[T <: Tuple](t: T)(using showTuple: ShowTuple[T]) =
  showTuple(t)

It can be used like this:

class TypeA(val i: Int)
class TypeB(val s: String)
class TypeC(val b: Boolean)

given Show[TypeA] = t => s"TypeA(${t.i})"
given Show[TypeB] = t => s"TypeB(${t.s})"
given Show[TypeC] = t => s"TypeC(${t.b})"

println(multiToString((new TypeA(10), new TypeB("foo"), new TypeC(true))))

Using a type for which an implicit is not given fails:

class TypeD

multiToString((new TypeA(10), new TypeB("foo"), new TypeC(true), new TypeD))

Try it in Scastie

Butterflies answered 12/8, 2020 at 18:44 Comment(0)
M
3

What is the type of paths?

If it's List[T] then there should be an implicit Show[T] in scope.

If it's List[Any] then there should be an implicit Show[Any] in scope.

If paths contains elements of different types and paths is not a List[Any] then paths shouldn't be a List[...] at all. It can be of type L <: HList. You can try

import shapeless.{HList, HNil, Poly1, Poly2}
import shapeless.ops.hlist.{LeftReducer, Mapper}

trait Show[T] {
  def show(t: T): String
}

implicit class ShowOps[T](t: T) {
  def show(implicit s: Show[T]): String = s.show(t)
}

object show extends Poly1 {
  implicit def cse[T: Show]: Case.Aux[T, String] = at(_.show)
}

object concat extends Poly2 {
  implicit def cse: Case.Aux[String, String, String] = at(_ + _)
}

def toString[L <: HList, L1 <: HList](xs: L)(implicit
  mapper: Mapper.Aux[show.type, L, L1],
  reducer: LeftReducer.Aux[L1, concat.type, String]
): String = xs.map(show).reduceLeft(concat)

type MyTypeA
type MyTypeB

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

val a1: MyTypeA = ???
val b1: MyTypeB = ???

toString(a1 :: b1 :: HNil)
Multitudinous answered 13/8, 2020 at 1:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.