Reference-able Package Objects
One limitation with package object
s 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 object
s are the natural “entry point” of a package. While top-level declarations reduce their need somewhat, they do not replace it: package object
s 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 object
s are not a replacement for package object
s, because only package object
s allow the package contents to be defined in other files. Normal object
s would require that the package contents be all defined in a single file in the object
body, or scattered into other files as trait
s in different package
s 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 object
s to expose the build definitions within eachpackage
, and each one is an instance ofmill.Module
-
Requests-Scala uses a
package object
to represent the defaultrequests.BaseSession
instance with the default configuration for people to use -
PPrint uses a
package object
to expose thepprint.log
and other APIs for people to use directly, as a default instance ofPPrinter
-
OS-Lib uses a
package object
to expose the primary API of theos.*
operations
None of these use cases can be satisfied by normal object
s or by top-level declarations, due to the necessity of documentation and inheritance. They need to be package object
s.
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.b
only expands toa.b.package
when used “standalone”, i.e. not when part of a larger select chaina.b.c
or equivalent postfix expressiona.b c
, prefix expression!a.b
, or infix expressiona.b c d
. -
a.b
expands toa.b.package
of the typea.b.package.type
, and only contains the contents of thepackage object
. It does not contain other things in thepackage
a.b
that 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 object
s and normal object
s.
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 object
s. Notably, normal object
s do not allow package
s 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 object
s 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 object
s 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