Skip to content

Commit 87474ec

Browse files
authored
Merge pull request #100 from lihaoyi/reference-package
SIP-68: Reference-able Package Objects
2 parents 83ebe86 + 9872294 commit 87474ec

File tree

1 file changed

+253
-0
lines changed

1 file changed

+253
-0
lines changed
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
---
2+
layout: sip
3+
permalink: /sips/:title.html
4+
stage: implementation
5+
status: under-review
6+
title: SIP-XX - Reference-able Package Objects
7+
---
8+
9+
**By: Li Haoyi**
10+
11+
## History
12+
13+
| Date | Version |
14+
|---------------|--------------------|
15+
| Dec 14th 2024 | Initial Draft |
16+
17+
## Summary
18+
19+
This proposal is to allow the following:
20+
21+
```scala
22+
package a
23+
package object b
24+
25+
val z = a.b // Currently fails with "package is not a value"
26+
```
27+
28+
29+
Currently the workaround is to use a `.package` suffix:
30+
31+
```scala
32+
val z = a.b.`package`
33+
```
34+
35+
This proposal is to make it such that given `a.b`, if `b` is a `package`
36+
containing a `package object`, expands to `a.b.package` automatically
37+
38+
39+
One limitation with `package object`s is that we cannot currently assign them to
40+
values: `a.b` fails to compile when `b` is a `package object`, even though it succeeds when
41+
`b` is a normal `object`. The workaround is to call `a.b.package`, which is ugly and
42+
non-obvious, or to use a normal `object`, which is not always possible. There is no other
43+
way to refer to the `package object b` in the example above.
44+
45+
Allowing `a.b` to automatically expand into `a.b.package` when `b` is a
46+
`package object` will simplify the language, simplify IDE support for the
47+
language, and generally make things more uniform and regular.
48+
49+
50+
Prior Discussion can be found [here](https://contributors.scala-lang.org/t/pre-sip-reference-able-package-objects/6939)
51+
52+
## Motivation
53+
54+
Although package objects have been discussed [being dropped](https://docs.scala-lang.org/scala3/reference/dropped-features/package-objects.html)
55+
in Scala 3, no concrete plans have been made as to how to do so, and we argue that they
56+
are sufficiently useful that keeping them around is preferably to dropping them.
57+
58+
### Package Entrypoints
59+
60+
`package object`s are the natural "entry point" of a package. While top-level declarations
61+
reduce their need somewhat, they do not replace it: `package object`s are still necessary
62+
for adding package-level documentation or having the package-level API inherit from traits
63+
or classes. For example the [Acyclic Plugin](https://github.com/com-lihaoyi/acyclic) uses package
64+
objects as a place to put package-level annotations in source code to apply package-level
65+
semantics in the compiler plugin.
66+
67+
Other languages have equivalent constructs (`module-info.java` or `__init__.py`)
68+
that fulfil the same need, so it's not just a quirk of the Scala language.
69+
70+
### Package API Facades
71+
72+
Many libraries use package objects to expose the "facade" of the package hierarchy:
73+
74+
- Mill uses `package object`s to expose the build definitions within each `package`, and
75+
each one is an instance of `mill.Module`
76+
77+
- Requests-Scala uses a `package object` to represent the default `requests.BaseSession`
78+
instance with the default configuration for people to use
79+
80+
- PPrint uses a `package object` to expose the `pprint.log` and other APIs for people to use
81+
directly, as a default instance of `PPrinter`
82+
83+
- OS-Lib uses a `package object` to expose the primary API of the `os.*` operations
84+
85+
None of these use cases can be satisfied by normal `object`s or by top-level declarations,
86+
due to the necessity of documentation and inheritance. They need to be `package object`s.
87+
88+
However, the fact that you cannot easily pass around these default instances as values e.g.
89+
`val x: PPrinter = pprint` without calling `pprint.package` is a source of friction.
90+
91+
### Uniform Semantics
92+
93+
This source of friction is not just for humans, but for tools as well. For example, IntelliJ
94+
needs a special case and special handling in the Scala plugin specifically to support this irregularity:
95+
96+
* Original irregularity https://github.com/JetBrains/intellij-scala/blob/idea242.x/scala/scala-impl/src/org/jetbrains/plugins/scala/lang/psi/impl/expr/ScReferenceExpressionImpl.scala#L198
97+
98+
* Special casing to support Mill, which allows references to package objects https://github.com/JetBrains/intellij-scala/pull/672
99+
100+
The fact that it is impossible to refer to the `package object` without using a `.package` suffix
101+
is a wart: `.package` is an implementation/encoding detail, and so should not be a necessary part
102+
of the user-facing language. We can refer to all other Scala definitions and objects without
103+
leaking implementation/encoding details, and it would be more uniform to allow that for
104+
`package object`s as well.
105+
106+
107+
## User Alternatives
108+
109+
The two main alternatives now are to use `.package` suffixes, e.g. in Mill writing:
110+
111+
```scala
112+
def moduleDeps = Seq(foo.`package`, bar.`package`, qux.baz.`package`)
113+
```
114+
115+
Or to use normal `object`s. Notably, normal `object`s do not allow `package`s of the
116+
same name, which leads to contortions. e.g. Rather than:
117+
118+
```scala
119+
package object foo extends _root_.foo.bar.Qux{
120+
val bar = 1
121+
}
122+
```
123+
```scala
124+
package foo.bar
125+
class Qux
126+
```
127+
128+
We need to move the `package foo` contents into `package foo2` to avoid conflicts with
129+
`object foo`, and then we need to add back aliases to all the declarations in `foo2` to make
130+
them available in `foo`:
131+
132+
```scala
133+
object foo extends foo2.bar.Qux{
134+
val bar = 1
135+
object bar{
136+
type Qux = foo2.bar.Qux
137+
}
138+
}
139+
```
140+
```scala
141+
package foo2.bar
142+
class Qux
143+
```
144+
145+
Both of these workarounds are awkward and non-idiomatic, but are necessary due to current
146+
limitations in referencing `package object`s directly
147+
148+
Notably, normal `object`s are not a replacement for `package object`s, because only
149+
`package object`s allow the package contents to be defined in other files. Normal `object`s
150+
would require that the package contents be all defined in a single file in the `object` body,
151+
or scattered into other files as `trait`s in _different_ `package`s and mixed into the
152+
`object`, both of which are messy and sub-optimal.
153+
154+
It's possible to have a convention _"the `object` named `foo` is always going to be the
155+
primary entrypoint for a package"_, but that is just a poor-man's `package object` with worse
156+
syntax and less standardization.
157+
158+
## Implementation Alternatives
159+
160+
* We could make `a.b` where `b` is a `package` refer to the entire `package b` namespace, not
161+
just the `package object`. This cannot in general work due to the JVM's _open packages_ and
162+
separate compilation: while `package object`s can only exist in one file present in one
163+
compilation run, JVM `package`s can contain arbitrary sets of classes from different compilation
164+
runs. Thus it is impossible in general to define a "complete" API for a JVM `package` for us to
165+
generate an object to refer to.
166+
167+
* Using Scala 3 [Top Level Definitions](https://docs.scala-lang.org/scala3/book/taste-toplevel-definitions.html)
168+
is one possible alternative to `package object`s, but they fall short on many use cases:
169+
* Top-level definitions cannot generate objects that inherit from classes or traits, which
170+
is necessary in many use cases: Mill (needs them to inherit `mill.Module`), Requests-
171+
Scala (needs it to inherit from `requests.BaseSession`), etc.
172+
* Top-level definitions can be defined in multiple files, so suffer from the issue that
173+
it is at any point in time impossible to know the "entire" API of a `package` provided
174+
by top-level definitions
175+
* Top-level definitions do not provide a natural "package entrypoint" to the `package` source folder,
176+
to provide package-level documentation, annotations, etc.. We could provide another `.scala`
177+
file that we specify by-convention to be the "package entrypoint", but we already have
178+
`package.scala` and it does the job just fine
179+
180+
181+
## Limitations
182+
183+
* With this proposal, `a.b.c` can be refactored to `val x = a.b; x.c` only when `c` is declared inside
184+
the `a.b` package object. This is slightly more irregular than the status quo, which disallows such
185+
a refactoring at any time. In general, a package with a package object no longer behaves the same
186+
as a package without.
187+
188+
## Open Questions
189+
190+
There are some open questions that can be resolved during experimentation
191+
192+
* Should package objects be usable as singleton type prefixes, e.g. `type foo == scala.type`?
193+
* Should package objects participate in `foo() -> foo.apply()` desugaring, e.g. `_root_.pprint(124)`?
194+
*
195+
196+
## Implementation & Testing
197+
198+
199+
Mill since version 0.12.0 already emulates this proposed behavior in Scala 2 using source-code
200+
mangling hacks, with custom support in IntelliJ. It works great and does what it was intended
201+
to do (allow passing around `package object`s as values without having to call `.package` every time)
202+
203+
We have a prototype Scala3 implementation here:
204+
205+
* https://github.com/scala/scala3/pull/22011
206+
207+
The necessary IntelliJ changes have been made below:
208+
209+
* https://github.com/JetBrains/intellij-scala/pull/672
210+
211+
With IntelliJ-side discussion:
212+
213+
* https://youtrack.jetbrains.com/issue/SCL-23198/Direct-references-to-package-objects-should-be-allowed-in-.mill-files
214+
215+
These IntelliJ changes are currently guarded to only apply to `.mill` files, but the
216+
guard can easily be removed to make it apply to any Scala files. In fact, implementing
217+
this proposal would involve _removing_ a considerable amount of special casing from
218+
the Intellij-Scala plugin, resulting in the code analysis for looking up references in
219+
the Scala language to become much more regular and straightforward:
220+
221+
```diff
222+
lihaoyi intellij-scala$ git diff
223+
diff --git a/scala/scala-impl/src/org/jetbrains/plugins/scala/lang/psi/impl/expr/ScReferenceExpressionImpl.scala b/scala/scala-impl/src/org/jetbrains/plugins/scala/lang/psi/impl/expr/ScReferenceExpressionImpl.scala
224+
index b820dff8c3..29ba15bcdd 100644
225+
--- a/scala/scala-impl/src/org/jetbrains/plugins/scala/lang/psi/impl/expr/ScReferenceExpressionImpl.scala
226+
+++ b/scala/scala-impl/src/org/jetbrains/plugins/scala/lang/psi/impl/expr/ScReferenceExpressionImpl.scala
227+
@@ -182,24 +182,7 @@ class ScReferenceExpressionImpl(node: ASTNode) extends ScReferenceImpl(node) wit
228+
})
229+
230+
override def getKinds(incomplete: Boolean, completion: Boolean = false): _root_.org.jetbrains.plugins.scala.lang.resolve.ResolveTargets.ValueSet = {
231+
- val context = getContext
232+
- context match {
233+
- case _ if completion =>
234+
- StdKinds.refExprQualRef // SCL-3092
235+
- case _: ScReferenceExpression =>
236+
- StdKinds.refExprQualRef
237+
- case postf: ScPostfixExpr if this == postf.operation || this == postf.getBaseExpr =>
238+
- StdKinds.refExprQualRef
239+
- case pref: ScPrefixExpr if this == pref.operation || this == pref.getBaseExpr =>
240+
- StdKinds.refExprQualRef
241+
- case inf: ScInfixExpr if this == inf.operation || this == inf.getBaseExpr =>
242+
- StdKinds.refExprQualRef
243+
- case _ =>
244+
- // Mill files allow direct references to package
245+
- // objects, even though normal .scala files do not
246+
- if (this.containingScalaFile.exists(_.isMillFile)) StdKinds.refExprQualRef
247+
- else StdKinds.refExprLastRef
248+
- }
249+
+ StdKinds.refExprQualRef
250+
}
251+
252+
override def multiType: Array[TypeResult] = {
253+
```

0 commit comments

Comments
 (0)