Can't pass generic method as context function argument (eta-expansion is not working?)

Hello!

Is working:

def foo[T](c: T => Boolean)(x: T): Boolean = c(x)
def boo[T](x: T): Boolean = false
println(foo(boo)(1))

Is not working:

def foo[T](c: T ?=> Boolean)(x: T): Boolean = c(using x)
def boo[T](using x: T): Boolean = false
println(foo(boo)(1))

Why? Some compiler drawbacks?

Compiler: Scala 3.7.2

Thanks!

No implicit search was attempted for parameter x of method boo in object Playground
since the expected type T is not specific enough
where: T is a type variable

Providing the type argument works:

People generally seem to hate providing type arguments, for some reason. I changed my mindset about this a while ago, and things are much better :smiley:

2 Likes

Thanks for answer with example!

But actually my question is why compiler can’t infer the type in the case of context function (for regular function it is working)? Is it bug in compiler? Or is it by design?

BTW, this is working:

type Wrapped[T] = T

def foo[T](c: Wrapped[T] ?=> Boolean)(x: T): Boolean = c(using x)
def boo[T](using x: Wrapped[T]): Boolean = false
println(foo(boo)(1))

So in this case compiler can infer the T.

To me it seemed by design (very specific error message), but it could be a bug, who knows. Hopefully some compiler people can answer.

My understanding is that, implicit search is extremely expensive, so the compiler has to make some decisions for when to do it, and when not to do it. Maybe one rule is: “if it’s a bare bones type parameter with no additional information, then don’t attempt the search because the search space is too large” or something like that.

I use a language called Lean where implicits are taken to far more extremes compared to Scala, and often it has to do these massive implicit searches (due to how mathematical proofs are coded) and it’s a massive performance issue.

So I’m leaning towards “by design”.

Context function expressions are rewritten early, before typechecking, as mentioned in the reference. Maybe this is a necessary limitation.

-Vprint:typer shows that it infers correctly

        foo[Int]((using contextual$1: Int) =>
          boo[Any](
            /* ambiguous: no implicit values were found that match expected type
The expected type Any is not specific enough, so no search was attempted */
              summon[<notype>]
          )
        )(42)

but perhaps it is too late for the body.

3 Likes

Thanks guys for trying to explain!

Tried to understand but couldn’t - why regular boo T can be infered as Int but context boo T is Any?

Regular boo example looks perfect, concise, in Scala “style” but context boo looks like weird special case (i should define context boo type explicitly, or introduce some alias (Wrapper)).

Seems i misunderstand something or context function implementation is not finished till end.

Actually your non-context function example also does not infer a specific type.

def f[T](c: T ?=> Boolean)(x: T): Boolean = c(using x)
def g[T](c: T => Boolean)(x: T): Boolean = c(x)

def p[T](using x: T): Boolean = false
def q[T](x: T): Boolean = false

@main def test = println:
  f(p)(42)
  g(q)(42)

infers Any in both cases

          f[Int]((using contextual$1: Int) =>
            p[Any](
              /* ambiguous: no implicit values were found that match expected type
The expected type Any is not specific enough, so no search was attempted */
                summon[<notype>]
            )
          )(42)
          g[Any]((x: Any) => q[Any](x))(42)

which would matter for def q[T: ClassTag] for example.

The improvement for the context function looks like the opposite of what I previously said: Because it has to construct (t: T) ?=> body, it has to infer an expected type, which apparently it does for f[?](cf)(Int). I didn’t look at the details; the reference doesn’t prescribe how inference is done for this case. Usually it’s “inside-out” or “bottom-to-top”.

3 Likes

Thanks for detail investigation, now i understand that i was wrong about regular (non-context) function inference.

But type inference for ‘Wrapped’ example (see above) is looked like:

[info] println(
[info]         foo[Int]((using contextual$1: Wrapped[Int]) => boo[Any](contextual$1))(1)
[info]       )

no problem with boo[Any]…

Changed in my example Boolean to T:

def fooContext[T](c: T ?=> T)(x: T): T = c(using x)
def booContext[T](using x: T): T = x

def foo[T](c: T => T)(x: T): T = c(x)
def boo[T](x: T): T = x

@main def test(): Unit =
  println( fooContext( booContext )( 1 ) )
  println( foo( boo )( 1 ) )

and got the weird compiler (Scala 3.7.2) output (with -Vprint:typer):

[info]     @main def test(): Unit =
[info]       {
[info]         println(
[info]           fooContext[Int]((using contextual$1: Int) =>
[info]             booContext[Int](
[info]               /* ambiguous: no implicit values were found that match expected type
[info] The expected type Int is not specific enough, so no search was attempted */
[info]                 summon[<notype>]
[info]             )
[info]           )(1)
[info]         )
[info]         println(foo[Any]((x: Any) => boo[Any](x))(1))
[info]       }

type inference for regular and context function very differ in this case.

Summary:

  1. Type inference of generic regular and context functions is very inconsistent and strange. (compiler infer for context function T == Int (but is not working anyway) and for regular T == Any (WOW, Why Any? Not logical for me)).
  2. Do not rely on type inference in generic code and define type explicitly (as adviced spamegg1 above):
@main def test(): Unit =
  println( fooContext[Int]( booContext )( 1 ) )
  println( foo[Int]( boo )( 1 ) )

type inference looks expected and working:

[info]     @main def test(): Unit =
[info]       {
[info]         println(
[info]           fooContext[Int]((using contextual$1: Int) =>
[info]             booContext[Int](contextual$1))(1)
[info]         )
[info]         println(foo[Int]((x: Int) => boo[Int](x))(1))
[info]       }

Type inference is undecidable and it has many weird gotchas like this.
If you give up on type inference, your life will be much happier :smiley:

//Can't pass generic method as context function argument (eta-expansion is not working?)
/*

  • boo as a value has the shape [T] => (T ?=> Boolean).

  • foo2 asks for c: T ?=> Boolean for a specific T.

  • Unless you pin T (by explicit type args, a type ascription,

  • or by making c polymorphic), the compiler refuses to guess and

  • also refuses to start an implicit search with an undetermined T.

  • It’s an inference limitation around “polymorphic method → context-function” adaptation, not your logic.

  • */
    object PassMethodAsContextArgument {
    def foo[T](c: T ?=> Boolean)(x: T): Boolean = c(using x)

    def boo[T](using x: T): Boolean = false

// println(foo(boo)(1)) // error: cannot infer type T for ?=> Boolean
// Give T explicitly
println(fooInt(1))

def foo2[T](x: T)(c: T ?=> Boolean): Boolean = c(using x)

def main(args: Array[String]): Unit = {
println(foo2(1)(boo[Int]))
// Eta-expand/instantiate to a value first
val booInt: Int ?=> Boolean = boo[Int]
println(foo(booInt)(1))

// Make boo monomorphic
def booI(using Int): Boolean = false

println(foo(booI)(1)) // T inferred as Int from c

}
}