5

I have a function that maps a value of type A to a pair (Seq[B], Seq[C]). I want to apply the function to a sequence of A, and return a pair of flattened Seq[B] and Seq[C]. Here is the code snippet:

val a: Seq[A] 
val mapped: Seq[(Seq[B], Seq[C])] = a.map(f)

val (b, c) = mapped.unzip
val bc: (Seq[B], Seq[C]) = (b.flatten, c.flatten)

The solution is acceptable, but is there a more idiomatic way to do this? I though about for-comprehensions or flatMaps, but I cannot see how I can apply them to the pair.

scand1sk
  • 1,114
  • 11
  • 25
  • Related question: http://stackoverflow.com/questions/2339863/use-map-and-stuff-on-scala-tuples – citxx Sep 18 '13 at 13:44
  • +1. This is a neat question. It feels like there should be a cleaner solution, but I can't think of one off the top of my head. – Travis Brown Sep 18 '13 at 14:07

3 Answers3

3

shapeless 2.0.0-M1 is available, so you can map on tuples:

import shapeless._, syntax.std.tuple._, poly._

val l = Seq(Seq(1, 2, 3, 4, 5) -> Seq('a, 'b, 'c), Seq(101, 102, 103) -> Seq('d, 'e, 'f))

type SS[T] = Seq[Seq[T]]
object flatten extends (SS ~> Seq) {
  def apply[T](ss: SS[T]) = ss.flatten
}

l.unzip.map(flatten) // l.unzip.hlisted.map(flatten).tupled for older versions of shapeless
// (Seq[Int], Seq[Symbol]) = (List(1, 2, 3, 4, 5, 101, 102, 103),List('a, 'b, 'c, 'd, 'e, 'f))

Actually it should be possible to convert polymorphic methods to polymorphic function automatically, but this code doesn't work:

def flatten[T](s: Seq[Seq[T]]) = s.flatten
l.unzip.map(flatten _)
//<console>:31: error: type mismatch;
// found   : Seq[T]
// required: Seq[Seq[T]]
//              l.unzip.map(flatten _)
//                          ^
senia
  • 37,745
  • 4
  • 88
  • 129
3

I would probably use the Monoid instance for Tuple2, and Foldable instance for List (which comes into play via its Traverse instance) to sum the resulting list.

scala> import scalaz._, Scalaz._
import scalaz._
import Scalaz._

scala> val a = List(11,222,3333,44444,555555)
a: List[Int] = List(11, 222, 3333, 44444, 555555)

scala> def f(a: Int): (List[String], List[Int]) = (a.toString.tails.toList, a.toString.tails.toList.map(_.size))
f: (a: Int)(List[String], List[Int])

scala> val mapped = a map f
mapped: List[(List[String], List[Int])] = List((List(11, 1, ""),List(2, 1, 0)), (List(222, 22, 2, ""),List(3, 2, 1, 0)), (List(3333, 333, 33, 3, ""),List(4, 3, 2, 1, 0)), (List(44444, 4444, 444, 44, 4, ""),List(5, 4, 3, 2, 1, 0)), (List(555555, 55555, 5555, 555, 55, 5, ""),List(6, 5, 4, 3, 2, 1, 0)))

scala> mapped.suml
res1: (List[String], List[Int]) = (List(11, 1, "", 222, 22, 2, "", 3333, 333, 33, 3, "", 44444, 4444, 444, 44, 4, "", 555555, 55555, 5555, 555, 55, 5, ""),List(2, 1, 0, 3, 2, 1, 0, 4, 3, 2, 1, 0, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0))

UPD: If you really want Seq, here's what you need to have in the scope to make it work:

implicit val isoTraverse: IsomorphismTraverse[Seq, List] = new IsomorphismTraverse[Seq, List] {
  def G = Traverse[List]
  def iso = new IsoFunctorTemplate[Seq, List] {
    def to[A](sa: Seq[A]): List[A] = sa.toList
    def from[A](la: List[A]): Seq[A] = la.toSeq
  }
}
George
  • 8,368
  • 12
  • 65
  • 106
  • Wow! It's clever! But works for lists and vectors only. As I know there is no `Monoid` instance for `Seq`. – senia Sep 18 '13 at 14:23
  • @senia Well, needed instances can be derived via [Isomorphism](https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/Isomorphism.scala). Here's an [example](https://github.com/scalaz/scalaz/blob/scalaz-seven/example/src/main/scala/scalaz/example/IsomorphismUsage.scala). – George Sep 18 '13 at 14:25
  • No guarantees that those derived instances are going to be lawful though. But they say programming isn't math, so who cares! – George Sep 18 '13 at 14:43
1

I don't find your code particularly non-idiomatic, but here's what I'd write. Note that I do not believe my code to be better than yours, I'm just a fan of folding.

// For testing purposes only, replace with your own code.
val b = List((List(1, 2), List(5, 6)), (List(3, 4), List(7, 8)))

val (l, r) = b.foldLeft((List[Int](), List[Int]())) {(acc, a) =>
  (acc._1 ::: a._1, acc._2 ::: a._2)
}

// Prints 'List(1, 2, 3, 4)'
println(l)

// Prints 'List(5, 6, 7, 8)'
println(r)
Nicolas Rinaudo
  • 6,068
  • 28
  • 41
  • 1
    I used to think that *idiomacy* for scala code means *clarity* (you see, that is likely the reason behind blending OOP and FP idioms in Scala -- sometimes code written in OOP style is plain, sometimes code in written in FP is straightforward and you always could pick the most clear one). Given this, I would use OP's solution rather than yours. – om-nom-nom Sep 18 '13 at 14:00
  • "used to think" - did your definition change? I'd be interested in what your new definition is. I agree with OP's code being clearer than mine - I find mine perfectly legible, but I've spent the last few weeks solving `Functional Programming with Scala`'s exercises and it might very well have affected my brain. – Nicolas Rinaudo Sep 18 '13 at 14:07
  • I'm still thinking so (treat *used to* as my disorientation in english tenses) :-) – om-nom-nom Sep 18 '13 at 14:12