35

I am trying to zip multiple sequences to form a long tuple:

val ints = List(1,2,3)
val chars = List('a', 'b', 'c')
val strings = List("Alpha", "Beta", "Gamma")
val bools = List(true, false, false)

ints zip chars zip strings zip bools

What I get:

List[(((Int, Char), String), Boolean)] =
  List((((1,a),Alpha),true), (((2,b),Beta),false), (((3,c),Gamma),false))

However I would like to get a sequence of flat tuples:

List[(Int, Char, String, Boolean)] = 
  List((1,a,Alpha,true), (2,b,Beta,false), (3,c,Gamma,false))

I now I can do:

List(ints, chars, strings, bools).transpose

But it returns weakly typed List[List[Any]]. Also I can do (ints, chars, strings).zipped, but zipped works only on 2-tuples and 3-tuples.

Is there a way to zip (arbitrary) number of equal-length sequences easily?

Tomasz Nurkiewicz
  • 334,321
  • 69
  • 703
  • 674

7 Answers7

11

Here's one way to solve your example, but this is not for an arbitrary number of sequences.

val ints = List(1,2,3)
val chars = List('a', 'b', 'c')
val strings = List("Alpha", "Beta", "Gamma")
val bools = List(true, false, false)

val input = ints zip chars zip strings zip bools

// Flattens a tuple ((A,B),C) into (A,B,C)
def f2[A,B,C](t: ((A,B),C)) = (t._1._1, t._1._2, t._2)

// Flattens a tuple ((A,B),C,D) into (A,B,C,D)
def f3[A,B,C,D](t: ((A,B),C,D)) = (t._1._1, t._1._2, t._2, t._3)

input map f2 map f3

I don't think it is possible to do it generically for tuples of arbitrary length, at least not with this kind of solution. Tuples are strongly-typed, and the type system doesn't allow you to specify a variable number of type parameters, as far as I know, which makes it impossible to make a generalized version of f2 and f3 that takes a tuple of arbitrary length ((A,B),C,D,...) (that would return a tuple (A,B,C,D,...)).

If there were a way to specify a variable number of type parameters, we wouldn't need traits Tuple1, Tuple2, ... Tuple22 in Scala's standard library.

Jesper
  • 202,709
  • 46
  • 318
  • 350
  • +1, thanks. I am currently using the `map` approach, but `t._1._1, t._1._2, t._2, t._3` isn't very readable - and in my case I need a 5-tuple, which makes matters even worse. I don't really need to support arbitrary length lists, but *long enough*. I though maybe there are some specialized methods that return strongly-typed tuples of correct length, but I get your point about `Tuple1`-`Tuple22` problem. – Tomasz Nurkiewicz Mar 09 '12 at 10:40
  • 8
    With pattern matching you can get rid of the unreadable `._1, ._2` etc. syntax: `def f2[A,B,C](t: ((A,B),C)) = t match { case ((a, b), c) => (a, b, c) }` – Jesper Mar 09 '12 at 11:12
8

I think pattern matching is a good option

val ints = List(1,2,3)
val chars = List('a', 'b', 'c')
val strings = List("Alpha", "Beta", "Gamma")
val bools = List(true, false, false)
(ints zip chars zip strings zip bools) map { case (((i,c),s),b) => (i,c,s,b)}

**res1: List[(Int, Char, java.lang.String, Boolean)] = List((1,a,Alpha,true), (2,b,Beta,false), (3,c,Gamma,false))**

or you can add type as well

(ints zip chars zip strings zip bools) map {case (((i:Int,c:Char),s:String),b:Boolean) => (i,c,s,b)}

**res2: List[(Int, Char, java.lang.String, Boolean)] = List((1,a,Alpha,true), (2,b,Beta,false), (3,c,Gamma,false))**
igx
  • 4,101
  • 11
  • 43
  • 88
7

I would create a class which represents the data sets:

case class DataSet(int: Int, char: Char, string: String, bool: Boolean)

This brings nicer names for accessing the values instead of _N we have in tuples. If the lists can have different sizes the shortest should be chosen:

val min = List(ints, chars, strings, bools).map(_.size).min

Now it is possible to extract the data:

val dataSets = (0 until min) map { i => DataSet(ints(i), chars(i), strings(i), bools(i)) }

When the original lists can contain a lot of values it is better to make them to a IndexedSeq so that the access time is O(1).

kiritsuku
  • 52,967
  • 18
  • 114
  • 136
5

Using shapeless, you could do:

import shapeless.Tuples._

val ints = (1, 2, 3)
val chars = ('a', 'b', 'c')

val megatuple = (ints, chars)

val megahlist = (megatuple hlisted) map hlisted

val transposed = (mhlist transpose) map tupled tupled

scala> transposed
res: ((Int, Char), (Int, Char), (Int, Char)) = ((1,a),(2,b),(3,c))

(not sure, if there are more implicts defined which lets you avoid the map and back-and-forth conversions)

[Edit: This part is not true anymore.

Note that the shapeless docs say, only conversions up to Tuple4 are currently supported. You’d have to manually create the HLists then.]

Debilski
  • 66,976
  • 12
  • 110
  • 133
2

Here is another solution which would work for your problem.

ints zip chars zip strings zip bools map{ case (((a, b), c), d) => (a,b,c,d)}
2

I share Jesper's opinion that this is not possible in general, since each tuple arity is represented as separate class in source code, so you have to write separate code to access them unless using a code generator.

But I want to add another possible solution. If you want to preserve the typing of your tuple entries, but are otherwise interested in a more collection-like type, maybe HLists (heterogenous lists) are for you. You can google hlist scala for implementations and explanations.

ziggystar
  • 28,410
  • 9
  • 72
  • 124
1

Using product-collections

scala> ints flatZip chars flatZip strings flatZip bools
res0: org.catch22.collections.immutable.CollSeq4[Int,Char,String,Boolean] = 
CollSeq((1,a,Alpha,true),
        (2,b,Beta,false),
        (3,c,Gamma,false))

This currently works for arity 1 - 22. As you can see the types are preserved.

Mark Lister
  • 1,103
  • 6
  • 16