Skip to content

Commit d5744c0

Browse files
committed
[Proposal] Common Declarations
This is a proposal to add `common` declarations to the language. These declarations are an important stepping stone to full Rust-like type classes. Given what's described here it would be a relatively small step to do the full typeclass encoding. Without the full encoding, the usefulness of `common` definitions is still there but not as great. So it's dubious whether this would pay its cost as a separate feature. Nevertheless, it's nicer to describe this feature in isolation, since we achieve better conceptual modularity that way.
1 parent 4a257df commit d5744c0

File tree

5 files changed

+526
-124
lines changed

5 files changed

+526
-124
lines changed
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
---
2+
layout: doc-page
3+
title: "Common Declarations"
4+
---
5+
6+
`common` declarations and definitions are a way to specify members of the companion object of a class. Unlike `static` definitions in Java, `common` declarations can be inherited.
7+
8+
As an example, consider the following trait `Text` with an implementation class `FlatText`.
9+
10+
```scala
11+
trait Text {
12+
def length: Int
13+
def apply(idx: Int): Char
14+
def concat(txt: Text): Text
15+
def toStr: String
16+
17+
common def fromString(str: String): Text
18+
common def fromStrings(strs: String*): Text =
19+
("" :: strs).map(fromString).reduceLeft(_.concat)
20+
}
21+
22+
class FlatText(str: String) extends Text {
23+
def length = str.length
24+
def apply(n: Int) = str.charAt(n)
25+
def concat(txt: Text): Text = new FlatText(str ++ txt.toStr)
26+
def toStr = str
27+
28+
common def fromString(str: String) = new FlatText(str)
29+
}
30+
```
31+
32+
The `common` definition of `fromString` is abstract in trait `Text`. It is defined in the implementing companion object of `FlatText`. By contrast, the `fromStrings` method in trait Text is concrete, with an implementation referring to the abstract `fromString`. It is inherited by the companion object `FlatText`. So the following are legal:
33+
34+
```scala
35+
val txt1 = FlatText.fromString("hello")
36+
val txt2 = FlatText.fromStrings("hello", ", world")
37+
```
38+
39+
`common` definitions are only members of the companion objectcs of classes, not traits. So the following would give a "member not found" error.
40+
41+
```scala
42+
val erroneous = Text.fromStrings("hello", ", world") // error: not found
43+
```
44+
45+
## The `Instance` type
46+
47+
In the previous example, the argument and result type of `concat` is just `Text`. So every implementation of `Text` has to be prepared to concatenate all possible implementatons of `Text`. Furthermore, we hide the concrete implementation type in the result type of `concat` and of the construction methods `fromString` and `fromStrings`. Sometimes we want a different design that specifyfies the actual implementation type instead of the base trait `Text`. We can refer to this type using the predefined type `Instance`:
48+
49+
```scala
50+
trait Text {
51+
def length: Int
52+
def apply(idx: Int): Char
53+
def concat(txt: Instance): Instance
54+
def toStr: String
55+
56+
common def fromString(str: String): Instance
57+
common def fromStrings(strs: String*): Instance =
58+
("" :: strs).map(fromString).reduceLeft(_.concat)
59+
}
60+
```
61+
62+
In traits that define or inherit `common` definitions, the `Instance` type refers to the (as yet unknown) instance type whose
63+
companion object implements the trait. To see why `Instance` is useful, consider another possible implementation of `Text`, implemented as a tree of strings. The advantage of the new implementation is that `concat` is constant time. Both old and new implementations share the definition of the `common` method `fromStrings`.
64+
65+
```scala
66+
enum ConcText extends Text {
67+
case Str(s: String)
68+
case Conc(t1: ConcText, t2: ConcText)
69+
70+
lazy val length = this match {
71+
case Str(s) => s.length
72+
case Conc(t1, t2) => t1.length + t2.length
73+
}
74+
75+
def apply(n: Int) = this match {
76+
case Str(s) => s.charAt(n)
77+
case Conc(t1, t2) => if (n < t1.length) t1(n) else t2(n - t1.length)
78+
}
79+
80+
def concat(txt: ConcText) = Conc(this, txt)
81+
82+
def toStr: String = this match {
83+
case Str(s) => s
84+
case Conc(t1, t2) => t1.toStr ++ t2.toStr
85+
}
86+
common def fromString(str: String): ConcText = Str(str)
87+
}
88+
```
89+
90+
The `concat` method of `ConcText` with type `(txt: ConcText): ConcText` is a valid implementation of the
91+
abstract method in `Text` of type `(txt: Instance): Instance` because `ConcText` is a class implementing `Text`
92+
which means that it fixes `Instance` to be `ConcText`.
93+
94+
Note: The `Instance` type is a useful abstraction for traits that are always implemented via `extends`. For type-class like traits that are intended to be implemented after the fact with extension clauses, there is another predefined type `This` that is generally more appropriate (more on `This` in the typeclass section).
95+
96+
## The `common` Reference
97+
98+
Let's add another method to `Text`:
99+
100+
trait Text {
101+
...
102+
def flatten: Instance = fromString(toStr)
103+
}
104+
105+
Why does this work? The `fromString` method is abstract in `Text` so how to we find the correct implementation in `flatten`?
106+
Comparing with the `toStr` reference, this one is an instance method and therefore is expanded to `this.toStr`. But the same does not work for `fromString` because it is a `common` method, not an instance method.
107+
In fact, the application above is syntactic sugar for
108+
109+
this.common.fromString(this.toStr)
110+
111+
The `common` selector is defined in each trait that defines or inherits `common` definitions. It refers at runtime to
112+
the object that implements the `common` definitions.
113+
114+
## Translation
115+
116+
The translation of a trait `T` that defines `common` declarations `common D1, ..., common Dn`
117+
and extends traits with common declarations `P1`, ..., Pn` is as follows:
118+
All `common` definitions are put in a trait `T.Common` which is defined in `T`'s companion object:
119+
120+
object T {
121+
trait Common extends P1.Common with ... with Pn.Common { self =>
122+
type Instance <: T { val `common`: self.type }
123+
D1
124+
...
125+
Dn
126+
}
127+
}
128+
129+
The trait inherits all `Common` traits associated with `T`'s parent traits. If no explicit definition of `Instance` is
130+
given, it declares the `Instance` type as shown above. The trait `T` itself is expanded as follows:
131+
132+
trait T extends ... {
133+
val `common`: `T.Common`
134+
import `common`._
135+
136+
...
137+
}
138+
139+
Any direct reference to `x.common` in the body of `T` is simply translated to
140+
141+
x.`common`
142+
143+
The translation of a class `C` that defines `common` declarations `common D1, ..., common Dn`
144+
and extends traits with common declarations `P1`, ..., Pn` is as follows:
145+
All `common` definitions of the class itself are placed in `C`'s companion object, which also inherits all
146+
`Common` traits of `C`'s parents. If `C` already defines a companion object, the synthesized parents
147+
come after the explicitly declared ones, whereas the common definitions precede all explicitly given statements of the
148+
companion object. The companion object also defines the `Instance` type as
149+
150+
type Instance = C
151+
152+
unless an explicit definition of `Instance` is given in the same object.
153+
154+
### Example:
155+
156+
As an example, here is the translation of trait `Text` and its two implementations `FlatText` and `ConcText`:
157+
158+
```scala
159+
trait Text {
160+
val `common`: Text.Common
161+
import `common`._
162+
163+
def length: Int
164+
def apply(idx: Int): Char
165+
def concat(txt: Instance): Instance
166+
def toStr: String
167+
def flatten = `common`.fromString(toStr)
168+
}
169+
object Text {
170+
trait Common { self =>
171+
type Instance <: Text { val `common`: self.type }
172+
def fromString(str: String): Instance
173+
def fromStrings(strs: String*): Instance =
174+
("" :: strs.toList).map(fromString).reduceLeft(_.concat(_))
175+
}
176+
}
177+
178+
class FlatText(str: String) extends Text {
179+
val `common`: FlatText.type = FlatText
180+
import `common`._
181+
182+
def length = str.length
183+
def apply(n: Int) = str.charAt(n)
184+
def concat(txt: FlatText) = new FlatText(str ++ txt.toStr)
185+
def toStr = str
186+
}
187+
object FlatText extends Text.Common {
188+
type Instance = FlatText
189+
def fromString(str: String) = new FlatText(str)
190+
}
191+
192+
enum ConcText extends Text {
193+
val `common`: ConcText.type = ConcText
194+
import `common`._
195+
196+
case Str(s: String)
197+
case Conc(t1: Text, t2: Text)
198+
199+
lazy val length = this match {
200+
case Str(s) => s.length
201+
case Conc(t1, t2) => t1.length + t2.length
202+
}
203+
204+
def apply(n: Int) = this match {
205+
case Str(s) => s.charAt(n)
206+
case Conc(t1, t2) => if (n < t1.length) t1(n) else t2(n - t1.length)
207+
}
208+
209+
def concat(txt: ConcText): ConcText = Conc(this, txt)
210+
211+
def toStr: String = this match {
212+
case Str(s) => s
213+
case Conc(t1, t2) => t1.toStr ++ t2.toStr
214+
}
215+
}
216+
217+
object ConcText extends Text.Common {
218+
type Instance = ConcText
219+
def fromString(str: String) = Str(str)
220+
}
221+
```
222+
223+
### Relationship with Parameterization
224+
225+
Common definitions do not see the type parameters of their enclosing class or trait. So the following is illegal:
226+
227+
```scala
228+
trait T[A] {
229+
common def f: T[A]
230+
}
231+
```
232+
233+
The implicit `Instance` declaration of a trait or class follows in its parameters the parameters of the
234+
trait or class. For instance:
235+
236+
```scala
237+
trait Sequence[+T <: AnyRef] {
238+
def map[U <: AnyRef](f: T => U): Instance[U]
239+
240+
common def empty[T]: Instance[T]
241+
}
242+
```
243+
The implicitly defined `Instance` declaration would be in this case:
244+
245+
```scala
246+
object Sequence {
247+
trait Common { self =>
248+
type Instance[+T <: AnyRef] <: Sequence[T] { val `common`: self.type }
249+
}
250+
}
251+
```
252+
253+
The rules for mixing `Instance` definitions of different kinds depend on the status of #4150. If #4150 is
254+
accepted, we permit `Instance` definitions to co-exist at different kinds. If #4150 is not accepted, we
255+
have to forbid this case, which means that a class must have the same parameter structure as all the traits
256+
with common members that it extends.

docs/sidebar.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ sidebar:
4343
url: docs/reference/multiversal-equality.html
4444
- title: Trait Parameters
4545
url: docs/reference/trait-parameters.html
46+
- title: Common Declarations
47+
url: docs/reference/common-declarations.html
4648
- title: Inline
4749
url: docs/reference/inline.html
4850
- title: Meta Programming

tests/pos/commons.scala

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/** Simple common definitions, no This type */
2+
trait Text {
3+
val `common`: Text.Common
4+
import `common`._
5+
6+
def length: Int
7+
def apply(idx: Int): Char
8+
def concat(txt: Text): Text
9+
def toStr: String
10+
}
11+
object Text {
12+
trait Common { self =>
13+
type Instance <: Text { val `common`: self.type }
14+
def fromString(str: String): Text
15+
def fromStrings(strs: String*): Text =
16+
("" :: strs.toList).map(fromString).reduceLeft(_.concat(_))
17+
}
18+
}
19+
20+
class FlatText(str: String) extends Text {
21+
val common: FlatText.type = FlatText
22+
def length = str.length
23+
def apply(n: Int) = str.charAt(n)
24+
def concat(txt: Text) = new FlatText(str ++ txt.toStr)
25+
def toStr = str
26+
}
27+
object FlatText extends Text.Common {
28+
type Instance = FlatText
29+
def fromString(str: String) = new FlatText(str)
30+
}
31+
32+
enum ConcText extends Text {
33+
val common: ConcText.type = ConcText
34+
35+
case Str(s: String)
36+
case Conc(t1: Text, t2: Text)
37+
38+
lazy val length = this match {
39+
case Str(s) => s.length
40+
case Conc(t1, t2) => t1.length + t2.length
41+
}
42+
43+
def apply(n: Int) = this match {
44+
case Str(s) => s.charAt(n)
45+
case Conc(t1, t2) => if (n < t1.length) t1(n) else t2(n - t1.length)
46+
}
47+
48+
def concat(txt: Text) = Conc(this, txt)
49+
50+
def toStr: String = this match {
51+
case Str(s) => s
52+
case Conc(t1, t2) => t1.toStr ++ t2.toStr
53+
}
54+
}
55+
56+
object ConcText extends Text.Common {
57+
type Instance = ConcText
58+
def fromString(str: String) = Str(str)
59+
}
60+
61+
object Test extends App {
62+
val txt1 = FlatText.fromStrings("hel", "lo")
63+
val txt2 = ConcText.fromString("world")
64+
println(txt2.concat(txt1))
65+
assert(txt1.concat(txt2).toStr == "helloworld")
66+
assert(txt2.concat(txt1).toStr == "worldhello")
67+
assert(txt1.concat(txt2)(5) == 'w')
68+
}

0 commit comments

Comments
 (0)