How do I make a swizzle method for tuples?

So I want to make a so called swizzle method for tuples, which takes a tuple A, and a tuple of indices, and then returns a new tuple that is the indices mapped to their corresponding element in A. The problem i have is having the types track properly. Please help me with this.

Example usage:

("hej", true, 10).swizzle(1, 0)    ==> (true, "hej") : (Boolean, String)
(1, 2, 3).swizzle(1, 1, 1)         ==> (2, 2, 2) : (Int, Int, Int)
("hej", false).swizzle(0, 1, 0, 1) ==> ("hej", false, "hej", false) : (String, Boolean, String, Boolean)
(1, 2, 3).swizzle(2, 1, 0)         ==> (3, 2, 1) : (Int, Int, Int)

I tried to do this: Scastie - An interactive playground for Scala.
But apparently it is too much for the compiler.

1 Like

That’s weird. I wonder what it is about the way you’re doing it that triggers the crash? I have much more elaborate code that uses the same concepts for copyFrom (except it has to look up indices from string constants too!).

Should I open an issue for this?

If you turn around the two case elements, the crash goes away and you get regular errors, which i tried to clean up, but I am not able to convince the compiler that the match case is irreducible. Ideas?

2 Likes

Nah yeah, I already had that error before adding the Swizzle type, I was just using transparent inline and a return of Tuple. I then thought that adding the Swizzle type would have helped.

I really can’t help but always feel frustrated when trying to use Tuples, they never work as I want them to work.

3 Likes

@devlaam Against my better nature, I went down this road too, and ended up with the following mystery: Scastie - An interactive playground for Scala.

case class Awkward[Indices <: Tuple](indices: Indices):
  inline transparent def bamboozle(): Tuple.Union[Indices] = 99


Awkward[(Int, Int)]((1, 0)).bamboozle()

Leading to the pithy error message:

Found:    (99 : Int)
Required: Tuple.Union[Indices]

where:    Indices is a type in class Awkward with bounds <: Tuple


Note: a match type could not be fully reduced:

  trying to reduce  Tuple.Union[Indices]
  trying to reduce  Tuple.Fold[Indices, Nothing, [x, y] =>> x | y]
  failed since selector Indices
  does not match  case EmptyTuple => Nothing
  and cannot be shown to be disjoint from it either.
  Therefore, reduction cannot advance to the remaining case

    case h *: t => h | Tuple.Fold[t, Nothing, [x, y] =>> x | y]

Some thoughts:

  1. When should we expect Indices to be something that could be analysed as a precise tuple subclass? Does this code have to go inside a macro or something to see inside Indices? If it isn’t in a macro, why not simply write the result tuples by hand, given that the indices are presumably literals?
  2. Are we about to rediscover the joy of C++ (metaprogramming) template error messages circa 2002 or so?
  3. Big picture moment: shouldn’t we just use Spring, Hibernate and ByteBuddy?

EDIT: I also tried hoisting the use of Tuple.Union into the match type definition to see if that could see deeper there, but while this compiles in itself, it still falls over at the use sites of the type.

1 Like

I will try to experiment with a macro for this, but I think something should be done to make this ”just work” with the transparent inline approach. Might take a while for me to write a macro because i have never done it before.

The fact that it doesn’t work might or might not be an issue per se – it might be worth bringing to the compiler team’s attention, but sometimes the answer is basically, “yeah, we can’t currently do that”.

But as for the crash? Yeah, I’d recommend doing so. My understanding is that any compiler crash, by definition, is an issue worth reporting, especially when you have a reasonably small example of what causes it.

1 Like

To be fair, my example is poor because the typecheck error occurs prior to application of the inlined method, so there would be no chance of 99 being able to match any arbitrary tuple’s union type anyway. Duh!

But if I engage my brain and write:

extension [Indices <: Tuple](inline indices: Indices)
  inline transparent def bamboozle(): Tuple.Union[indices.type] = indices.head


((1, 0)).bamboozle()

Then I get two error messages, again the usual problem with the scrutinee type not being matched with specific tuple deconstruction cases, and also because inlining indices makes an invalid singleton type.

It seems the type Indices is just too abstracted to match against to yield precise types, which you would need to get Tuple.Head to agree with Tuple.Union.

Poking around in Tuple.scala, I see:

inline def head[This >: this.type <: Tuple]: Head[This]

That’s got a self-type as a lower bound and is also inlined as a core method, so presumably this gets to see the precise type of the tuple subclass and thus the match type reduction works.

1 Like

Ok I have a “working” swizzle now. It is a bit unergonomic. By giving the indices as a type argument we do not run into this issue, it is however not very ergonomic to me. Would be nice to have a swizzle method that simply takes a tuple as a regular argument. What does everyone think? Scastie - An interactive playground for Scala.

import scala.compiletime.{erasedValue, constValue}
import scala.compiletime.ops.int.S

type Swizzle[T <: Tuple, Indices <: Tuple] <: Tuple =
  Indices match
    case EmptyTuple => EmptyTuple
    case 0 *: xs => Tuple.Elem[T, 0] *: Swizzle[T, xs]
    case S[n] *: xs => Tuple.Elem[T, S[n]] *: Swizzle[T, xs]

extension [T <: Tuple](inline tup: T)
  inline def swizzle[I <: Tuple]: Swizzle[T, I] =
    inline erasedValue[I] match
      case _: EmptyTuple => EmptyTuple
      case _: (0 *: xs) => tup(0) *: tup.swizzle[xs]
      case _: (S[x] *: xs) => tup(constValue[S[x]]) *: tup.swizzle[xs]

val t1: (Int, Int, Int) = (1, 2, 3).swizzle[(2, 1, 0)]
val t2: (Boolean, String) = ("hello", 10, true).swizzle[(2, 0)]
val t3: (Boolean, Int, Boolean, Int) = (true, 10).swizzle[(0, 1, 0, 1)]

println(t1)
println(t2)
println(t3)
3 Likes

Well, I’m guessing that you want this whole compile-time swizzling capability because you have a macro or some conventional code generator that plays around with indices, and you want the last step of rearranging some fixed tuple to conform to the index pattern to be done by the generated code itself, rather than as more logic in the macro / generator.

Is that the situation?

(Otherwise, if the indices are generated at runtime in the ‘final’ code, then the swizzled tuple types are up in the air and you may as well just work with an indexed sequence of Any or whatever. If the indices are known to whoever is writing the code and you’re writing the swizzle calls by hand, you may was well just write the swizzled tuples directly.)

On that basis, I would say your lateral thinking solution where the indices are singleton type literals is the better one, because it makes it clear that swizzling is purely a compile-time operation involving constant indices.

What’s more, if it’s a macro that generates the use-sites for swizzling, then the ergonomics of not writing the extra square braces is irrelevant anyway. Nobody will be reading that code.

Ship it! (I confess, I didn’t test it, but it looks right and I’m sure you have already).

In passing, may I state the responses on this topic have been most educational; I’ve neglected this corner of Scala so far. Still think Spring is awesome, though. Ahem.

EDIT: Forgot to ask my usual question - was an LLM used?

1 Like

Clever thinking, i did not thought of that!

I agree with @sageserpent-open here, it’s a compile time thing anyway, so using types makes clear is not usable at runtime.

I see all kinds of uses outside the scope of the macro for this. For example, in a DSL where you want to define a whole bunch of properties at the start of some code, and filter particular ones out further down the road for separate processing. This code should be added as an example how to handle these kind of cases in some cookbook. :grinning_face_with_smiling_eyes: