pato
pato

Reputation: 96

General comprehensions in Scala

As far as I understand, the Scala for-comprehension notation relies on the first generator to define how elements are to be combined. Namely, for (i <- list) yield i returns a list and for (i <- set) yield i returns a set.

I was wondering if there was a way to specify how elements are combined independently of the properties of the first generator. For instance, I would like to get "the set of all elements from a given list", or "the sum of all elements from a given set". The only way I have found is to first build a list or a set as prescribed by the for-comprehension notation, then apply a transformation function to it - building a useless data structure in the process.

What I have in mind is a general "algebraic" comprehension notation as it exists for instance in Ateji PX:

`+ { i | int i : set }               // the sum of all elements from a given set
set() { i | int i : list }           // the set of all elements from a given list
concat(",") { s | String s : list }  // string concatenation with a separator symbol

Here the first element (`+, set(), concat(",")) is a so-called "monoid" that defines how elements are combined, independently of the structure of the first generator (there can be multiple generators and filters, I just tried to keep the examples concise).

Any idea about how to achieve a similar result in Scala while keeping a nice and concise notation ? As far as I understand, the for-comprehension notation is hard-wired in the compiler and cannot be upgraded.

Thanks for your feedback.

Upvotes: 4

Views: 1319

Answers (4)

Ruediger Keller
Ruediger Keller

Reputation: 3034

You can also force the result type of the for comprehension by explicitly supplying the implicit CanBuildFrom parameter as scala.collection.breakout and specifying the result type.

Consider this REPL session:

scala> val list = List(1, 1, 2, 2, 3, 3)
list: List[Int] = List(1, 1, 2, 2, 3, 3)

scala> val res = for(i <- list) yield i
res: List[Int] = List(1, 1, 2, 2, 3, 3)

scala> val res: Set[Int] = (for(i <- list) yield i)(collection.breakOut)
res: Set[Int] = Set(1, 2, 3)

It results in a type error when not specifying the CanBuildFrom explicitly:

scala> val res: Set[Int] = for(i <- list) yield i
<console>:8: error: type mismatch;
 found   : List[Int]
 required: Set[Int]
       val res: Set[Int] = for(i <- list) yield i
                                 ^

For a deeper understanding of this I suggest the following read:

http://www.scala-lang.org/docu/files/collections-api/collections-impl.html

Upvotes: 1

Jan van der Vorst
Jan van der Vorst

Reputation: 476

If you want to use for comprehensions and still be able to combine your values in some result value you could do the following.

case class WithCollector[B, A](init: B)(p: (B, A) => B) {
  var x: B = init
  val collect = { (y: A) => { x = p(x, y) } }
  def apply(pr: (A => Unit) => Unit) = {
    pr(collect)
    x
  }
}

// Some examples
object Test {

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

    // It's still functional
    val r1 = WithCollector[Int, Int](0)(_ + _) { collect =>
      for (i <- 1 to 10; if i % 2 == 0; j <- 1 to 3) collect(i + j)
    }

    println(r1) // 120

    import collection.mutable.Set

    val r2 = WithCollector[Set[Int], Int](Set[Int]())(_ += _) { collect =>
      for (i <- 1 to 10; if i % 2 == 0; j <- 1 to 3) collect(i + j)
    }

    println(r2) // Set(9, 10, 11, 6, 13, 4, 12, 3, 7, 8, 5)
  }

}

Upvotes: 0

oxbow_lakes
oxbow_lakes

Reputation: 134270

About the for comprehension

The for comprehension in scala is syntactic sugar for calls to flatMap, filter, map and foreach. In exactly the same way as calls to those methods, the type of the target collection leads to the type of the returned collection. That is:

list map f   //is a List
vector map f // is a Vector

This property is one of the underlying design goals of the scala collections library and would be seen as desirable in most situations.

Answering the question

You do not need to construct any intermediate collection of course:

(list.view map (_.prop)).toSet //uses list.view

(list.iterator map (_.prop)).toSet //uses iterator

(for { l <- list.view} yield l.prop).toSet //uses view

(Set.empty[Prop] /: coll) { _ + _.prop } //uses foldLeft

Will all yield Sets without generating unnecessary collections. My personal preference is for the first. In terms of idiomatic scala collection manipulation, each "collection" comes with these methods:

//Conversions
toSeq
toSet
toArray
toList
toIndexedSeq
iterator
toStream

//Strings
mkString

//accumulation
sum 

The last is used where the element type of a collection has an implicit Numeric instance in scope; such as:

Set(1, 2, 3, 4).sum //10
Set('a, 'b).sum //does not compile

Note that the String concatenation example in scala looks like:

list.mkString(",")

And in the scalaz FP library might look something like (which uses Monoid to sum Strings):

list.intercalate(",").asMA.sum

Your suggestions do not look anything like Scala; I'm not sure whether they are inspired by another language.

Upvotes: 12

huynhjl
huynhjl

Reputation: 41646

foldLeft? That's what you're describing.

The sum of all elements from a given set:

(0 /: Set(1,2,3))(_ + _)

the set of all elements from a given list

(Set[Int]() /: List(1,2,3,2,1))((acc,x) => acc + x)

String concatenation with a separator symbol:

("" /: List("a", "b"))(_ + _) // (edit - ok concat a bit more verbose:
("" /: List("a", "b"))((acc,x) => acc + (if (acc == "") "" else ",")  + x)

Upvotes: 4

Related Questions