Boilerplate: Automating packing and unpacking values


#1

Hi,

I am looking for a way to pack and unpack values of different types to and from a
Val class in a Map[String,Val]. Val here is basically a value type. I first tried this with
implicits and type parameters and could not get it to work. I then got
it working with type members.

So this is what I have:

First I have the Val and the implicits that pack the value.

  import scala.language.implicitConversions

  sealed trait Val {
    type V
    val v: V
  }

  case class ErrVal(override val v:String) extends Val {
    type V = String
  }

  implicit def strVal(a:String): Val = new Val {
    override type V = String
    override val v: V = a
  }

  implicit def intVal(a:Int): Val = new Val {
    override type V = Int
    override val v: V = a
  }

I also have the implicits that unpack the value:


  def errMsg(expected:String, a:Val) = Left(s"Expected a $expected but got ${a.v.getClass}")

  implicit def valStr(a:Val): Either[String,String] = a.v match {
    case e:String => Right(e)
    case _ => errMsg("String", a)
  }


  implicit def valInt(a:Val): Either[String,Int] = a.v match {
    case e:Int => Right(e)
    case _ => errMsg("Int", a)
  }

The following class Row is used to process the value. It simply allows one to apply
a function and let the implicits unpack and pack the values as required
(apply simply applies the function f to a number of elements):

  case class Row(r:Map[String,Val]) {
    def apply[A,B](f: A => B, cols:String*)(implicit a: Val => Either[String,A], b: B => Val): Row = {
      val cls = cols.toSet
      val t = r.filterKeys( cls.contains ).map { case (k,v) =>
        // Get the row element
        val va = a(v)
        val e = va.fold( { msg =>
          // If it is not the correct type, error
          ErrVal(msg)
        }, { oa =>
          // If it is the correct type, use it
          val vb = f(oa)
          val avb = b(vb)
          avb
        })
        (k, e)
      }
      // Replace what we changed
      Row(r ++ t)
    }
  }

And I can use it so:

  def main(args: Array[String]): Unit = {

    val data0 = Frame(
      "x" -> Seq("1","1","1","0").toIterator,
      "y" -> Seq("2","2","2","0").toIterator)
    //println(data0)

    val data1 = Frame(
      "x" -> Seq(1,1,1,0),
      "y" -> Seq(2,2,2,0))
    //println(data1)

    val r1: Row = data0.iterable.head
    println(r1)
    val r2 = r1( (x:String) => x.toInt, "x", "y" )
    println(r2)

    val r3: Row = data1.iterable.head
    println(r3)
    val r4 = r3( (x:String) => x.toInt, "x", "y" )
    println(r4)
 }

which produces something like:

Row(Map(x -> AutoTask$$anon$3@617c74e5, y -> AutoTask$$anon$3@34b7bfc0))
Row(Map(x -> AutoTask$$anon$4@7c0e2abd, y -> AutoTask$$anon$4@48eff760))
Row(Map(x -> AutoTask$$anon$4@402f32ff, y -> AutoTask$$anon$4@573f2bb1))
Row(Map(x -> ErrVal(Expected a String but got class java.lang.Integer), y -> ErrVal(Expected a String but got class java.lang.Integer)))

Ok, so this is all well and good but I or anyone else that wants to use Row with new
types must always define the implicit conversions for those types . But this is simple
repetitive boilerplate. The question is how can I automate this? What is the simplest
most maintenance friendly way of doing this?

TIA,

Code follows in case anyone wants to experiment with it.

import scala.collection.AbstractIterator

object AutoTask {

  import scala.language.implicitConversions

  sealed trait Val {
    type V
    val v: V
  }

  case class ErrVal(override val v:String) extends Val {
    type V = String
  }

  implicit def strVal(a:String): Val = new Val {
    override type V = String
    override val v: V = a
  }

  implicit def intVal(a:Int): Val = new Val {
    override type V = Int
    override val v: V = a
  }


  // https://medium.com/@sinisalouc/overcoming-type-erasure-in-scala-8f2422070d20
  //  non-variable type argument String in type pattern AutoTask.Val[String] is unchecked since it is eliminated by erasure
  /*
  implicit def valStr[T](a:Val[T])(implicit tag: TypeTag[T]):String =
    tag.tpe match {
      case TypeRef(utype, usymbol, args) =>
        List(utype, usymbol, args).mkString("\n")
    }
  */

  def errMsg(expected:String, a:Val) = Left(s"Expected a $expected but got ${a.v.getClass}")

  implicit def valStr(a:Val): Either[String,String] = a.v match {
    case e:String => Right(e)
    case _ => errMsg("String", a)
  }


  implicit def valInt(a:Val): Either[String,Int] = a.v match {
    case e:Int => Right(e)
    case _ => errMsg("Int", a)
  }


  case class Row(r:Map[String,Val]) {
    def apply[A,B](f: A => B, cols:String*)(implicit a: Val => Either[String,A], b: B => Val): Row = {
      val cls = cols.toSet
      val t = r.filterKeys( cls.contains ).map { case (k,v) =>
        // Get the row element
        val va = a(v)
        val e = va.fold( { msg =>
          // If it is not the correct type, error
          ErrVal(msg)
        }, { oa =>
          // If it is the correct type, use it
          val vb = f(oa)
          val avb = b(vb)
          avb
        })
        (k, e)
      }
      // Replace what we changed
      Row(r ++ t)
    }

  }

  case class Frame(src: Iterable[Row]) {

    def iterable: Iterable[Row] = {
      src.toIterator.map { row => row /* TODO */ }
      }.toIterable
  }

  object Frame {

    def apply[T](data: (String, Iterator[T])*)(implicit f: T => Val): Frame = {
      // Create iterator on data
      val src = new Iterable[Row] {
        override def iterator: Iterator[Row] = new AbstractIterator[Row] {

          override def hasNext: Boolean = data.forall( p => p._2.hasNext)

          override def next(): Row = {
            val cols = data.map { case (k,v) =>
              val vv = f( v.next() )
              (k,vv)
            }
            Row(cols.toMap)
          }
        }
      }
      new Frame(src)
    }

    // https://stackoverflow.com/questions/3307427/scala-double-definition-2-methods-have-the-same-type-erasure
    def apply[T](data: (String, Seq[T])*)(implicit f: T => Val, d: DummyImplicit): Frame = {
      val ndata = data.map{ case (k,v) => (k,v.toIterator) }
      apply(ndata:_*)(f)
    }
  }

  
  def main(args: Array[String]): Unit = {

    val data0 = Frame(
      "x" -> Seq("1","1","1","0").toIterator,
      "y" -> Seq("2","2","2","0").toIterator)
    //println(data0)

    val data1 = Frame(
      "x" -> Seq(1,1,1,0),
      "y" -> Seq(2,2,2,0))
    //println(data1)

    val r1: Row = data0.iterable.head
    println(r1)
    val r2 = r1( (x:String) => x.toInt, "x", "y" )
    println(r2)

    val r3: Row = data1.iterable.head
    println(r3)
    val r4 = r3( (x:String) => x.toInt, "x", "y" )
    println(r4)
  }


}