Having Another Go at Exports (Pre-SIP)

I was thinking about a lookup scheme first. Below is a draft indicating how far I got before I abandoned that line of thought.

The problem with a lookup based scheme is that it complicates further what is already complicated: lookup. This means many interactions with other parts of the compiler. By contrast, the forwarder scheme is simple in the sense of being independent from the rest of the compiler. Once forwarders are generated in Namer we are done; no other part of the compiler needs to be concerned with exports. Also, the Tasty format does not need to have entries for them.

About size of generated code: Yes it’s a concern, but not worse than what we already do for mixins. If size of the generated byte code was of primary importance we could decree that all export alias methods are inline, which means no byte code is generated for them. That would give us power roughly on par with a lookup based scheme. But it would mean that export aliases would be invisible to Java and that they could not implement interface methods.


layout: doc-page
title: “Export”

Syntax changes:

TemplateStat      ::=  ...
                    |  Export
Export            ::=  ‘export’ ImportExprs

Similarly to imports, an export with several parts

export iexpr_1, ..., iexpr_n`

is a shorthand for

export iexpr_1
...
export iexpr_n`

To explain how export clauses influence type checking we explain in the following one possible scheme. As always, compilers are free to pick a different scheme if the observable results are the same.

An export export prefix . selectors in a template is legal if prefix is a path that refers either to a member of the template or to a globally accessible object.
Such an export generates a public “export” member with a compiler-generated name that’s inaccessible to user programs and globally unique, and a special type

ExportType(PT, E)

where E is the export clause itself and PT is the type of prefix. The PT part of an ExportType behaves as usual with regards to type maps such as substitution or as-seen-from.

When typing a selection p.m, if p does not have a member named m, the following adaptation is tried before searching for an implicit conversion of p:

Let es be all the export members of p. We first search all explicit exports and then, if that returns no results, all wildcard exports for a match with m, exactly as it is done when processing import statements. If there are several matching members they all must have the same prefix type PT, or an ambiguity error is reported. Otherwise, pick an arbitary element of the set of matching members, say member e defined in class C with type ExportType(PT, export q . ss). The selection p.m is then rewritten to one of the following alternatives, depending on the form of q.

  • If q refers to a globally accessible object: q.m
  • If q is of the form C.this . q_1 . ... q _n: p . q_1 . ... . q_n . m

You can make proxy classes, but your example would not work, since call1 would be doubly defined. But you can express it like this:

trait Session{
  def call1(): Unit
  def call2(): Unit
  def call3(): Unit
}

class SessionProxy(val session:Session) extends Session {
  export session.{call2, call3}

  def call1(): Unit = {
    println("call1")
    session.call1()
  }
}

A variation of the proposal would automatically suppress export aliases if they would lead to a conflict. If that was done, you could write export session._. It would be easy to do but I am not sure we want to go that far. I’d be interested to read people’s opinion on this.

1 Like

It is difficult to maintain.
Actually when one release of library contains call4 and previous releases does not contain it. It is real headache.

I think it will be killer feature for proxies.

1 Like

It is very useful for dynamic dependency injection.

If I understand the proposal correctly, you can do export session.{ call1 => _, _ }.

1 Like

If I understand the proposal correctly, you can do export session.{ call1 => _, _ }

Yes indeed. I think that’s the best solution. Lightweight and makes clear what gets generated.

My main reservation against automatically suppressing forwards on conflicts are the possible surprises. “Why did it not install an alias for this method? Oh it’s because a method with the same name is inherited through this sequence of traits!” That’s the kind of surprises we know from inheritance that we want to avoid here.

How will it work with overloaded methods?

IIUC Such way to exclude ones is not very convenient for overloaded methods.

Hey - I’d love to see some functionality along these lines.

I think there are two possible semantics here and I think we should be clear which of the two we are targeting. The first is a very light-weight semantic of simply pulling identifiers into scope automatically when you pull other identifiers in. The second is a heavyweight semantic of binding new identifiers as aliasses for underlying identifiers.

class Copier {
  private val scanUnit = new Scanner
  export scanUnit.scan
}

So the two potential semantics I can see here are:

  • lightweight: wherever I have val c: Copier in scope, I have import c.scan._ also in scope, with any needed magic to navigate visibility restrictions
  • heavyweight: the class Copier is augmented with a bunch of synthetic members def foo = this.scanUnit.foo, adjusted as needed for signature

The distinction really, really matters. In the former case, we don’t make any new values. There are no new implied instances, for example. It lets you lift members out of objects to package level for facades, or out of instances to support call-sight delegation, for example.

In the latter case, it becomes possible to use the mechanic to implement API-level delegation. Classes get new, concrete members proxying in the delegate members. However, this comes with the potential cost of aliasing, for example, resulting in clashing implied instances pointing to the same value, or accidentally holding things in memory through unexpected references.

Overloaded methods will be excluded in bulk. So, yes, if you want to replace one of them you have to replace all of them. I believe that’s still an acceptable price to pay.

Holding things in memory is not an issue because all aliases are defs. Clashing implicits: I am not yet sure whether this would be an issue in practice. If it is an issue one could tweak the resolution rules to
declare two implicits the same if they forward to the same value.

Thank you it is clear.
It seems I am in other camp :slight_smile:
I just do not understand which gain will we receive for that price?
I can see name clashing. I think it is the same question as variable shadowing.
For example let us try to imagine which gain we will receive if variable shadowing is not enabled. Will it be better?
I can see troubles but I cannot see much gain comparable to that troubles .
I think such name clashes just increase coupling in the case when we use

export someVal._ 

I do not think that

export someVal.{method =>_, _ }

is a good statement.

Let us imagine that instead of override def someMethod() we would use something like ‘class A extends B.{method =>_, _ }’.
I think it is something very ugly.

Good question. The difference is two (or three?) degrees of implicitness vs one. For variable shadowing, a variable you write could shadow a variable further out. For export hiding, some member that you think is accessed by a wildcard export is in fact not aliased since there is some preexisting definition in some baseclass with the same name and signature. There’s not a single syntactic instance that could tell you this is even happening. I believe this is not comparable with variable showing, and is clearly a lot more problematic.

3 Likes

IIUC thare are 2 points

  1. It is not obvious whether exports should “shadow” the base member or it should be shadowed.
  2. there can be shadowing which is made by mistake

I think if the first point is forbidden nobody will really suffer.
The second point can be solved very easy with some annotation:

trait Session{
  def call1(): Unit
  def call2(): Unit
  def call3(): Unit
}
trait SomeAspect{
  def call1(): Unit = {}
}
class SessionProxy(val session:Session) extends SomeAspect with Session {
  export session._
  
@overimport
  override def call1(): Unit = {
    super.call1()
    session.call1()
  }
}

I cannot see any advantages in export someVal.{someMethod=>} when I want to exclude method. But disadvantages is obvious at least for overrided method.
Just to note in PreparedStatement
it seems that more than half methods are overrided.

You could allow inline export syntax to opt into the inline semantics.

1 Like

Is it possible to allow exporting members in self-type?

type ID = Any { self: Long =>
  export self.{+, -}
}

I think it would be useful when creating opacity types Proposal for Opaque Type Aliases

1 Like

That’s not legal synax, but let’s assume instead this:

trait ID { self: Long =>
  export self.{+, -} 
}

You can write that, but you won’t be able to tie the knot afterwards. + and - would be final in ID. At some point you have to mix Long and ID in one class. At that point you would get an “cannot override final member” error.

It’s not legal syntax, it’s syntax he proposed in the other thread

If we allow self-types for type aliases, there would not be the mix type problem, since the typer knows that ID =:= Long at the context inside ID .

If my library uses export from another library, e.g., shapeless, at top level, does it mean that the user of my library will get all of the shapeless namespace?

I’m unsure what the rules for implicit values are.

trait Foo
object Import {
  implicit val foo : Foo = new Foo{}
}
object Export {
  implicit val foo : Foo = new Foo{}
}

import Import._
export Export._
implcitly[Foo]

Do we have a name collision? Do we have an ambiguity? Does the order between import and export matter?

Just for comparison:
https://kotlinlang.org/docs/reference/delegation.html

interface Base {
    fun printMessage()
    fun printMessageLine()
}

class BaseImpl(val x: Int) : Base {
    override fun printMessage() { print(x) }
    override fun printMessageLine() { println(x) }
}

class Derived(b: Base) : Base by b {
    override fun printMessage() { print("abc") }
}

I think it is just a simple.