Pre-SIP: export, dual* of import

Hello,

I’ve begun a draft to finally introduce an export keyword into Scala, similar to the existing import keyword.

It’s currently just all my own thoughts so far. Feedback very welcome!

It’s my first time attempting a SIP and I’ve never contributed to the compiler itself so if anyone would like to jump onboard and help out in any capacity at all, that would be very welcome too - pls DM me on Twitter or email me.

Cheers
David

7 Likes

It’d be nice to compare this with previous proposals such as https://groups.google.com/d/msg/scala-debate/qfWK-4zy-Yw/O-1RfTRjDJYJ

1 Like

I have often wanted something like this but I fear for what atrocities library authors will do, so I’d like it to be opt-in, and not automatic when doing an import ._

My biggest usecase for this would be a custom predef, which I think is already addressed by Andy Scott’s work in that area.

I think most (if not all?) export usages could still be done using import, if we had some sort of an import alias, as I mentioned at https://github.com/scala/scala-dev/issues/441#issuecomment-371993598 ( he said without proper knowledge of compiler phases and their constraints)


If I have import theEntireLib = import myLib.{core._, implicits._, otherStuff._} , and I use import theEntireLib then that’s just syntactic sugar for import myLib.{core._, implicits._, otherStuff._} .
You can limit the alias to the scope of the package it resides in, so people can’t just import several libraries in one import alias (creating a huge binary as the sum of all those libraries).

2 Likes

I think it’s an interesting proposal. Something like export would help modular architectures by making it easier to do facades. It strikes me there are (at least) two ways to define export: as delegation or as a dual of import. It seems the proposal goes in the first direction:

Upon using the export keyword, Scala will add types and terms to its object/class/trait.

That looks like delegation to me. If one thinks it through that approach gives a lot of power - it’s in fact close in power to inheritance. But it also means that export is a dual of import only in name, but not in substance.

A more symmetric approach could be to define export as a way to expand shorthands exactly like import does. The effect of an import clause

import p.m

is that references to m are expanded to p.m. Dually the effect of an export clause

export p.m

in a class C could be that if c: C then c.m expands to c.p.m. This one would not involve creation of member definitions.

2 Likes

Unfortunately, the original proposal link has long since disappeared, but for the sake of posterity, here’s the discussion around a similar proposal made by Jamie Webb, back in 2008…

http://www.scala-archive.org/RFC-Export-proposal-td2006245.html

Cheers,

Jon

I really like the idea of having first-class support for delegation.

From an OOP perspective, there seems to be some truth to the usual “composition over inheritance” mantra, and composition is made that much nicer with delegation. I think Scala is already a superior OOP language than Java or C#, but having this tool would cement that position.
Note that there is a restricted form of delegates in Kotlin, where you only delegate to constructor parameters in order to implement an interface – similarly implemented in this old macro annotation library for Scala. I like the approach proposed here better as it can be applied to more scenarios (e.g., reduce import forwarding boilerplate).

This is not just good for OOP. There is also a valid use case from the point of view of modular programming as in ML and OCaml, which has an include keyword that has pretty much the same relation with open as export would have with import:

module type MT = sig
  val x : int
end

module N(M: MT) = struct
  include M  /* x visible from outside */
  let y = x + 1
end

module O(M: MT) = struct
  open M  /* x invisible from outside */
  let y = x + 1
end

is equivalent to Scala’s:

trait MT {
  def x: Int
}
class N(m: MT) {
  export m._  // x visible from outside
  def y = x + 1
}
class O(m: MT) {
  import m._  // x invisible from outside
  def y = x + 1
}
4 Likes

Just for notes: here is an analogical proposal from 2013 (with implementation): https://docs.google.com/document/d/1dlT6NgB9610jqLscCJW2LRB7TapDh3q4d2S3YA_q5zs/edit

  1. (the main differences:
  2. keyword instead annotation.
  3. annotate not imports, but objects we want to export. Btw, this can be interesting but will require all exports be coherent. In large projects, this can be difficult, so if we want to annotate/exports objects in place, then better also add some syntax to ‘mute’ unwanted exports.

import {x, export -> _ } ?

  1. The main question - are somebody from core team want to guide this (?). I remember, my original proposal was closed with notes “changes too big for language” without any consideration.

  2. If the answer for (2) will be positive, it can be interesting to join those proposals.

  3. Other ‘alternative’ idea which will close the same problem: annotations for expanding on imports in macroses. (Can be part of new type macroses, but this is far, far, far … perspective)

  4. Handling ‘delegation’ in one packet (this wide the scope) and require interaction with other ‘relative’ futures of the scala language:

object ThisProvider {
   val me: this.type = this
}

class Wrapper
{
  export ThisProvider._
}

what will be the type of wrapper.me ? wrapper or thisProvider ? The same question when ThisProvider is class and trait. A bit interesting questions, but require a big work.

1 Like

The alternative where c.m expands into c.p.m is more appealing to me. The fact that it does not involve the creation of additional member definitions has at least two significant advantages:

  • They are more robust wrt. binary compatibility: if I add a member to p and I have export p._ somewhere, I do not need to recompile the class containing export p._.
  • With an object A { export pack.B } (instead of the current object A { val B: pack.B.type = pack.B }), a “call site” A.B can completely avoid A and directly bypass to pack.B. This can be important not to trigger the constructor of A, and in turn this allows the other members of A not to be “reachable” for platforms that perform reachability analysis (JS, Native, but also via Proguard on the JVM).

The second bullet would be particularly important when applied to Predef.

Also, this interpretation gives a straightforward answer to @rssh’s last question: the type of wrapper.me is of course ThisProvider.me.type since wrapper.me expands into ThisProvider.me.

2 Likes

btw, this was implemented in @exported import: changes to the compiler was only in search sequence for symbols, without generating a desugared code.

btw, this was implemented in @exported import: changes to the compiler was only in search sequence for symbols, without generating a desugared code.

That looks very much like what I have in mind! Apologies that your earlier work went without follow-up. We collectively dropped the ball for a number of years on the SIP side. After the restart by the Scala Center we fixed a lot of the processes so things should work better now.

Trying to merge two proposal, discovered case, where code generation for bridge methods still needed (or can be useful):

trait MyInterface {
  def myMethod
}

class MyImpl extends MyInterface

class WrappedMyImpl(v: MyImpl) extends MyInterface {
  export v._
}

Question - if myMethod is not defined in b , should bridge method in WrappedMyImpl be generated or class WrappedMyImp will be abstract ?

Without code generation, WrappedMyImpl should not be compiled, but this kill a useful use-case: delegation. (which was not covered in ‘2013 annotated imports’, but covered by @japgolly proposal).

Guess, programmer intuition will vote for automatic generation of the bridge methods.

Yes, that’s the essential difference between redirect models and delegation models. Can exports implement abstract methods? My personal opinion is that we should resist the temptation of doing this, but am willing to entertain arguments to the contrary.

Just one observation: Once you accept that export can implement abstract members would it not be logical that you should also override concrete members? But that needs an override! So what should happen in this case? (I believe there is no good answer under the delegation model).

As a devil advocate, I can say about heuristics, that each idea should be extended to its maximum.

Ie. if the compiler has the ability to generate bridge method and if this generation can be defined in a consistent and univocal way, then the compiler should generate one. Otherwise, a programmer will need to learn, that ‘extending of idea stops here’, since the usual maximum extension heuristics is not works.

With overriding of the concrete methods the situation no longer univocal (compiler must know - what to deduce), so it’s the same as two unrelated traits with the same method definition. It’s obvious that compiler should stop (if we have no ‘abstract override’ in one of definitions)).

From another side, I understand the opposite point :wink:

Also interesting, that my filling about including delegation, depends on the possibility to implement this use-case in another way. Ie. if I expect, that in some hypothetical bright future, metaprogramming systems allow implementing something like Delegated[T](t:T) extends T, then demand to the automatic delegation of exported methods looks low.

Maybe it’s a sign, that for delegation better raise the other language/ library change, which will allow generation of something like DynamicProxy.

I haven’t followed entire discussion but want to note that Rust has some notion of exporting items through pub use syntax: https://doc.rust-lang.org/reference/visibility-and-privacy.html#re-exporting-and-visibility

I have some bad news. Unfortunately I’m not going to be able to continue working on this SIP.

The day after I posted this, all full of energy and enthusiasm, there was a change in my home situation that I didn’t anticipate would have the effect it’s having. I no longer have the time or capacity to spend on this, and I likely won’t for months yet. Literally every single day over the last week I’ve planned to work on this and have been unable each time.

I like that there’s interesting discussion happening. Please feel free to pick up my proposal and run with it, or replace it with a better one. This would be a nice feature to have.

Again, I am sorry to start this just before I discover that I won’t be able to see it through. Good luck.

4 Likes

Thanks for getting the ball rolling, I hope your home situation improves :+1:.

3 Likes

I think this proposal is so cool, and originally I also wanted to make a similar proposal.

Love this idea. Actually, just made another post about the problem then found this one :slight_smile: