Reference-able Package Objects
One limitation with package objects is that we cannot currently assign them to values: a.b fails to compile when b is a package object, even though it succeeds when b is a normal object. The workaround is to call a.b.package, which is ugly and non-obvious, or to use a normal object, which is not always possible. This proposal is to allow a.b to automatically expand into a.b.package when b is a package object. Such usage will simplify the language, simplify IDE support for the language, and generally make things more uniform and regular.
Why?
package objects are the natural “entry point” of a package. While top-level declarations reduce their need somewhat, they do not replace it: package objects are still necessary for adding package-level documentation or having the package-level API inherit from traits or classes. Other languages have equivalent constructs (module-info.java or __init__.py) that fulfil the same need, so it’s not just a quirk of the Scala language.
Notably, normal objects are not a replacement for package objects, because only package objects allow the package contents to be defined in other files. Normal objects would require that the package contents be all defined in a single file in the object body, or scattered into other files as traits in different packages and mixed into the object, both of which are messy and sub-optimal.
It’s possible to have a convention “the object named foo is always going to be the primary entrypoint for a package”, but that is just a poor-man’s package object with worse syntax and less standardization.
Many libraries use package objects to expose the “facade” of the package hierarchy:
-
Mill uses
package objects to expose the build definitions within eachpackage, and each one is an instance ofmill.Module -
Requests-Scala uses a
package objectto represent the defaultrequests.BaseSessioninstance with the default configuration for people to use -
PPrint uses a
package objectto expose thepprint.logand other APIs for people to use directly, as a default instance ofPPrinter -
OS-Lib uses a
package objectto expose the primary API of theos.*operations
None of these use cases can be satisfied by normal objects or by top-level declarations, due to the necessity of documentation and inheritance. They need to be package objects.
However, the fact that you cannot easily pass around these default instances as values e.g. val x: PPrinter = pprint without calling pprint.package is a source of friction.
This source of friction is not just for humans, but for tools as well. For example, IntelliJ needs a special case and special handling in the Scala plugin specifically to support this irregularity:
- Original irregularity intellij-scala/scala/scala-impl/src/org/jetbrains/plugins/scala/lang/psi/impl/expr/ScReferenceExpressionImpl.scala at idea242.x · JetBrains/intellij-scala · GitHub
- Special casing to support Mill, which allows references to package objects Fix SCL-23198: Direct references to package objects should be allowed in `.mill` files by lihaoyi · Pull Request #672 · JetBrains/intellij-scala · GitHub
What
This proposal is meant to allow the following:
package a package object b val z = a.b // Currently fails with "package is not a value" Currently the workaround is to use a .package suffix:
val z = a.b.`package` This proposal is to make it such that given a.b, if b is a package containing a package object, expands to a.b.package automatically
Limitations
-
a.bonly expands toa.b.packagewhen used “standalone”, i.e. not when part of a larger select chaina.b.cor equivalent postfix expressiona.b c, prefix expression!a.b, or infix expressiona.b c d. -
a.bexpands toa.b.packageof the typea.b.package.type, and only contains the contents of thepackage object. It does not contain other things in thepackagea.bthat are outside of thepackage object
Both these requirements are necessary for backwards compatibility, and anyway do not impact the main goal of removing the irregularity between package objects and normal objects.
Alternatives
The two main alternatives now are to use .package suffixes, e.g. in Mill writing:
def moduleDeps = Seq(foo.`package`, bar.`package`, qux.baz.`package`) Or to use normal objects. Notably, normal objects do not allow packages of the same name, which leads to contortions. e.g. Rather than:
package object foo extends _root_.foo.bar.Qux{ val bar = 1 } package foo.bar class Qux We need to move the package foo contents into package foo2 to avoid conflicts with object foo, and then we need to add back aliases to all the declarations in foo2 to make them available in foo:
object foo extends foo2.bar.Qux{ val bar = 1 object bar{ type Qux = foo2.bar.Qux } } package foo2.bar class Qux Both of these workarounds are awkward and non-idiomatic, but are necessary due to current limitations in referencing package objects directly
Implementations
Mill since version 0.12.0 already emulates this proposed behavior in Scala 2 using source-code mangling hacks, with custom support in IntelliJ. It works great and does what it was intended to do (allow passing around package objects as values without having to call .package every time)
We have a prototype Scala3 implementation here Expand value references to packages to their underlying package objects by odersky · Pull Request #22011 · scala/scala3 · GitHub