The issue with exporting things to which you don’t have access is that you have to write synthetic forwarders to get them, making export
not just the mirror of import
. All import
does is save you some typing, for non-givens; for givens it additionally brings them into consideration, but you had to be able to see them to begin with, so you could have done it some other way. The following are therefore equivalent:
// No imports
run(new foo.Foo, here.there.everywhere.Bar.default)
// Imports
import foo._
import here.there._, everywhere.Bar._
run(new Foo, default)
But export
doesn’t just save typing if you can use it to share access-restricted members.
class Bar { def bippy = "bippy, of course!" }
// This is a compile error
class Foo(private[this] val bar: Bar) {}
myFoo.bar.bippy
// This is totally cool
class Foo(private[this] val bar: Bar) { export bar._ }
myFoo.bippy
Most of the difficulties pointed out by @joshlemer arise from this sort of potentially unintentional leaking of private data. You have an internal implementation which is not what you want for your public API…but the syntax makes it easiest to leak all the details with an export._
, so you do it anyway.
It’s more work, but if you insist that the content be part of the public API already and export
is just a convenience for usage, then this bad-design-by-laziness isn’t reinforced. You can still be lazy with your public API, but at least exporting doesn’t make you any more lazy, and you’re being up-front about it.
trait Pub { def foo: Foo }
private[pkg] trait Impl extends Pub { def bar: Bar }
class Baz private (private val impl: Impl) {
export impl._ // This kind of thing provides all the trouble!
}
(new Baz).impl.bar // Can't do this
(new Baz).bar // So why can we automatically do this?
Here I’ve broken two kinds of access control at the same time–you can see the internals of a package-private trait, and you can gain access to the methods and fields of a private val.
Yes, it saves a lot of work. But it is exactly the saving of work that also causes the danger. If instead you could not share internal details, at least not without explicitly saying how to share them, then the danger would go away.
Idea one (no synthetic methods):
trait Pub { def foo: Foo }
private[pkg] trait Impl extends Pub { def bar: Bar }
class Baz private (private val impl: Impl) {
val pub: Pub = impl
export pub._ // This is now purely syntactic sugar
}
// (new Baz).pub.bar // Can't do this!
(new Baz).pub.foo // Can do this, though!
(new Baz).foo // So this is surely okay!
Idea two (explicitly request synthetic methods):
trait Pub { def foo: Foo }
private[pkg] trait Impl extends Pub { def bar: Bar }
class Baz private (private val impl: Impl) {
export impl as Pub._ // Can bikeshed exact syntax
}
(new Baz).foo // Seems reasonable; we asked explcitly
There is the other issue of having the synthetic forwarders implement the methods of some other trait (matching by name and type, not inheritance) which I don’t have clear thoughts about right now.
But at least in terms of confusion, I think the way to make things better is to get more restrictive, not more permissive. If more is allowed, there are more opportunities to think about when considering what the code does, and it’s harder to understand in the case where there are any problems.