Alex Wilson
Alex Wilson

Reputation: 6740

Scala tuple question

New to Scala, but experienced in C++ I'm trying to implement (possibly misguidedly) a small library on top of the sqlite4java library to allow me to auto-fill tuples of abritrary type from query rows (rows whose column types are each compatible with the respective tuple element type).

In C++ I normally implement this using boost::tuples and compile-time template recursion (terminated using template specialisation as shown below). Boost tuples are implemented very similarly to Haskell HLists. The pattern would be (assuming, for simplicity that the query is returned as vector of strings):

template<typename T1, typename T2>
void populateTuple( boost::tuples::cons<T1, T2>& tupleRec, int index, const std::vector<std::string>& vals )
{
    tupleRec.head = boost::lexical_cast<T1>( vals[index] );
    populateTuple( tupleRec.tail, index+1, vals );
}

template<typename T>
void populateTuple( boost::tuples::cons<T, boost::tuples::null_type>& tupleRec, int index, const std::vector<std::string>& vals )
{
    tupleRec.head = boost::lexical_cast<T>( vals[index] );
}

(Apologies - I've not run the above through a compiler, but am hoping it shows what I mean)

I'd love to be able to do something similar with Scala. I can recurse over a general Tuple type via the Product trait - and get the type of each element at run time using pattern matching (for the small number of column types I support). However I haven't found a way to assign Tuple elements via the product trait. And to be honest, I'm not convinced that this is a particularly nice or idiomatic way to do what I require anyway.

But something like:

val returnedFromQuery = List[String]( "Hello", "4", "6.0" )
val rowAsTuples = interpretListAsTuple[(String, Int, Float)]( returnedFromQuery )

Where rowAsTuples has type (String, Int, Float). Again, please excuse any syntax errors.

Anyone have any thoughts? Or alternative suggestions? In advance - I'm not interested in any higher-level SQL query libraries. I'm happy with sqlite4java but want to wrap it with a simple more abstract interface.

Upvotes: 3

Views: 933

Answers (5)

Alex Wilson
Alex Wilson

Reputation: 6740

So I was asking this question because I wanted to write a simple wrapper around the sqlite4java sqlite interface. To enable code of the following form where the row type from a query could be specified in the prepared statement (I intend to add type checking to the parameters passed based on a similar method):

test("SQLite wrapper test")
{
    val db = new SQLiteWrapper()
    db.exec( "BEGIN" )
    db.exec( "CREATE TABLE test( number INTEGER, value FLOAT, name TEXT )" )

    val insStatement = db.prepare( "INSERT INTO test VALUES( ?, ?, ? )", HNil )
    insStatement.exec( 1, 5.0, "Hello1" )
    insStatement.exec( 2, 6.0, "Hello2" )
    insStatement.exec( 3, 7.0, "Hello3" )
    insStatement.exec( 4, 8.0, "Hello4" )

    val getStatement = db.prepare( "SELECT * from test", Col[Int]::Col[Double]::Col[String]::HNil )
    assert( getStatement.step() === true )
    assert( _1(getStatement.row) === Some(1) )
    assert( _2(getStatement.row) === Some(5.0) )
    assert( _3(getStatement.row) === Some("Hello1") )

    getStatement.reset()

    db.exec( "ROLLBACK" )
}

And to enable this, using a variety of helpful SO suggestions I've come up with the code below. This is my first attempt at any form of generic programming in Scala - I've only been playing with the language for a week or two. So this is code is unlikely to be considered nice/good by the experienced Scala community. Any suggestions/feedback welcomed....

import java.io.File
import com.almworks.sqlite4java._

object SqliteWrapper
{
    trait TypedCol[T]
    {
        var v : Option[T] = None
        def assign( res : SQLiteStatement, index : Int )
    }

    sealed trait HList
    {
        def assign( res : SQLiteStatement, index : Int )
    }

    final case class HCons[H <: TypedCol[_], T <: HList]( var head : H, tail : T ) extends HList
    {
        def ::[T <: TypedCol[_]](v : T) = HCons(v, this)
        def assign( res : SQLiteStatement, index : Int )
        {
            head.assign( res, index )
            tail.assign( res, index+1 )
        }
    }

    final class HNil extends HList
    {
        def ::[T <: TypedCol[_]](v : T) = HCons(v, this)
        def assign( res : SQLiteStatement, index : Int )
        {
        }
    }

    type ::[H <: TypedCol[_], T <: HList] = HCons[H, T]

    val HNil = new HNil()



    final class IntCol extends TypedCol[Int]
    {
        def assign( res : SQLiteStatement, index : Int ) { v = Some( res.columnInt(index) ) }
    }

    final class DoubleCol extends TypedCol[Double]
    {
        def assign( res : SQLiteStatement, index : Int ) { v = Some( res.columnDouble(index) ) }
    }

    final class StringCol extends TypedCol[String]
    {
        def assign( res : SQLiteStatement, index : Int ) { v = Some( res.columnString(index) ) }
    }

    trait TypedColMaker[T]
    {
        def build() : TypedCol[T]
    }

    object TypedColMaker
    {
        implicit object IntColMaker extends TypedColMaker[Int]
        {
            def build() : TypedCol[Int] = new IntCol()
        }
        implicit object DoubleColMaker extends TypedColMaker[Double]
        {
            def build() : TypedCol[Double] = new DoubleCol()
        }
        implicit object StringColMaker extends TypedColMaker[String]
        {
            def build() : TypedCol[String] = new StringCol()
        }
    }

    def Col[T : TypedColMaker]() = implicitly[TypedColMaker[T]].build()

    // Hideousness. Improve as Scala metaprogramming ability improves
    def _1[H <: TypedCol[_], T <: HList]( t : HCons[H, T] ) = t.head.v
    def _2[H1 <: TypedCol[_], H2 <: TypedCol[_], T <: HList]( t : HCons[H1, HCons[H2, T]] ) = t.tail.head.v
    def _3[H1 <: TypedCol[_], H2 <: TypedCol[_], H3 <: TypedCol[_], T <: HList]( t : HCons[H1, HCons[H2, HCons[H3, T]]] ) = t.tail.tail.head.v
    def _4[H1 <: TypedCol[_], H2 <: TypedCol[_], H3 <: TypedCol[_], H4 <: TypedCol[_], T <: HList]( t : HCons[H1, HCons[H2, HCons[H3, HCons[H4, T]]]] ) = t.tail.tail.tail.head.v
    def _5[H1 <: TypedCol[_], H2 <: TypedCol[_], H3 <: TypedCol[_], H4 <: TypedCol[_], H5 <: TypedCol[_], T <: HList]( t : HCons[H1, HCons[H2, HCons[H3, HCons[H4, HCons[H5, T]]]]] ) = t.tail.tail.tail.tail.head.v
    def _6[H1 <: TypedCol[_], H2 <: TypedCol[_], H3 <: TypedCol[_], H4 <: TypedCol[_], H5 <: TypedCol[_], H6 <: TypedCol[_], T <: HList]( t : HCons[H1, HCons[H2, HCons[H3, HCons[H4, HCons[H5, HCons[H6, T]]]]]] ) = t.tail.tail.tail.tail.tail.head.v
    def _7[H1 <: TypedCol[_], H2 <: TypedCol[_], H3 <: TypedCol[_], H4 <: TypedCol[_], H5 <: TypedCol[_], H6 <: TypedCol[_], H7 <: TypedCol[_], T <: HList]( t : HCons[H1, HCons[H2, HCons[H3, HCons[H4, HCons[H5, HCons[H6, HCons[H7, T]]]]]]] ) = t.tail.tail.tail.tail.tail.tail.head.v
    def _8[H1 <: TypedCol[_], H2 <: TypedCol[_], H3 <: TypedCol[_], H4 <: TypedCol[_], H5 <: TypedCol[_], H6 <: TypedCol[_], H7 <: TypedCol[_], H8 <: TypedCol[_], T <: HList]( t : HCons[H1, HCons[H2, HCons[H3, HCons[H4, HCons[H5, HCons[H6, HCons[H7, HCons[H8, T]]]]]]]] ) = t.tail.tail.tail.tail.tail.tail.tail.head.v

    final class DataWrapper[T <: HList]( var row : T )
    {
        def assign( res : SQLiteStatement ) { row.assign( res, 0 ) }
    }

    final class SQLiteWrapper( dbFile : File )
    {
        val conn = new SQLiteConnection( dbFile )
        conn.open()

        def exec( statement : String )
        {
            conn.exec( statement )
        }

        def prepare[T <: HList]( query : String, row : T ) =
        {
            new PreparedStatement(query, row)
        }

        // TODO: Parameterise with tuple type
        // make applicable to for comprehensions (implement filter, map, flatMap)
        final class PreparedStatement[T <: HList]( query : String, var row : T )
        {
            val statement = conn.prepare( query )

            private def bindRec( index : Int, params : List[Any] )
            {
                println( "Value " + params.head )
                // TODO: Does this need a pattern match?
                params.head match
                {
                    case v : Int => statement.bind( index, v )
                    case v : String => statement.bind( index, v )
                    case v : Double => statement.bind( index, v )
                    case _ => throw new ClassCastException( "Unsupported type in bind." )
                }

                if ( params.tail != Nil )
                {
                    bindRec( index+1, params.tail )
                }
            }

            def bind( args : Any* )
            {
                bindRec( 1, args.toList )
            }

            def exec( args : Any* )
            {
                bindRec( 1, args.toList )
                step()
                reset()
            }

            def reset()
            {
                statement.reset()
            }

            def step() : Boolean =
            {
                val success = statement.step()
                row.assign( statement, 0 )
                return success
            }
        }
    }
}

Upvotes: 0

Daniel C. Sobral
Daniel C. Sobral

Reputation: 297305

AFAIK, you can't get around type parameter arity. For evidence of that, take the fact that function and tuples have one definition for each arity, up to the arbitrary arity of 22.

If you nest type declarations, which is what existing implementations of HList do, then you can do something.

Upvotes: 0

Rex Kerr
Rex Kerr

Reputation: 167921

I think you should try using pattern-matching instead of interpretation. First, we need something that will pull out our types from strings using unapply:

object Stringy {
  def unapply(s: String) = Some(s)
}
object Inty {
  def unapply(s: String) = {
    try { Some(s.toInt) }
    catch { case nfe: NumberFormatException => None }
  }
}
object Floaty {
  def unapply(s: String) = {
    try { Some(s.toFloat) }
    catch { case nfe: NumberFormatException => None }
  }
}

Now we can use them in pattern matches:

scala> List("Hello","4","6.0") match {
  case Stringy(s) :: Inty(i) :: Floaty(f) :: Nil => Some((s,i,f))
  case _ => None
}
res3: Option[(String, Int, Float)] = Some((Hello,4,6.0))

Note that if you do it this way, you (1) don't have to return tuples if you don't want to; (2) have access to all the parsed out variables right away; (3) have automatic error checking built in (with options).

Upvotes: 5

Peter Schmitz
Peter Schmitz

Reputation: 5844

I think you got problems with dynamic tuple arity, so you have to implement a method for each tuple arity, something like that:

def interpretListAsTuple2[A,B](s: List[String])(implicit s2a: String => A, s2b: String => B) = {
  s.grouped(2).map { case x :: y => (x: A, y.head: B) }
}

def interpretListAsTuple3[A,B,C](s: List[String])(implicit s2a: String => A, s2b: String => B, s2c: String => C) = {
  s.grouped(3).map { case x :: y :: z => (x: A, y: B, z.head: C) }
}

implicit def string2String(s: String) = s
implicit def string2Int   (s: String) = s.toInt
implicit def string2Float (s: String) = s.toFloat

val returnedFromQuery = List( "Hello", "4", "6.0" )

interpretListAsTuple3[String,Int,Float](returnedFromQuery)

Sorry, this code doesn´t work, because of ambiguity of implicit conversions for String to float in scala´s Predef respectively LowPriorityImplicits. Perhaps someone can help and fix that. But the main idea should be clear, though. You only have to define the implicit conversions once for your data-types, then it works with all tuple-aritys.

EDIT:

You can use the above version to map a list with strings of several tuples. List("Hello", "4", "6.0","Hey","1", "2.3") If you want to handle only one tuple then use this:

def interpretListAsTuple3[A,B,C](s: List[String])(implicit s2a: String => A, s2b: String => B, s2c: String => C): (A,B,C) = {
  s.grouped(3).map { case x :: y :: z => (x: A, y: B, z.head: C) }.next
}

Of course, arguments have to be well formed.

Upvotes: 1

E. Verda
E. Verda

Reputation: 285

Try HList from the MetaScala library.

Upvotes: 1

Related Questions