0

I followed this article to create union types. The articles has few answers about the Primitive type but my scenario is an extension to it.

So, I am trying to define a method which takes Map[String, A] where A is the set of allowed type.

This is my class of union types:

sealed trait SupportedType[A]

object SupportedType {
  implicit val byteBufferColumn : SupportedType[ByteBuffer] = new SupportedType[ByteBuffer] {}
  implicit val longColumn : SupportedType[java.lang.Long] = new SupportedType[java.lang.Long] {}
  implicit val byteArrayColumn : SupportedType[Array[Byte]] = new SupportedType[Array[Byte]] {}
  implicit val stringColumn : SupportedType[String] = new SupportedType[String] {}
}

This is my method I defined:

def upsert[A: SupportedType](key: T, values: Map[String, A], timestamp: Long, ttl: Duration): Future[Unit]

This is how I am calling the method:

dataStore.upsert(
      cacheKey,
      Map(
        itColumn      -> ByteBuffer.wrap(Utils.compress(iti.toByteArray)),
        cacheWriteTimeColumn -> writeTime.toEpochMilli
      ),
      writeTime.toEpochMilli,
      ttl
    )

error: No implicit arguments of type: SupportedType[Any]

My guess is writeTime.toEpochMilli returns java.long type and as you can see in SupportedType, I tried to define java.lang.Long but thats not working.

Any help would be appreciated.

Vipul
  • 58
  • 1
  • 2
  • 8
  • The **Map** doesn't know that all elements inside it have a proper instance of the **typeclass**. Your options are, to use a **magnet** pattern, or zip each value with its instance of the **typeclass** in another class. – Luis Miguel Mejía Suárez May 20 '21 at 14:29
  • Can you give an example? – Vipul May 20 '21 at 15:51
  • May I ask, what will you do with those values inside the **Map** later? Maybe all you want to do is call a single method like `serialize` that will turn the values into a byte buffer or something like that? – Luis Miguel Mejía Suárez May 20 '21 at 16:45
  • This map holds the column-name, column-value which will store in the database and yes I thought of the same thing which you ar saying but with the current design approach in the team I am storing every column separately – Vipul May 20 '21 at 16:51
  • 1
    Does something like [this](https://scastie.scala-lang.org/BalmungSan/hwveyk4MRCufJuAyLCNQjA/10) would work for your use case? If so, let me know to post it as an answer. – Luis Miguel Mejía Suárez May 20 '21 at 17:22
  • Can you check for ByteBuffer As well. I did try to do it: https://scastie.scala-lang.org/DCJPuRdwQaGPHCeRMavVkA – Vipul May 21 '21 at 04:06
  • Well, `15` is not a printable character, other than that it seems to work ok. Try with a printable character like `65` – Luis Miguel Mejía Suárez May 21 '21 at 13:58
  • Sorry for late comment... seems like it is working.. thanks for the help... Though you can post this an answer but really wanted to know how it worked...(serialisation part) – Vipul May 24 '21 at 03:35
  • Sorry, not sure what you mean with _"how it worked...(serialisation part)"_, I may try to explain what is happening but it is difficult without knowing what exactly you do not understand. – Luis Miguel Mejía Suárez May 25 '21 at 17:22

1 Answers1

2

You can use the magnet pattern in combination to a typeclass like this:

import scala.language.implicitConversions

trait ColumnValue {
  def serialize(): String
}

object ColumnValue {
  trait SupportedType[A] {
    def toColumn(a: A): ColumnValue
  }
  
  object SupportedType {
    implicit final val stringSupportedType: SupportedType[String] =
      new SupportedType[String] {
        override def toColumn(str: String): ColumnValue =
          new ColumnValue {
            override def serialize(): String =
              "\"" + str + "\""
          }
      }
    
    implicit final val intSupportedType: SupportedType[Int] =
      new SupportedType[Int] {
        override def toColumn(int: Int): ColumnValue =
          new ColumnValue {
            override def serialize(): String =
              int.toString
          }
      }
    
    implicit final val booleanSupportedType: SupportedType[Boolean] =
      new SupportedType[Boolean] {
        override def toColumn(bool: Boolean): ColumnValue =
          new ColumnValue {
            override def serialize(): String =
              if (bool) "1" else "0"
          }
      }
    
    implicit final def listSupportedType[A](implicit ev: SupportedType[A]): SupportedType[List[A]] =
      new SupportedType[List[A]] {
        override def toColumn(list: List[A]): ColumnValue =
          new ColumnValue {
            override def serialize(): String =
              list.map(a => ev.toColumn(a).serialize()).mkString("[", ",", "]")
          }
      }
  }
  
  def apply[A](a: A)(implicit ev: SupportedType[A]): ColumnValue =
    ev.toColumn(a)
  
  implicit def supportedType2Column[A : SupportedType](a: A): ColumnValue =
    apply(a)
}

You may create some helper functions to reduce some of the boilerplate.

Which can be used like this:

final case class Table(data: Map[String, ColumnValue]) {
  def upsert(values: Map[String, ColumnValue]): Table =
    copy(this.data ++ values)
}

object Table {
  val empty: Table =
    Table(data = Map.empty)
}

val result = Table.empty.upsert(Map(
  "a" -> "foo",
  "b" -> 10,
  "c" -> List(true, false, true)
))

See the code running here.