Having Another Go at Exports (Pre-SIP)

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.

It’s indeed just as simple in this case. But what if the exported object is not an argument but a locally defined member? I don’t see how this would work with by, so it seems that this construct does not support aggregation in general.

I just wanted to say that overriding is more simple in kotlin.

The kotlin’s documentaion says:
The Delegation pattern has proven to be a good alternative to implementation inheritance

And we have such relatively complicated rule for overriding.

But such overriding is more complicated in general.

Let’s look, how this can be applied to the problem of the composability of the import
statements (where you want to have a way to ‘shrink’ few import statements into one, like importing db access API with a specific context, as in motivation example in https://docs.google.com/document/d/1dlT6NgB9610jqLscCJW2LRB7TapDh3q4d2S3YA_q5zs/edit?usp=sharing )

The simplest possible analog version in dotty will-be:

DAO:

package db

class  DAO[K,T]  given PersistencyOptions[T], IdOptions[K,T]  {
     def  find(id: k): Future[Option[T]]
     def  save(value:T):  Future[Unit]
}

MyProjectDBImplicits:

package myproject.dboptions

implied def myPersistenceOptions[T]  given Generic[T]

Also exists module for default db.stdoptions

package db.stdoptions

implied def defaultPersistenceOptions[T]  given Generic[T]

So, I want to have one clause, i.e. I want:

object MyCompanyDAOS  {
     export db._
     implied export myproject.dboptions._
}

But for using this, developers should write:

import MyCompanyDAOS._
implied import MyCompanyDAOS._

(i.e. still two imports, and developers also have the possibility to shoot yourself in the foot, by writing

import MyCompanyDAOS._
implied import db.stdoptions._

)

So, the original problem still exists, but now not because of imports uncomposability, but because we require separate imports for ‘normal’ and ‘implicit’ cases.

The other part (ie. shrink set of different implicit-s into one) this PRE-SIP cover well. So, we can say that 1/2 of the original problem is solved :wink:

A question which remains open for me: is it possible to restrict use of required implicit values, in such way, that

import MyCompanyDAOS._
implied import db.stdoptions._

should generate compile-time error ?

Having old-style ‘full’ imports, with ‘explicit’ and ‘implicit’ as options, as proposed in the Import implied covering import will fix this issue.

Instead of omitting existing names from wildcard exports you could make the exported methods non-final to enable delegation. This is what we typically do anyway for big interfaces. See https://github.com/slick/slick/blob/master/slick-testkit/src/main/scala/com/typesafe/slick/testkit/util/DelegateConnection.scala for an example.

You don’t implement forwarders for missing methods in all possible Connection impementations that delegate the other methods to another object. Instead you implement a DelegateConnection which delegates all calls once and then subclass it to override individual methods.

This is not as convenient for ad-hoc delegation because you need an additional delegate class but it pays off as soon as you need more than one implementation.

I believe that would make export a powerful (and rather dangerous!) booster for inheritance based architectures. But that’s not its purpose.

It’s purpose is to provide something simpler where inheritance is not necessary. I believe final is an essential ingredient for that. It prevents you from playing overriding tricks with export aliases, and I believe that’s a good thing.

1 Like