Skip to content

SAM extending Function1 causes anonymous class to be generated #19387

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

Open
armanbilge opened this issue Jan 6, 2024 · 2 comments
Open

SAM extending Function1 causes anonymous class to be generated #19387

armanbilge opened this issue Jan 6, 2024 · 2 comments

Comments

@armanbilge
Copy link
Contributor

Compiler version

3.4.0-RC1-bin-20240104-2746ee8-NIGHTLY

Minimized code

//> using scala 3.4.0-RC1-bin-20240104-2746ee8-NIGHTLY

trait Foo[-A, +B] {
  def apply(a: A): B
}

trait Bar[-A, +B] extends Foo[A, B]

trait Baz[-A, +B] extends Function1[A, B]

class Test {
  def captureFunction1[A, B](f: Function1[A, B]): Unit = ()
  def captureFoo[A, B](f: Foo[A, B]): Unit = ()
  def captureBar[A, B](f: Bar[A, B]): Unit = ()
  def captureBaz[A, B](f: Baz[A, B]): Unit = ()

  def test() = {
    captureFunction1[String, String](_.length.toString)
    captureFoo[String, String](_.length.toString)
    captureBar[String, String](_.length.toString)
    captureBaz[String, String](_.length.toString)
  }
}

Output

/*
 * Decompiled with CFR 0.151.
 */
import java.io.Serializable;
import scala.Function1;
import scala.runtime.BoxesRunTime;

public class Test {
    public <A, B> void captureFunction1(Function1<A, B> f) {
    }

    public <A, B> void captureFoo(Foo<A, B> f) {
    }

    public <A, B> void captureBar(Bar<A, B> f) {
    }

    public <A, B> void captureBaz(Baz<A, B> f) {
    }

    public void test() {
        this.captureFunction1((Function1<String, String> & Serializable)_$1 -> BoxesRunTime.boxToInteger(_$1.length()).toString());
        this.captureFoo(_$2 -> BoxesRunTime.boxToInteger(_$2.length()).toString());
        this.captureBar(_$3 -> BoxesRunTime.boxToInteger(_$3.length()).toString());
        this.captureBaz(new Baz<String, String>(this){
            {
                if ($outer == null) {
                    throw new NullPointerException();
                }
            }

            public final String apply(String _$4) {
                return Test.Test$$_$test$$anonfun$4(_$4);
            }
        });
    }

    public static final /* synthetic */ String Test$$_$test$$anonfun$4(String _$4) {
        return BoxesRunTime.boxToInteger(_$4.length()).toString();
    }
}

Expectation

Implementing a SAM (single abstract method) should avoid generating an anonymous class.

@armanbilge armanbilge added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Jan 6, 2024
@jchyb jchyb added area:backend area:transform and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Jan 9, 2024
@jchyb
Copy link
Contributor

jchyb commented Jan 9, 2024

An interesting part of this is that all scala versions seem to generate anonymous classes like this, including scala 2.

@lrytz
Copy link
Member

lrytz commented Jan 12, 2024

In Scala 2, every trait with a non-abstract member gets a $init$ method.

trait A { def f = 1 }

turns into

public interface A {
    public static /* synthetic */ int f$(A $this) { return $this.f(); }
    default public int f() { return 1; }
    public static void $init$(A $this) { }
}

Scala 3 behaves differently in this regard, the $init$ method is not added (see #10530).

Function1 has concrete methods (compose), and being part of the standard library it's compiled with 2.13, so it has an $init$ method. That's the reason Baz is not treated as a SAM type, it's parent Function1 has an $init$ method (https://github.com/lampepfl/dotty/blob/3.3.1/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala#L44).

Corresponding code in Scala 2: https://github.com/scala/scala/blob/v2.13.12/src/compiler/scala/tools/nsc/transform/Erasure.scala#L1436.

I haven't checked, but the reason captureFunction1 doesn't expand must be because the compiler takes a different code path for Function1, both in Scala 2 and 3.

Maybe it would be safe to add a special case for Function1 to isSam (Scala 3) / compilesToPureInterface (Scala 2).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants