Comments (7)
Similar to #19021
https://docs.scala-lang.org/scala3/reference/contextual/context-functions.html
Context functions require that the expected type is a context function and the expression is a context function literal. Otherwise a context function value gets applied.
Maybe the doc needs to be more explicit, and also show what does not work.
Maybe the "implicit function types" blog could be updated and added to the docs.
There are two rules that guide type checking of implicit function types.
references to implicit functions get applied to implicit arguments in the same way as references to implicit methods.
The second rule is the dual of the first. If the expected type of an expression t is an implicit function type then t is converted to an implicit closure, unless it is already one.
On these two commandments hang all the law and the prophets.
The broader context is that the feature is a way to simplify the syntax for passing implicit context. It is not merely a different way of writing { implicit x => }
anonfun literals. (That is a feature people ask for back.) In this sense, it does not cater to folks writing explicit arrows, which is to restore boilerplate.
Leaving this ticket open for the doc enhancement. Also tagging the Scala 3 spec for adding this information, and also removing the paragraph on implicit x
as noted by the current reference.
from scala3.
"Context functions require that the expected type is a context function and the expression is a context function literal." makes sense to me. But even with that in mind, I still find the compiler's behavior confusing.
In particular, neither File1
nor File2
specifies foo
's return type, so "the expected type is a context function" is false in both cases. Yet File1
compiles and File2
doesn't.
Furthermore, if I change File2
to
def foo(): (Int) ?=> Int =
println()
(x: Int) ?=> x + 1
to make it clear that a context function is expected, compilation fails with this error:
-- [E007] Type Mismatch Error: Main.scala:3:2 ----------------------------------
3 | (x: Int) ?=> x + 1
| ^^^^^^^^^^^^^^^^^^
| Found: (Int) ?=> Int
| Required: Int
|
| longer explanation available when compiling with `-explain`
1 error found
Which doesn't make sense to me. Where does Required: Int
come from?
In contrast, a similar change to File1
is totally fine.
Additionally,
def foo() =
val f = (x: Int) ?=> x + 1
f(using 10)
compiles fine. But in val f = (x: Int) ?=> x + 1
, there is no expectation for f
to be a context function either (no explicit type is given to f
), so compilation should fail with a similar message as File2
(the first version), to be consistent.
At the risk of side tracking, I would like to mention another confusing behavior, because it somehow seems related:
def foo() =
given Int = 10
val f = (x: Int) ?=> x + 1
f
compiles. But
def foo() =
given Int = 10
(x: Int) ?=> x + 1
fails with
-- [E172] Type Error: Main.scala:3:20 ------------------------------------------
3 | (x: Int) ?=> x + 1
| ^
| No given instance of type Int was found for parameter of (Int) ?=> Int
1 error found
from scala3.
Example for inferring a context function when the RHS is a context function literal: https://github.com/scala/scala3/blob/main/tests/run/implicitFuns.scala#L8
It's not sufficient that the RHS is a block that may later turn out to have a context function result expression.
I, too, made up a brief catalog.
I wrote: "There must be a pop quiz on Coursera that asks which of these compiles as expected?"
I retracted the comment, but I think it's funny because one wouldn't expect this to be that tricky. (As I'm sure you agree.) Maybe I'm wrong that this is only a doc issue; maybe they've come up with an improvement in ergonomics.
def foo(): Int ?=> Int =
println()
(x: Int) ?=> x + 1
def bar() =
(x: Int) ?=> x + 1
def baz() =
println()
(x: Int) ?=> x + 1
def buz(): Int ?=> Int =
println()
val f: Int ?=> Int = (x: Int) ?=> x + 1
f
def f: Int ?=> Int = (x: Int) ?=>
println()
x + 1
buz
is your side-tracking example, but it doesn't compile as I would expect, since it does not return f
unmolested. It has an extra closure, but I see that is eliminated in erasure, so, "no harm no foul".
The useful option is -Vprint:parser,typer
. The docs emphasize that the rewrites happen early, that is, before type checking; for example, invoke(e)
becomes invoke(x ?=> e)
to make the implicit arg available in e
.
foo
(which is when you naturally give it an explicit CF return type) is actually
def foo(): (Int) ?=> Int =
{
def $anonfun(using contextual$1: Int): Int =
{
println()
{
def $anonfun(using x: Int): Int =
{
x.+(1)
}
closure($anonfun)
}
}
closure($anonfun)
}
where the "outer" function wrapper must return an int
and the difference with buz
that typechecks:
def buz(): (Int) ?=> Int =
{
def $anonfun(using contextual$2: Int): Int =
{
println()
val f: (Int) ?=> Int =
{
def $anonfun(using x: Int): Int = x.+(1)
closure($anonfun)
}
f.apply(contextual$2)
}
closure($anonfun)
}
from scala3.
def foo() =
given Int = 10
(x: Int) ?=> x + 1
has the apply
inserted at the block (!).
def foo: <error no implicit values were found that match type Int> =
{
final lazy given val given_Int: Int = 10
{
def $anonfun(using x: Int): Int =
{
x.+(1)
}
closure($anonfun)
}
}.apply(/* missing */summon[Int])
that is the same as baz
, but is a more interesting puzzler during happy hour.
from scala3.
Thank you for the explanation. But I still can't deduce a clear set of rules which I can use to reason about the compiler's behavior. Most importantly, why is apply(summon[Int])
or apply(contextual$2)
added in some cases and not others?
from scala3.
That is rule one. (There are two rules.)
def f(i: Int)(implicit ord: Ordering[Int]) = ???
f(42)
f(42)(Ordering_Int)
analogously
def f: Int ?=> Int = ???
f
f(given_Int)
compare with
scala> def f(i: Int)(implicit ord: Ordering[Int]) = i+1
def f(i: Int)(implicit ord: Ordering[Int]): Int
scala> Option(summon[Ordering[Int]]).map(f(42))
val res0: Option[Int] = Some(43)
where f(42)
is applied or not depending on the expected type.
The details page in the reference says:
Context function literals (x1: T1, ..., xn: Tn) ?=> e are automatically created for any expression e whose expected type is scala.ContextFunctionN[T1, ..., Tn, R], unless e is itself a context function literal. This is analogous to the automatic insertion of scala.Function0 around expressions in by-name argument position.
The analogy holds in the other direction, where p
reference to a by-name parameter usually means evaluation, except in eta-expansion.
So rule two is when apply
is not inserted. It says your ordinary expression is wrapped in a context function where that is expected ... unless it is already a context function literal, which is the case you ask about.
Probably one of the usual suspects will come along to provide clarifying insight.
from scala3.
OK, I think I understand now.
The Rules
- A context function literal always simply produces a context function value, no matter the context of use.
- In a context where a context function is required/expected (based on explicitly/user provided type signatures), an expression which is not a context function literal will be rewritten to be wrapped inside an auto-generated context function literal. This is done even if the expression's type is a context function (but the expression is not a context function literal).
- In other contexts, if an expression has a context function type, but is not a context function literal, then it is rewritten such that an invocation of "apply(summon[...])" is attached to it.
Note that when a block expression appears in a Rule2-context, the context DOES NOT propagate to the block expression's last sub-expression. For example, in
def foo(): (Int) ?=> Int =
given Int = 10
val f = (x: Int) ?=> x + 1
f
Because of the explicit return type for foo
which asks for a context function, the block expression {given Int = 10; val f = (x: Int) ?=> x + 1; f}
will receive a Rule2-rewrite. But f
, the last sub-expression, which is a context function but not a literal, will receive a Rule3-rewrite.
Case 0
def foo() =
(x: Int) ?=> x + 1
def foo(): (Int) ?=> Int =
(x: Int) ?=> x + 1
Both compile because of Rule 1.
Case 1
def foo() =
println()
(x: Int) ?=> x + 1
In this case, the compiler encounters TWO expressions with context function type. First the compiler comes across (x: Int) ?=> x + 1
. The compiler leaves it be (Rule 1). As such, the compiler then comes across { println(); (x: Int) ?=> x + 1 }
, which has type (Int) ?=> Int
, so the complier rewrites it to { println(); (x: Int) ?=> x + 1 }.apply(summon[Int])
(Rule 3). Even knowing the rules, this could be surprising, because one may not realize that the multi-line body of a method is in fact an expression. But it is, and thus we get the compilation error:
-- [E172] Type Error: Main.scala:3:20 ------------------------------------------
3 | (x: Int) ?=> x + 1
| ^
| No given instance of type Int was found for parameter of (Int) ?=> Int
1 error found
The error message unfortunately adds to the confusion. As it makes it seem like the compiler has added apply(summon[Int])
to the literal. But really the compiler has added apply(summon[Int])
to the block expression.
Case 2
def foo(): (Int) ?=> Int =
println()
(x: Int) ?=> x + 1
Similar to Case 1, except { println(); (x: Int) ?=> x + 1 }
is now in a context where a context function of type (Int) ?=> Int
is expected, thus Rule 2, not Rule 3, is triggered. So the block expression is rewritten to (_: Int) ?=> ({ println(); (x: Int) ?=> x + 1 }: Int)
. Hence the compilation error:
-- [E007] Type Mismatch Error: Main.scala:3:2 ----------------------------------
3 | (x: Int) ?=> x + 1
| ^^^^^^^^^^^^^^^^^^
| Found: (Int) ?=> Int
| Required: Int
|
| longer explanation available when compiling with `-explain`
1 error found
Once again, in addition to the rules, it is key to realize that method foo
's whole body is a block expression. As such, the expectation of an (Int) ?=> Int
context function as foo
's return value is applied to the block expression (but not to the last expression (i.e. the literal) within the block, as explained in The Rules)
Case 3
def foo() =
given Int = 10
val f = (x: Int) ?=> x + 1
f
Two expressions with context function types. First (x: Int) ?=> x + 1
, no rewrite (Rule 1). Then f
, Rule 3 adds apply(summon[Int])
. As such, the block expression { given Int = 10; val f = (x: Int) ?=> x + 1; f }
has type Int
, and is in an unconstrained context, so no more rewrites. Compilation passes.
Case 4
def foo() =
given Int = 10
(x: Int) ?=> x + 1
Two expressions with context function types. First (x: Int) ?=> x + 1
, no rewrite (Rule 1). Then { given Int = 10; (x: Int) ?=> x + 1 }
, Rule 3 adds apply(summon[Int])
. Since apply(summon[Int])
is outside the block, it cannot see given Int = 10
, thus the compilation error:
-- [E172] Type Error: Main.scala:3:20 ------------------------------------------
3 | (x: Int) ?=> x + 1
| ^
| No given instance of type Int was found for parameter of (Int) ?=> Int
1 error found
Rules Changes to Reduce Confusion
If the rules are changed to the following, the compiler's behavior may be less confusing.
- A context function literal always simply produces a context function value, no matter the context of use.
- In a context where a context function is required/expected (based on explicitly/user provided type signatures), an expression which is not a context function
literalwill be rewritten to be wrapped inside an auto-generated context function literal.This is done even if the expression's type is a context function (but the expression is not a context function literal). - In other contexts, if an expression has a context function type, but is not a context function literal or block expression, then it is rewritten such that an invocation of "apply(summon[...])" is attached to it.
from scala3.
Related Issues (20)
- Automatic rewrite for `<function> _` can produce non-compiling code for curried functions
- Crash at inlining probably polutes the error reports when inlined value is `Nothing`
- Type mismatch after compiler rewrite for `A with B <:< C`
- Crash in match type with wildcards when using `-Xkind-projector:underscores` HOT 3
- Leaking scoped capabilities HOT 1
- No workaround to dependent-type match types after SIP-56 HOT 6
- Import outside a package clause is not shadowed by an import inside the package clause
- Using full qualifier names for anonymous given naming to avoid double definition HOT 3
- Overloading resolution incorrectly drops alternative with match type result when a target type is provided
- given CanEqual reported as unused even if required by strictEquality HOT 1
- Cannot select from named tuple literal
- Overlapping patches crash when using `-rewrite` and `compiletime.testing.typeCheckErrors`
- Associated types become opaque with indirection
- `-source:3.4-migration` `-rewrite` should remove empty argument list for class with only context bounds
- "Deferred inline" error when calling inline methods on a type class
- Regression for `-Wunused:import` in `business4s/decisions4s` HOT 1
- Match type fails to reduce in non-exhaustive pattern match
- `experimental.modularity` breaks `as`-named infix type
- Scaladoc removes indentation of first line, making code incorrect
- Incrementally compiled code that iterates over Mirror's `MirroredElemTypes` in a macro sometimes dealiases an `opaque` type to its underlying type
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from scala3.