Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala elegant list comprehension as in F#

Just using the basic JDBC interface to read some data using Scala. In F# (using System.Data.SqlClient namespace) we could do something like this to return an immutable list from the database.

    let rs = cmd.ExecuteReader()
    [while rs.Read() do yield rs.GetInt32(1)]

In Scala this proves more difficult, as far as I know there is no "while" comprehension like F#. Effectively I'd like to do something close to F# in Scala without having to use mutable vars - if only because they look ugly and add to Lines of Code.

Something like this seems to be commonplace in my Scala code right now:

 var result = Seq.empty[Int]
 val rs = stmt.executeQuery()
 while (rs.next()) {
     result = result :+ rs.getInt(1) }
like image 739
Richard Todd Avatar asked Jun 24 '13 13:06

Richard Todd


3 Answers

I would create a custom subclass of Iterator that wraps a query result. It's really easy; senia showed how.

But you could also

val rs = stmt.executeQuery
val it = Iterator.continually(if (rs.next()) Some(rs.getInt(1)) else None)
val result = it.takeWhile(_.isDefined).toList.flatten
like image 140
Rex Kerr Avatar answered Oct 22 '22 06:10

Rex Kerr


You could use same way in scala, but I think it's ugly:

class Reader(var i: Int){
  def read = { i-=1; i > 0 }
  def getInt32 = i
}

val r = new Reader(10)
Stream.from(0).takeWhile{ _ => r.read}.map{ _ => r.getInt32}.toList
// List(9, 8, 7, 6, 5, 4, 3, 2, 1)

Idiomatic scala way is to convert your Reader to an Iterator:

implicit class ReaderIterator(r: Reader) extends Iterator[Int] {
  def hasNext = r.read
  def next = r.getInt32
}

scala> new Reader(10).toList
res0: List[Int] = List(9, 8, 7, 6, 5, 4, 3, 2, 1)

But if you are really missing this syntax you could add it:

import scala.collection.immutable.VectorBuilder
class FWhile(c: => Boolean){
  def apply[T](e: => T): Seq[T] = {
    val b = new VectorBuilder[T]
    while (c) b += e
    b.result
  }
}
object FWhile{
  def apply(c: => Boolean) = new FWhile(c)
}

scala> FWhile(r.read){r.getInt32}
res0: Seq[Int] = Vector(9, 8, 7, 6, 5, 4, 3, 2, 1)
like image 24
senia Avatar answered Oct 22 '22 06:10

senia


You could use an implicit class together with an implicit CanBuildFrom. This does use a mutable builder, but not at the caller's side:

object MyResultSetContainer {
  implicit class MyResultSet(rs: ResultSet) {
    def map[T, C <: Iterable[T]](f: (ResultSet) => T)
                               (implicit cbf: CanBuildFrom[Nothing, T, C]): C = {
      val builder = cbf()
      while (rs.next()) {
        builder += f(rs)
      }
      builder.result()
    }
  }
}

to be used like this:

import MyResultSetContainer._
val rs = stmnt.executeQuery("select * from pg_user")

val names = for (row <- rs) yield (row.getString(1))

println(names)
rs.close()

The for comprehension uses map under the hood, so if you prefer map directly:

val names = rs.map(row => row.getString(1))

which produces a sequence. Thanks to CanBuildFrom you can produce other collections as well by providing a type explicitly:

val names: List[String] = rs.map(row => row.getString(1))

How does CanBuildFrom work? The Scala compiler looks at the types involved in this expression: There is the resulting type, and the type returned by the function called by map. Based on this information, the Scala compiler provides a factory implicitly which can be used to create a suitable builder. So you need only one method to produce different types of collections.

If you want to return multiple values, just return a tuple:

val columns = rs.map(row => (row.getInt(2), row.getString(1)))

and the tuple can be used to create a Map directly:

val keyNamesMap: Map[Int, String] = 
  rs.map(row => (row.getInt(2), row.getString(1)))

This is based on the idea that a result set is a list of rows, and so the map function should be available on top of it. The implicit class is used to add the map method to the underlying result set implicitly.

like image 5
Beryllium Avatar answered Oct 22 '22 06:10

Beryllium