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 ?
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.
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 ?
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.
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.