Why NamedTuple.NamedTuple is not imported by default in Scala 3.7?

I’ve noticed that using NamedTuple requires explicitly referencing NamedTuple.NamedTuple in type constraints, which feels a bit verbose compared to the simpler Tuple.

For example:

def f[T <: Tuple](t: T): Unit = ??? // Simple and concise for regular tuples
def g[N <: NamedTuple.NamedTuple](n: N): Unit = ??? // More verbose for named tuples

I understand that NamedTuple is defined in the scala.NamedTuple module, and I can simplify the syntax by importing scala.NamedTuple.* or defining a type alias like type NT = NamedTuple.NamedTuple. However, this still feels less ergonomic than the default availability of Tuple.

My questions are:

  • What are the design reasons behind not importing NamedTuple.NamedTuple by default in Scala 3.7? For instance, is this to avoid namespace pollution, ensure explicit intent, or address potential type inference issues?

  • Are there plans to introduce a shorter alias (e.g., scala.NTuple) or make NamedTuple more seamlessly integrated into the language, similar to Tuple?

  • Are there recommended best practices for working with NamedTuple to reduce verbosity while maintaining clarity?

  • (Another question) Why not define opaque type AnyNamedTuple <: Tuple = Tuple

2 Likes

Let me provide a concrete example: implement a toCsv method that converts a sequence of NamedTuples into a CSV-formatted string.

First, here’s a working implementation:

scalaimport scala.compiletime.constValueTuple
import scala.NamedTuple.NamedTuple

inline def toCsv[N <: Tuple, V <: Tuple](rows: NamedTuple[N, V]*): String = 
  (constValueTuple[N].toList +: rows.map(_.toList)).map(_.mkString(",")).mkString("\n")

However, the type parameters in this method are a bit verbose, so I tried to simplify them. After some experimentation, I arrived at the following working version:

scalaimport scala.compiletime.constValueTuple
import scala.NamedTuple.AnyNamedTuple

inline def toCsv[NT <: AnyNamedTuple](rows: NT*): String = 
    (constValueTuple[NamedTuple.Names[NT]].toArray +: 
     rows.map(_.asInstanceOf[Tuple].toArray)).map(_.mkString(",")).mkString("\n")

Note that the .asInstanceOf[Tuple] in the code is necessary. Without it, the compiler reports an error:

---[E008] Not Found Error:
  |     rows.map(_.toArray)).map(_.mkString(",")).mkString("\n")
  |              ^^^^^^^^^
  |              value toArray is not a member of NT.
  |              An extension method was tried, but could not be fully constructed:
  |
  |                  NamedTuple.toArray[N, V](_$1)
  |
  |              where:    NT is a type in method toCsv with bounds <: NamedTuple.AnyNamedTuple
1 error found

The reason for this error is the current definition of AnyNamedTuple in the Scala 3 standard library:

opaque type AnyNamedTuple = Any

It seems that changing this definition to:

opaque type AnyNamedTuple = Tuple

might eliminate the need for asInstanceOf[Tuple] in the code above.
Could this modification work, and if so, what would be the appropriate runtime representation for AnyNamedTuple? If not, what’s the best way to simplify the type signature while avoiding asInstanceOf?

Perhaps, there are better answers coming, but here’s my take.

I think importing scala.NamedTuple.* by default is potentially confusing, particularly to a newcomer. Here’s an example of why I claim this;

In this image, I would have wanted a compiler / tooling suggestion that Map is not in scope, as I’m clearly searching for a traditional Map. Instead, I get a NamedTuple complaint. For reasons like this, I’m not sure importing “all” of NamedTuple is a good move.

Your concrete suggestion was for scala.NamedTuple.NamedTuple. I agree, this dodges that particular complaint.

My counter argument to just importing NamedTuple, is that it could also lead to confusion in the other direction, as some of its more interesting functionality isn’t in scope and it becomes less likely that people go searching for it. This could create the impression that NamedTuple is “hobbled” somehow.

Finally, I would note that the two intuitive uses cases I would (personally) reach for at the start of NamedTupleness

are in scope without imports…

… and Once I got to the stage of knowing I wanted more power, it was easy to find :slight_smile: - the act of going looking for it actually taught me quite a bit.

For me I conclude, that the status quo is quite well thought through :person_shrugging:.

Finally given your concrete question above (shameless plug alert) this repo went quite deep on
“NamedTuple as CSV”…

the library should define an extension method .toTuple on AnyNamedTuple that returns Tuple (unless that is ambiguous with the one on NamedTuple) - due to it being an opaque type, changing the RHS does not impact how it is viewed outside

due to it being an opaque type, changing the RHS does not impact how it is viewed outside

How about defined as follow, which should work

opaque type AnyNamedTuple <: Tuple = Tuple

Named tuples ARE NOT considered to be sub-types of tuples, and this is by design. You can read the SIP and relevant discussion for the reasons why.

I had seen these before, but I forgot when I asked the question just now. :sweat_smile:

Maybe we could define a common parent class—call it something like TupleOps—and have both NamedTuple and Tuple inherit from it? Just thinking out loud; this is already way beyond my skill level.

That would break binary backwards compatibility, so that’s not going to happen.