Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No way to differentiate NamedTuple from other types #22422

Open
bishabosha opened this issue Jan 21, 2025 · 8 comments · May be fixed by #22431
Open

No way to differentiate NamedTuple from other types #22422

bishabosha opened this issue Jan 21, 2025 · 8 comments · May be fixed by #22431
Labels

Comments

@bishabosha
Copy link
Member

bishabosha commented Jan 21, 2025

Assume you have a type level algorithm over named tuples that needs to recurse when the value type is a named tuple, otherwise do something else. (e.g. check a named tuple is substructually equivalent to another)

Currently there isn't a way to do that because you cannot differentiate named tuple from other types

Compiler version

3.6.3

Minimized code

Perhaps there is a better way, but for simplicity you can not use match types on their own because the upper bound of AnyNamedTuple is Any.

type IsNamedTuple[T] = T match
  case AnyNamedTuple => false
  case _             => true

val x: IsNamedTuple[NamedTuple.Empty] = true
val y: IsNamedTuple[(foo: Int)] = true
val z: IsNamedTuple[Int] = false // error
val q: IsNamedTuple[String] = false // error

Output

val z: IsNamedTuple[Int] = false // error
Found:    (false : Boolean)
Required: IsNamedTuple[Int]

Note: a match type could not be fully reduced:

  trying to reduce  IsNamedTuple[Int]
  failed since selector Int
  does not match  case NamedTuple.AnyNamedTuple => (true : Boolean)
  and cannot be shown to be disjoint from it either.
  Therefore, reduction cannot advance to the remaining case

    case _ => (false : Boolean)
val q: IsNamedTuple[String] = false // error
Found:    (false : Boolean)
Required: IsNamedTuple[String]

Note: a match type could not be fully reduced:

  trying to reduce  IsNamedTuple[String]
  failed since selector String
  does not match  case NamedTuple.AnyNamedTuple => (true : Boolean)
  and cannot be shown to be disjoint from it either.
  Therefore, reduction cannot advance to the remaining case

    case _ => (false : Boolean)

Expectation

the standard library could have a way to determine if a type is a match type or not, perhaps a primitive compile-time op?

object NamedTuple:
  type IsNamedTuple[T] <: Boolean

Another trick I wanted to try was if NamedTuple.From[T] would reduce or not, but again this isnt useful because NamedTuple.From[(foo: Int)] matches NamedTuple.From[t] and also NamedTuple[ns, vs] so cant be distinguished from a reduced or non-reduced form

@bishabosha bishabosha added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Jan 21, 2025
@bishabosha bishabosha linked a pull request Jan 22, 2025 that will close this issue
@sjrd
Copy link
Member

sjrd commented Jan 22, 2025

The fact that we are unable to do that is a direct consequence of the semantics of opaque type aliases. Given that named tuples are opaque type aliases, we cannot distinguish them that way, since they are not provably disjoint from anything else.

Could you elaborate on why you would need this? You said:

Assume you have a type level algorithm over named tuples that needs to recurse when the value type is a named tuple, otherwise do something else. (e.g. check a named tuple is substructually equivalent to another)

That sounds a lot like the type-level equivalent of "arbitrary-depth flattening of lists". IIUC I could write the same idea of deep structural equivalence of lists this way:

def deepSameListShape(x: Any, y: Any): Boolean = (x, y) match {
  case (xHead :: xTail, yHead :: yTail) =>
    deepSameListShape(xHead, yHead) && deepSameListShape(xTail, yTail)
  case (Nil, _ :: _) | (_ :: _, Nil) =>
    false
  case _ =>
    true // neither are lists, or both are Nil
}

That's usually considered a big code smell. The user of deepSameListShape could be surprised that user-level deep elements actually happen to be lists themselves, defeating the intended logic.

Dispatching on whether an arbitrary type is a named tuple or not seems very similar in spirit to the above. Based on that, it seems like a code smell as well.

@bishabosha
Copy link
Member Author

bishabosha commented Jan 22, 2025

@sjrd the use case is to say that Context[(siteMap: (articles: Pages, about: Pages))] can be provided to a use site that expects Context[(siteMap: (articles: Pages))] because of structural equivalence, so when you reach a non-named tuple you should compare the types.

Another one is path type where you can keep looking within a named tuple for a chain of nested keys - however when you go to some depth that is no longer a named tuple then its not possible to compare to a named tuple type pattern and match type refuses to reduce

@sjrd
Copy link
Member

sjrd commented Jan 22, 2025

Is the argument to Context a phantom type? Because named tuples don't have that subtyping relationship, so values of the former can definitely not be assigned to the latter.

So, I guess you're using named tuples for their type syntax only, without using value?

@bishabosha
Copy link
Member Author

bishabosha commented Jan 22, 2025

its using Selectable Fields - and so there would be some operation using an evidence parameter that the substructure proof exists between the types to do a "Safe cast"

@bishabosha
Copy link
Member Author

I imagine the same sort of thing could be used to convert between two similar named tuples - e.g. to swap order - or drop/add fields

@sjrd
Copy link
Member

sjrd commented Jan 22, 2025

Usually for this sort of thing, you would statically know the expected depth of tupling. And so you wouldn't need to "dynamically" (at the type-level) test whether a type is a named tuple or not.

Likewise with lists of lists. You would statically know whether you need to do xs.map(...) or xss.map(_.map(...))).

@bishabosha
Copy link
Member Author

bishabosha commented Jan 22, 2025

but for derivation you dont know, it comes from the structure of types, if you can only inspect 1 level deep that is very limited.

I guess you are suggesting for this context example, basically each level should then be boxed in a new Context - but to construct the initial context from e.g. a raw arbitrarily nested named tuple value then you need to traverse the type - perhaps this would work in the way such that you NamedTuple.Map each level starting from the outside, but as you enter the inner levels maybe this would not reduce, id have to try

@bishabosha
Copy link
Member Author

bishabosha commented Jan 22, 2025

Given that summon[scala.util.NotGiven[Int <:< AnyNamedTuple]] works (so i can probably generate the substructural proof the long winded way with implicit induction), why can there not be a way to do this without implicit search?

if not a general subtyping compiletime op, then an alternative mode to match types that is static dispatch? (is that inline match)?

scala> summon[scala.util.NotGiven[Int <:< NamedTuple.AnyNamedTuple]]
val res1: scala.util.NotGiven[Nothing] = scala.util.NotGiven@23eb7e28

@Gedochao Gedochao added area:named-tuples Issues tied to the named tuples feature. area:match-types area:opaque-types and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Jan 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants