Now that Named Tuples may become a reality, I want to bring up a pain point that this feature can help solve.
The problem with structural types
Currently structural types are very limiting. The only way to get a refinement is through casting and requires every function that transforms the refined object to keep the refinement along the way.
For example, say I have a class Foo[T]
. I need to have specific fields available (at compile-time) for instances of Foo
according to the type T
. Structural types provide no such functionality.
Solving the problem via Selectable and Implicit Conversions
The only way to achieve this is by extending Selectable
and creating an ugly implicit conversion and a helper macro like so:
final class Foo[T] extends Selectable:
def selectDynamic(name: String): Any = /* runtime field selection */
given [T](using r: Refiner[T]): Conversion[Foo[T], r.Out] = _.asInstanceOf[r.Out] //just forcing the compiler to cooperate
trait Refiner[T]{ type Out <: Foo[T] }
object Refiner:
transparent inline given [T]: Refiner[T] = ${ refineMacro[T] }
def refineMacro[T](using Quotes, Type[T]): Expr[Refiner[T]] =
import quotes.reflect.*
val tTpe = TypeRepr.of[T]
val fooTpe = TypeRepr.of[Foo[T]]
val fields: List[(String, TypeRepr)] = /*your field logic here*/
val refined = fields.foldLeft(fooTpe) { case (r, (n, t)) => Refinement(r, n, t) }
val refinedType = refined.asTypeOf[Foo[T]]
'{ new Refiner[T]{ type Out = refinedType.Underlying }}
end refineMacro
end Refiner
This is clearly ugly, relies on a clunky implicit conversion mechanism (like implicit classes did) and also fails to support field auto-completion by the IDE because the IDE cannot know what fields are actually available.
Proposal: Expanding Selectable
with a Named Tuple Fields
type member
We add to Selectable
a Tuple type member Fields
.
trait Selectable:
type Fields <: Tuple
Rules:
- If
Fields
is unbounded further by the user (the upper bound remainsTuple
), then the current behavior of Selectable extended objects remains the same. (If the user just bounds withNonEmptyTuple
we treat it the same also). - If
Fields
is anEmptyTuple
, there are no selectable fields available and we should get a compiler error when trying to select a field that does not already exist. - If
Fields
is some kind of non-zero Tuple arity, then the available fields and their types will be defined by the tuple type. This should work with both named and unnamed tuples just the same, but of course generally more useful with named tuples.