How to make a typeclass works with an heterogenous List in scala
Asked Answered
A

1

2

Given the following typeclass and some instances for common types

trait Encoder[A] {
  def encode(a: A): String
}

object Encoder {
  implicit val stringEncoder = new Encoder[String] {
    override def encode(a: String): String = a
  }
  implicit val intEncoder = new Encoder[Int] {
    override def encode(a: Int): String = String.valueOf(a)
  }
  implicit def listEncoder[A: Encoder] =
    new Encoder[List[A]] {
      override def encode(a: List[A]): String = {
        val encoder = implicitly[Encoder[A]]
        a.map(encoder.encode).mkString(",")
      }
    }
}

Is there a way to make it work

Encoder.listEncoder.encode(List("a", 1))
Arrestment answered 22/9, 2020 at 13:43 Comment(5)
Where did you get this list from? Does it have to be heterogenous ?Huey
Short answer, no you can't. At least not the way you want. You may use a real heterogeneous list, create an ADT of all possible values or use the magnet pattern.Seashore
@LuisMiguelMejíaSuárez I guess for a magnet an additional implicit (Any) will have to be defined as well as for a type class.Viscosity
@DmytroMitin not sure what your idea is, mine is something like object MyList { def apply(data: Magnet*): List[Magnet] = data.toList }Seashore
@LuisMiguelMejíaSuárez I see, you're right, thanks.Viscosity
V
2

If you define an instance for Any

object Encoder {
  implicit val anyEncoder = new Encoder[Any] {
    override def encode(a: Any): String = a.toString
  }

  //instances for String, Int, List[A]
}

then

Encoder.listEncoder[Any].encode(List("a", 1))

will work.

You have to specify type parameter here explicitly (listEncoder[Any]) because that's how type inference work in scala. Indeed, after the type of argument of encode is inferred

Encoder.listEncoder[???].encode(List("a", 1))
//                              ^^^^^^^^^^^^
//                              List[Any]

it's too late to come back to infer the type parameter of listEncoder.

If you define a delegator function

def encode[A](a: A)(implicit encoder: Encoder[A]): String = encoder.encode(a)

or syntax

implicit class EncoderOps[A](a: A)(implicit encoder: Encoder[A]) {
  def encode: String = encoder.encode(a)
}

    // or
// implicit class EncoderOps[A](a: A) {
//   def encode(implicit encoder: Encoder[A]): String = encoder.encode(a)
// }

then you don't have to specify type parameter explicitly

encode("a")
encode(1)
encode(List("a", 1))

"a".encode
1.encode
List("a", 1).encode

By the way, List("a", 1) is not a heterogenous list, it's an ordinary homogenous list with element type Any. A heterogenous list would be

val l: String :: Int :: HNil = "a" :: 1 :: HNil

You can try a magnet instead of type class

trait Magnet {
  def encode(): String
}

object Magnet {
  implicit def fromInt(a: Int): Magnet = new Magnet {
    override def encode(): String = String.valueOf(a)
  }
  implicit def fromString(a: String): Magnet = new Magnet {
    override def encode(): String = a
  }
}

def encode(m: Magnet): String = m.encode()

encode("a")
encode(1)
List[Magnet]("a", 1).map(_.encode())

or HList instead of List[A]

sealed trait HList 
case class ::[+H, +T <: HList](head: H, tail: T) extends HList 
case object HNil extends HList 
type HNil = HNil.type

implicit class HListOps[L <: HList](l: L) {
  def ::[A](a: A): A :: L = new ::(a, l)
}

trait Encoder[A] {
  def encode(a: A): String
}

object Encoder {
  implicit val stringEncoder: Encoder[String] = new Encoder[String] {
    override def encode(a: String): String = a
  }
  implicit val intEncoder: Encoder[Int] = new Encoder[Int] {
    override def encode(a: Int): String = String.valueOf(a)
  }
  implicit val hnilEncoder: Encoder[HNil] = new Encoder[HNil] {
    override def encode(a: HNil): String = ""
  }
  implicit def hconsEncoder[H, T <: HList](implicit
                                           hEncoder: Encoder[H],
                                           tEncoder: Encoder[T]
                                          ): Encoder[H :: T] = new Encoder[H :: T] {
    override def encode(a: H :: T): String = 
      s"${hEncoder.encode(a.head)},${tEncoder.encode(a.tail)}"
  }
}

def encode[A](a: A)(implicit encoder: Encoder[A]): String = encoder.encode(a)

implicit class EncoderOps[A](a: A)(implicit encoder: Encoder[A]) {
  def encode: String = encoder.encode(a)
}

val l: String :: Int :: HNil = "a" :: 1 :: HNil
encode(l)
l.encode
Viscosity answered 22/9, 2020 at 14:9 Comment(4)
Thanks @Dmytro. The issue is that it won't use the implicit encoder defined in the scope.Arrestment
@YannMoisan Why?Viscosity
for a List("", 42), it will use the Encoder[Any] instead of Encoder[Int] to encode the Int.Arrestment
@YannMoisan Surely (now I can see what you meant). Because all elements of List("", 42) are Any. Luis is right, you should use either HList instead of List[A] or a magnet instead of type class.Viscosity

© 2022 - 2024 — McMap. All rights reserved.