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 workaround to dependent-type match types after SIP-56 #21402

Closed
WojciechMazur opened this issue Aug 20, 2024 · 8 comments · Fixed by #21700
Closed

No workaround to dependent-type match types after SIP-56 #21402

WojciechMazur opened this issue Aug 20, 2024 · 8 comments · Fixed by #21700
Assignees
Labels
area:match-types area:spec area:typer itype:bug regression This worked in a previous version but doesn't anymore
Milestone

Comments

@WojciechMazur
Copy link
Contributor

WojciechMazur commented Aug 20, 2024

Based on the problem found in akka/akka and apache/pekko

In the snippet we can come up with workaround to TypedMultiMap.get by using transparent inline however there seems to be no workaround for the TypedMultiMap.inserted method.

Compiler version

All Scala 3.4+ versions

Minimized code

abstract class AbstractServiceKey:
  type Protocol

abstract class ServiceKey[T] extends AbstractServiceKey:
  type Protocol = T

type Aux[P] = AbstractServiceKey { type Protocol = P }
type Service[K <: Aux[?]] = K match
  case Aux[t] => ActorRef[t]
type Subscriber[K <: Aux[?]] = K match
  case Aux[t] => ActorRef[ReceptionistMessages.Listing[t]]

trait ActorRef[-T]

object ReceptionistMessages:
  final case class Listing[T](key: ServiceKey[T])

class TypedMultiMap[T <: AnyRef, K[_ <: T]]:
  def get(key: T): Set[K[key.type]] = ???
  transparent inline def getInlined(key: T): Set[K[key.type]] = ???
  inline def inserted(key: T, value: K[key.type]): TypedMultiMap[T, K] = ???

object LocalReceptionist {
  final case class State(
      services: TypedMultiMap[AbstractServiceKey, Service],
      subscriptions: TypedMultiMap[AbstractServiceKey, Subscriber]
  ):
    def testInsert(key: AbstractServiceKey)(serviceInstance: ActorRef[key.Protocol]): State = {
      val fails = services.inserted(key, serviceInstance) // error
      ???
    }

  def testGet[T](key: AbstractServiceKey): Unit = {
    val newState: State = ???
    val fails: Set[ActorRef[key.Protocol]] = newState.services.get(key) // error
    val works: Set[ActorRef[key.Protocol]] = newState.services.getInlined(key) // workaround

    val fails2: Set[ActorRef[ReceptionistMessages.Listing[key.Protocol]]] = newState.subscriptions.get(key) // error
    val works2: Set[ActorRef[ReceptionistMessages.Listing[key.Protocol]]] = newState.subscriptions.getInlined(key) // workaround
  }
}

Output

[error] ./test.scala:29:42
[error] Found:    (serviceInstance : ActorRef[key.Protocol])
[error] Required: Service[(key : AbstractServiceKey)]
[error] 
[error] Note: a match type could not be fully reduced:
[error] 
[error]   trying to reduce  Service[(key : AbstractServiceKey)]
[error]   failed since selector (key : AbstractServiceKey)
[error]   does not uniquely determine parameter t in
[error]     case Aux[t] => ActorRef[t]
[error]   The computed bounds for the parameter are:
[error]     t
[error]       val fails = services.inserted(key, serviceInstance) // error
[error]                                          ^^^^^^^^^^^^^^^
[error] ./test.scala:35:46
[error] Found:    Set[Service[(key : AbstractServiceKey)]]
[error] Required: Set[ActorRef[key.Protocol]]
[error] 
[error] Note: a match type could not be fully reduced:
[error] 
[error]   trying to reduce  Service[(key : AbstractServiceKey)]
[error]   failed since selector (key : AbstractServiceKey)
[error]   does not uniquely determine parameter t in
[error]     case Aux[t] => ActorRef[t]
[error]   The computed bounds for the parameter are:
[error]     t
[error]     val fails: Set[ActorRef[key.Protocol]] = newState.services.get(key) // error
[error]                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^
[error] ./test.scala:38:77
[error] Found:    Set[Subscriber[(key : AbstractServiceKey)]]
[error] Required: Set[ActorRef[ReceptionistMessages.Listing[key.Protocol]]]
[error] 
[error] Note: a match type could not be fully reduced:
[error] 
[error]   trying to reduce  Subscriber[(key : AbstractServiceKey)]
[error]   failed since selector (key : AbstractServiceKey)
[error]   does not uniquely determine parameter t in
[error]     case Aux[t] => ActorRef[ReceptionistMessages.Listing[t]]
[error]   The computed bounds for the parameter are:
[error]     t
[error]     val fails2: Set[ActorRef[ReceptionistMessages.Listing[key.Protocol]]] = newState.subscriptions.get(key) // error
[error]                                         

Expectation

This kind of issues might pop-up more often as projects would migrate to Scala 3.4+. It would be great if we could propose some workarounds that would allow for their compilation..

@WojciechMazur WojciechMazur added itype:bug area:typer area:match-types regression This worked in a previous version but doesn't anymore labels Aug 20, 2024
@sjrd
Copy link
Member

sjrd commented Aug 20, 2024

Isn't there a way to work around using a cast, at least?

@SethTisue SethTisue changed the title No workaround to dependant-type match types after SIP-56 No workaround to dependent-type match types after SIP-56 Aug 21, 2024
@SethTisue
Copy link
Member

fyi @patriknw

@dwijnand
Copy link
Member

dwijnand commented Aug 21, 2024

In the snippet we can come up with workaround to TypedMultiMap.get by using transparent inline however there seems to be no workaround for the TypedMultiMap.inserted method.

Btw, the only reason your getInlined works is because ???, being of type Nothing, conforms to Set[ActorRef[key.Protocol]]. But if you use the original, whose RHS is typed as Set[K[key.type]], then you get the same type mismatch error

    val works2: Set[ActorRef[ReceptionistMessages.Listing[key.Protocol]]] = newState.subscriptions.getInlined(key) // workaround
                                                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     Found:    Set[Subscriber[(key : AbstractServiceKey)]]
     Required: Set[ActorRef[ReceptionistMessages.Listing[key.Protocol]]]

The problem is these are type projections, reimplemented with match types. We have the same problem captured in tests/pos/i15155.scala, which is trying to reimplement Enumeration#Value using a match type aliased as EnumValue. So, another workaround is:

import scala.language.`3.3`

@smarter
Copy link
Member

smarter commented Aug 21, 2024

The fundamental abstraction we're missing here is lambdas from terms to types:

class TypedMultiMap[T <: AnyRef, K <: (x: T) =>> Any]:
  def get(key: T): Set[K(key)] = ???
  transparent inline def getInlined(key: T): Set[K(key)] = ???
  inline def inserted(key: T, value: K(key)): TypedMultiMap[T, K] = ???

object LocalReceptionist {
  final case class State(
      services: TypedMultiMap[AbstractServiceKey, x =>> ActorRef[x.Protocol]],
      subscriptions: TypedMultiMap[AbstractServiceKey, x =>> ActorRef[ReceptionistMessages.Listing[x.Protocol]]
  )

I don't think there's a way to encode this currently ([k <: AbstractServiceKey & Singleton] =>> ActorRef[k#Protocol]] doesn't help because a subtype of Singleton is not necessarily a singleton type)

@dwijnand
Copy link
Member

How about with the new -language:experimental.modularity and : Singleton context bound?

@smarter
Copy link
Member

smarter commented Aug 21, 2024

The context bound itself is a term parameter, for Singleton in particular it's an erased term parameter, but we still have the same issue about not being able to go from a term to a type

@dwijnand
Copy link
Member

@sjrd I'm looking at the spec (https://github.com/scala/improvement-proposals/pull/65/files), in lines 280-289:

  * If `T` is a refined type of the form `Base { type Y = ti }`:
    * Let `q` be `X` if `X` is a stable type, or the skolem type `∃α:X` otherwise.
    * If `q` does not have a type member `Y`, fail as not matching (that implies that `X <:< Base` is false, because `Base` must have a type member `Y` for the pattern to be legal).
    * If `q.Y` is abstract, fail as not specific.
    * If `q.Y` is a class member:
      * If `q` is a skolem type `∃α:X`, fail as not specific.
      * Otherwise, compute `matchPattern(ti, q.Y, 0, scrutIsWidenedAbstract)`.
    * Otherwise, the underlying type definition of `q.Y` is of the form `= U`:
      * If `q` is a skolem type `∃α:X` and `U` refers to `α`, fail as not specific.
      * Otherwise, compute `matchPattern(ti, U, 0, scrutIsWidenedAbstract)`.

The failures in the example are all Service[(key : AbstractServiceKey)] and Subscriber[(key : AbstractServiceKey)] which fall into line 283 "If q.Y is abstract". But the next two branches (L284 & L290) take into account whether the scrutinee type X is stable or not (via whether q is a skolem). Why don't we also that that into consideration in our branch? That keeps Service[AbstractServiceKey] as failing and makes Service[(key : AbstractServiceKey)] equivalent to ActorRef[key.Protocol]. This would allow TypedMultiMap to be defined with an abstract function from key type to value type, and it could be successfully used with a member type extracting MT as long as we provide it with stable key types as input.

@sjrd
Copy link
Member

sjrd commented Aug 27, 2024

Yes, I think I thought about that as well when I initially looked at the Pekko issue a while ago. AFAICT it should be sound, but it requires a new round of amending that SIP.

sjrd added a commit to dotty-staging/dotty that referenced this issue Oct 4, 2024
…inees in match types.

Previously, through the various code paths, we basically allowed
type member extraction for stable scrutinees if the type member
was an alias or a class member. In the alias case, we took the
alias, whereas in the class case, we recreated a selection on the
stable scrutinee. We did not allow that on abstract type members.

We now uniformly do it for all kinds of type members. If the
scrutinee is a (non-skolem) stable type, we do not even look at
the info of the type member. We directly create a selection to it,
which corresponds to what we did before for class members.

We only try to dealias type members if the scrutinee type is not
a stable type.
sjrd added a commit to dotty-staging/dotty that referenced this issue Oct 4, 2024
…inees in match types.

Previously, through the various code paths, we basically allowed
type member extraction for stable scrutinees if the type member
was an alias or a class member. In the alias case, we took the
alias, whereas in the class case, we recreated a selection on the
stable scrutinee. We did not allow that on abstract type members.

We now uniformly do it for all kinds of type members. If the
scrutinee is a (non-skolem) stable type, we do not even look at
the info of the type member. We directly create a selection to it,
which corresponds to what we did before for class members.

We only try to dealias type members if the scrutinee type is not
a stable type.
sjrd added a commit to dotty-staging/dotty that referenced this issue Oct 4, 2024
…inees in match types.

Previously, through the various code paths, we basically allowed
type member extraction for stable scrutinees if the type member
was an alias or a class member. In the alias case, we took the
alias, whereas in the class case, we recreated a selection on the
stable scrutinee. We did not allow that on abstract type members.

We now uniformly do it for all kinds of type members. If the
scrutinee is a (non-skolem) stable type, we do not even look at
the info of the type member. We directly create a selection to it,
which corresponds to what we did before for class members.

We only try to dealias type members if the scrutinee type is not
a stable type.
sjrd added a commit to dotty-staging/dotty that referenced this issue Oct 8, 2024
…inees in match types.

Previously, through the various code paths, we basically allowed
type member extraction for stable scrutinees if the type member
was an alias or a class member. In the alias case, we took the
alias, whereas in the class case, we recreated a selection on the
stable scrutinee. We did not allow that on abstract type members.

We now uniformly do it for all kinds of type members. If the
scrutinee is a (non-skolem) stable type, we do not even look at
the info of the type member. We directly create a selection to it,
which corresponds to what we did before for class members.

We only try to dealias type members if the scrutinee type is not
a stable type.
sjrd added a commit that referenced this issue Oct 8, 2024
… in match types. (#21700)

Previously, through the various code paths, we basically allowed type
member extraction for stable scrutinees if the type member was an alias
or a class member. In the alias case, we took the alias, whereas in the
class case, we recreated a selection on the stable scrutinee. We did not
allow that on abstract type members.

We now uniformly do it for all kinds of type members. If the scrutinee
is a (non-skolem) stable type, we do not even look at the info of the
type member. We directly create a selection to it, which corresponds to
what we did before for class members.

We only try to dealias type members if the scrutinee type is not a
stable type.
@WojciechMazur WojciechMazur added this to the 3.6.0 milestone Oct 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:match-types area:spec area:typer itype:bug regression This worked in a previous version but doesn't anymore
Projects
None yet
6 participants