PartialFunctions are broken when returning ContextFunctions

tested in 3.1.0 and 3.1.1-RC1
Scastie

It appears the compiler produces a faulty PartialFunction when returning Context Functions.
isDefinedAt is always true regardless of input and applyOrElse throws an exception for undefined input instead of delegating to orElse

val ctx2: PartialFunction[Int, Long ?=> String] = { case 1 => "a" }
ctx2.isDefinedAt(1) //true
ctx2.isDefinedAt(2) //true - expected false

Is this a known issue, or should I report it to the dotty issue tracker ?

1 Like

this is desugared to something similar to the following:

val ctx2: PartialFunction[Int, Long ?=> String] =
  def outer(x: Int): Long ?=> String = (j: Long) ?=>

    def inner(using Long): String =
      x match
        case 1 => "a"
    end inner

    inner(using j)
  end outer

  (x: Int) => outer(x)
end ctx2

so the context parameter is lifted to be in scope before the pattern match, conversely to your intuition that you expect it to be lifted to be on the rhs of each branch of the pattern match.

if you read the docs I think this is expected, as the expression { case 1 => "a" } comes on the rhs of a definition typed as Int => Long ?=> String (as PartialFunction is a sub type of Function):

Conversely, if the expected type of an expression E is a context function type (T_1, ..., T_n) ?=> U
and E is not already an context function literal, E is converted to a context function literal by rewriting it to

  (x_1: T1, ..., x_n: Tn) ?=> E

where the names x_1, …, x_n are arbitrary. This expansion is performed before the expression E is typechecked, which means that x_1, …, x_n are available as givens in E.

Thanks for clarifying.
I looked at the output after Typer and it is indeed desugared exactly as you describe it.

Looking at the other examples in the documentation I expected it to behave like a function, except with the parameter being marked as using.

val fun:PartialFunction[Int, Long => String]{ case 1 => usingLong => "a" }

which is because I expected it to behave like this

def b(using Long) = "b"
def fun[A](a:A):PartialFunction[Int, A] = { case 1 => a }

val ctx = fun[Long ?=> String](b)
ctx.isDefinedAt(1) // true
ctx.isDefinedAt(2) // false

ctx(1)(using 1L) // "b"
ctx(2)(using 1L) // match-error

So while the behaviour makes sense given your explanation (thank you!), I’m worried that I’m not the only one who will be caught out by this.

Any thoughts on changing the behaviour of how context functions are desugared in partial functions, or perhaps disallowing this combination completely, like context functions in case classes ?

2 Likes

The explanation does not make immediate sense to me, as an inexpert user, or not even a user, just a dabbler. I’ll try again after the turkey wears off.

Having read the spec, I would expect that the expected type is what was asked for:

new PartialFunction[Int, Long ?=> String] {
  def apply(i: Int): Long ?=> String = i match {
    case 1 => "a"
  }
  def isDefinedAt = // as usual
}

to be rewritten

def apply(i: Int) = (x_0: Long) ?=> i match { case 1 => "a" }

because the expected type is the context function.

1 Like

I thought I had replied to this topic, but apparently it got lost somewhere. There are a number of surprising things (edit: or perhaps, rather, a number of surprising manifestations of the same thing) about how context functions are desugared. I recently submitted a patch to fix an issue where using an existing context function variable as a parameter to another function could result in unbounded stack growth.

My guess is that you could add on to my optimizer pass to check other expression contexts (besides just function arguments), s.t. if you provide an explicit annotation that the case bodies have the appropriate type, my existing unwrapper should do its job.