Adding `.foreach` extension for tuples

I am working in a DSL where having to repeatedly type List in List(a,b,c).foreach is a pain point, I want to extend a (a,b,c).foreach method so that I can write the elements in tuple literal notation which will translate into a List() or Seq() .foreach, like so

val s = (1,2,3).foreach(n => n + 1) 
//  List(1,2,3).foreach(n => n + 1) when evaluated

It must work for tuples of arbitrary length.
I attempt it here

extension [T <: Tuple](tuple: T)
  inline def foreach =
    tuple.productIterator.toList.foreach

val s = (1,2,3).foreach(n => n + 1)

but get this error

value + is not a member of Any

How can I get this working?


extension [T <: Tuple](tuple: T)
  //using direct types
  inline def foreach(f: Tuple.Union[T] => Any): Unit = 
    tuple.toList.foreach(s => f(s.asInstanceOf[Tuple.Union[T]]))
  //with type argument
  inline def foreach2[U >: Tuple.Union[T]](f: U => Any): Unit = 
    tuple.toList.foreach(s => f(s.asInstanceOf[U]))

//with type argument on extension
extension [T <: Tuple, U >: Tuple.Union[T]](tuple: T)
  inline def foreach3(f: U => Any): Unit = 
    tuple.toList.foreach(s => f(s.asInstanceOf[U]))
1 Like

Alternative:

extension [T <: Tuple](tuple: T)
  inline def foreach(f: Tuple.Union[tuple.type] => Unit): Unit =
    tuple.toList.foreach(f)

However, #foreach() only exposes side effects, i.e.

(1, 2, 3).foreach(_ + 1)

“works”, but it effectively is a no-op. A more effective (pun intended) application would be:

(1, 2, 3).foreach(n => println(n + 1))

You rather seem to be looking for a #map() for tuples. I’m not sure if and how this can be accomplished, though. A naive take brings me this far:

extension [T <: Tuple](tuple: T)
  inline def map[A](f: Tuple.Union[tuple.type] => A): List[A] =
    tuple.toList.map(f)

(1, 2, 3).map(_ + 1) // List(2, 3, 4)

…but for proper #map() semantics, the result would have to be a tuple again rather than a list… :thinking:

1 Like

Yes you are right I am looking for a map. Your solution looks good, what is wrong about it’s semantics? You just mean it’s misleading in what is typically understood as what mapping does? (i.e mapping on a container should preserve the container type)

Yes, exactly. And off my head wouldn’t know how to do this. Tuple has #from[I]Array(), but we don’t have a proper element type for this, I guess…? :thinking: One might match on list size (for some finite range of sizes), but this match won’t work at compile time, and from there it’s not that far to full case-by-case extensions, anyway. :face_with_diagonal_mouth:

extension [A](t1: Tuple1[A])
  def map[B](f: A => B): (B) =
    f(t1(0))

extension [A](t2: (A, A))
  def map[B](f: A => B): (B, B) =
    (f(t2(0)), f(t2(1)))

// ...

If you’re fine with lists as results from the “map”, this needn’t concern you, of course. (I’d probably call it something other than #map(), though.)

There’s already builtin map functionality in the std library. It’s not super easy to use because it’s built on polymorphic functions and match types, but it works:

scala> type F[A] = A match
     |   case Int => String
     |   case String => Boolean
     |

scala> val f: [A] => (a: A) => F[A] = [A] => (a: A) => a match
     |   case i: Int => s"int: $i"
     |   case s: String => s.nonEmpty
     |
val f: [A] => (a: A) => F[A] = Lambda/0x00000008006e1800@64b1d0ab

scala> (32, "", "foo", 5).map(f)
val res1: (String, Boolean, Boolean, String) = (int: 32,false,true,int: 5)
2 Likes

Ah, right, nice. In the meantime I found that a past version of me knew that there is some kind of #map() for tuples, but since I never could put it to good use, it slipped from my radar.

Just to note, depending on compiler settings (and Scala version?), it should be a.asMatchable match ....

Not sure if there’s any sane way to put plain functions for uniform tuples on top of this mechanism to make this a match for the DSL use case. But the OP seems to be happy with lists, anyway…

Yeah the receiving function in my DSL expects a List, not a Tuple, It’s just that I find having to type List(a,b,c) over and over again everywhere quite terrible in a markup DSL that’s aiming to be terse and readable, so I just want to write it in tuple literal form and have that convert to a List. Other implicit conversion solutions I would typically go for can’t work here as I must do a .map(fn) at the call-site inlined. i.e I can’t simply do f(a,b,c) with * or f((a,b,c)) with an implicit Tuple -> List conversion imported, then inside the function call .map; instead I must do the map in the callsite f((a,b,c).map(g)) (that’s what the DSL looks like), and I want to avoid f(List(a,b,c).map(g)

I see. Personally, I’d probably be willing to spend one additional char per instantiation in order to keep things simple and just introduce a simple factory function for lists, e.g.

def $[A](as: A*): List[A] = as.toList

$(1, 2, 3).map(_ + 1) // List(2, 3, 4)

But then basically you would be ok with a map function that you can invoke on a tuple literal but returns a List? That should be doable.

scala> extension (tup: Tuple)
     |   def map[B](f: Tuple.Union[tup.type] => B): List[B] =
     |     tup.toList.map(f)
     |
def map(tup: Tuple)[B](f: Tuple.Union[tup.type] => B): List[B]

scala> (1, 2, 3, 4).map(_ + 3)
val res0: List[Int] = List(4, 5, 6, 7)

Which I see is exactly the same thing that @sangamon already suggested :stuck_out_tongue: