Clarifying export of members common to exporter and exportee

I am seeing some behavior around export that seems sensible & IMO desirable, but which doesn’t seem to be mentioned in the Export docs or the WIP Spec.

In the following example, the exporter Exporter and the exportee Exported both share a common superclass Super. common is a member of this supertrait and thus already defined by Exporter.

However, it seems we are permitted to use a wildcard export Exported.{*, given} without hitting a compile error. The wildcard seems to skip over the conflicting export of common, since it’s already inherited by the exporting object. The other export foo is still honored.

I just wanted to confirm this is intended behavior, not an omission or bug? Or is it covered by the export docs somehow and I missed it?

trait Super:
  def common = this.getClass 

object Exported extends Super:
  def foo = 1

object Exporter extends Super:
  export Exported.{*, given}

Exporter.foo
Exporter.common

Scastie

This is the intended behavior. The doc says:

It is a compile-time error if a simple or renaming selector does not identify any eligible members.

It does not talk about wildcard selectors here.

1 Like

Of course, “intended” does not specify “bug or feature”.

This seems like a lintable offense.

“As an exporter, I intended to export m, but then someone added m in a trait somewhere and nobody told me.”

On import, collisions are resolved in my favor if they appear to be explicitly intended by me.

Worth adding that I wanted to joke that the quote was “doc so nice, they said it twice”, but the unintended duplication has already been corrected. Kudos for attention to detail. I noticed a stdlib ticket about missing doc and tried to address it because, even though doc makes me :roll_eyes: (that is, rolling eyes like my teenager no matter what I say), I am inspired to act “against type” (so to speak) because of the fine example you set.

1 Like

“As an exporter, I intended to export m, but then someone added m in a trait somewhere and nobody told me.”
On import, collisions are resolved in my favor if they appear to be explicitly intended by me.

OK, that makes sense and I like the general principle. It will make export more robust if duplicate export of the same signature is treated as benign and skipped over, as there are a number of reasons why this could occur.

Unfortunately, I’ve found a counter-example where it doesn’t work. Shall I raise an issue?

object A:
	def foo: String = "foo"

trait A:
	export A.*

object B:
	export A.*

trait B extends A:
	export B.*   
//error overriding method foo in trait A of type => String;
//  method foo of type => String cannot override final member method foo in trait A  

Scastie

Here’s another example where layered wildcard exports of the same underlying signatures don’t compose peacefully:

object A:
	def foo = ""

object B:
	export A.*

object C:
	export A.*

object D:
	export B.*
	export C.*
//Double definition:
//final def foo: String in object D at line 14 and
//final def foo: String in object D at line 15
//have the same type after erasure.

Scastie

Contrast with the way that traits compose:

trait A:
	def foo = ""
trait B extends A
trait C extends A
object D extends B with C

Here’s another example where *-exports currently clash.

It’s not just a thought experiment, but a miniaturisation of practical issues one encounters when trying to adopt the Scala 3 Units of Measure library Coulomb atm.

object si:  //https://en.wikipedia.org/wiki/International_System_of_Units#Base_units
	case class Meter()
	case class Kilogram()

object extended:
	export si.{*, given}
	case class Gram()

object usage:
	export si.{*, given}
	export extended.{*, given}. //wont compile

Scastie

It’s a question whether we want to augment the export mechanism to a degree where it would basically amount to inheritance. Your examples look like they would be better written like this:

trait si:
  case class Meter()
  case class Kilogram()
object si extends si

trait extended extends si:
  case class Gram()
object extended extends extended

object usage extends si, extended

At the small scale of my example, yes, traits seem like a natural fit.

However, please consider the full size version in the Coulomb library; the si object and the re-export of it in accepted.

The usage object in my example represents the packaging up of several unit systems used by an app into a single importable definition containing all units. However, it encounters trouble if the packaged unit systems contain shared elements.

A potential issue if Coulomb chose to make use of traits is that library clients are, understandably, generally less keen to inherit from library traits, vs importing objects. IME inheritance-based frameworks often have a worse track record of adoption.

What’s wrong with that?

Actually that’s what I would like to have.

Inheritance and exports are two completely different mechanisms (even the result looks similar on the surface). One is dynamic dispatch, the other is static dispatch. Those things aren’t freely interchangeable!

You need both, because depending on context one of the two ways to implement something may be infeasible.

But both ways should be similarly first-class features. (Actually, static dispatch, together with delegation, is for most use-cases the more favorable approach, whereas dynamic dispatch is only needed in some very special circumstances. Java “just” got the defaults wrong; as with almost all other defaults actually. Copying Java was a very bad idea. Rust shows how things should be regarding the defaults. That’s why Rust “is fast and efficient” by default.)

Scala could do a really great job here too, imho. Because we can make dynamic and static dispatch look almost the same. Just a little syntax twist in the declaration. That would be better than what Rust has, with their dyn traits “bolted on”.

But this means that static dispatch (together with powerful tools for delegation) needs to become a first-class citizen in Scala. Not something “bolted on”, which can’t be even used in all circumstances.

Please, just make the language symmetric in this regard.

1 Like

@MateuszKowalewski’s appeal for “Symmetry” resonates with me.

I think export will prove useful in some surprising and “emergent” ways we dont yet fully appreciate. We should be open as to the purposes programmers will find for it.

Particularly, at the appropriate point in the solidification of Scala 3, its precise behaviour should be specified so we can confidently predict how it will work in a given situation.

2 Likes