Skip to content

use FunctionalInterface to indicate trait cannot have a constructor, or violate other SAM type conditions #197

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
adriaanm opened this issue Aug 10, 2016 · 12 comments
Milestone

Comments

@adriaanm
Copy link
Contributor

to deal with no-op constructors gratuitously being added, ruining a trait's potential as a SAM

@retronym
Copy link
Member

Noting some overlap with universal traits, in which extends Any prevents constructors.

scala> trait T extends Any { def foo = 14 }
defined trait T

scala> :javap -private T
Compiled from "<console>"
public interface T {
  public abstract int foo();
}

scala> trait T extends Any { def foo = 14; ??? }
<console>:11: error: this statement is not allowed in universal trait extending from class Any
       trait T extends Any { def foo = 14; ??? }
                                           ^

@adriaanm
Copy link
Contributor Author

adriaanm commented Aug 10, 2016

ref #191

@lrytz
Copy link
Member

lrytz commented Aug 11, 2016

what's the proposal exactly? that the compiler automatically adds @FI to interface classfiles that have only methods (no fields, init stats)?

@retronym
Copy link
Member

I take it to mean that if a trait is so annotated, we'll a) error if a constructor is required, b) error if any ancestor has a constructor and c) allow LMF to use it as a lambda target type.

@retronym
Copy link
Member

Maybe FI is the wrong annotation to mark this, though

@lrytz
Copy link
Member

lrytz commented Aug 11, 2016

It seems unfortunate to me that the user is required add an annotation in order to get the LMF translation.

@retronym
Copy link
Member

retronym commented Aug 11, 2016

Separate compilation and the generality of traits are the thorns in our side.

We might be able to do better down the track with a ScalaLambdaMetafactory that checks at link time which ancestor traits actually have mixin constructors, and generates a suitable constructor for the generated anonymous class. We'd need to pass make the linearization order known the the bootstrap method to call them in the right order.

You could even implement fields in the same manner.

@lrytz
Copy link
Member

lrytz commented Aug 11, 2016

That sounds interesting! Maybe at some point having our own LMF will solve enough issues for us to consider it.

But back to the proposal. Suppose I write @FunctionalInterface trait B extends A { def sam(): Int } with A coming from an external library. If A changes things may break, but that's irrespective of whether the @FI annotation is user-written or automatically added.

@retronym
Copy link
Member

Let's call the annotation @NoInit (named after the dotty flag of the same name), rather than @FunctionaInterface. (The latter has the extra meaning of "has exactly one abstract method" which is a separate concern.)

@NoInit would mean: this trait has not initializer today, and all its Scala defined ancestors are must also have this annotation. By using this annotation, I'm saying that intend to keep things that way, so clients of the class can omit the call to $init$.

Traits are super fragile with separate compilation, but the current scheme allows you to add side effects to the constructor of a trait without recompiling subclasses.

There is just the one special case that if a trait has no method bodies in its decls, the $init$ method and the impl class are elided. That inconsistency brings separate compilation problems: https://gist.github.com/retronym/af3037454509de9412fc8e1d5d38d43c

The closest thing we have to this today is trait T extends AnyVal, although that brings the addtional restriction that you can't extends arbitrary Java interfaces and that you can't refer to AnyRef methods.

@DarkDimius
Copy link

For the record, Dotty uses NoInits flag to represent similar notion, with only difference that it does not speak about ancestors. Why would you need transitivity?

@retronym
Copy link
Member

retronym commented Aug 11, 2016

With dotty, compile:

trait T
trait U extends T { def apply(a: Any): Any }
object Test {
  def main(args: Array[String]): Unit = {
    val u: U = x => x
    assert(u("") == "")
  }
}

Then, recompile T as:

trait T { throw new Exception("hello?") }

Rerunning Test, we don't get the Exception("hello"), nor do we get a LinkageError.

Dotty does seem to respect ancestors when computing whether to use indy or not:

trait T { ??? }
trait U extends T { def apply(a: Any): Any }
object Test {
  def main(args: Array[String]): Unit = {
    val u: U = x => x // new Test$$anonfun$2
    assert(u("") == "")
  }
}

@retronym
Copy link
Member

This is conceptually the same as in Java:

// run 1
interface I {}
class C implements I {}
class I

This would result in an IncompatibleClassChangeError when loading C.

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

No branches or pull requests

4 participants