Skip to content

Commit 570ccf2

Browse files
committed
Explain possible typeclass trait extension
1 parent ca35cd6 commit 570ccf2

13 files changed

+794
-306
lines changed

docs/docs/reference/common-declarations.md

Lines changed: 0 additions & 256 deletions
This file was deleted.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
layout: doc-page
3+
title: "Common Declarations"
4+
---
5+
6+
Typeclass traits can have `common` declarations. These are a way to
7+
abstract over values that exist only once for each implementing type.
8+
At first glance, `common` declarations resemble `static` definitions in a language like Java,
9+
but they are more general since they can be inherited.
10+
11+
As an example, consider the following trait `Text` with an implementation class `FlatText`.
12+
13+
```scala
14+
trait Text extends TypeClass {
15+
def length: Int
16+
def apply(idx: Int): Char
17+
def concat(txt: This): This
18+
def toStr: String
19+
20+
common def fromString(str: String): This
21+
common def fromStrings(strs: String*): This =
22+
("" :: strs).map(fromString).reduceLeft(_.concat)
23+
}
24+
25+
case class FlatText(str: String) extends Text {
26+
def length = str.length
27+
def apply(n: Int) = str.charAt(n)
28+
def concat(txt: FlatText): FlatText = FlatText(str ++ txt.str)
29+
def toStr = str
30+
31+
common def fromString(str: String) = FlatText(str)
32+
}
33+
```
34+
35+
The `common` method `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:
36+
37+
```scala
38+
val txt1 = FlatText.fromString("hello")
39+
val txt2 = FlatText.fromStrings("hello", ", world")
40+
```
41+
42+
`common` declarations only define members of the companion objects of classes, not traits. So the following would give a "member not found" error.
43+
44+
```scala
45+
val erroneous = Text.fromStrings("hello", ", world") // error: not found
46+
```
47+
48+
The `common` definition of `fromString` in `FlatText` implicitly defines a member of
49+
`object FlatText`. Alternatively, one could have defined it as a regular method in the
50+
companion object. This is done in the following second implementation of `Text`, which
51+
represents a text as a tree of strings. Both old and new implementations share the definition of the `common` method `fromStrings` in `Text`.
52+
53+
```scala
54+
enum ConcText {
55+
56+
case Str(s: String)
57+
case Conc(t1: ConcText, t2: ConcText)
58+
59+
lazy val length = this match {
60+
case Str(s) => s.length
61+
case Conc(t1, t2) => t1.length + t2.length
62+
}
63+
64+
def apply(n: Int) = this match {
65+
case Str(s) => s.charAt(n)
66+
case Conc(t1, t2) => if (n < t1.length) t1(n) else t2(n - t1.length)
67+
}
68+
69+
def concat(txt: Text) = Conc(this, txt)
70+
71+
def toStr: String = this match {
72+
case Str(s) => s
73+
case Conc(t1, t2) => t1.toStr ++ t2.toStr
74+
}
75+
}
76+
object ConcText {
77+
def fromString(str: String): ConcText = Str(str)
78+
}
79+
```
80+
81+
## The `common` Reference
82+
83+
Let's add another method to `Text`:
84+
```scala
85+
trait Text {
86+
...
87+
def flatten: Instance = fromString(toStr)
88+
}
89+
```
90+
Why does this work? The `fromString` method is abstract in `Text` so how to we find the correct implementation in `flatten`?
91+
Comparing with the `toStr` reference, that 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.
92+
In fact, the application above is syntactic sugar for
93+
94+
```scala
95+
this.common.fromString(this.toStr)
96+
```
97+
The `common` selector is defined in each typeclass trait. It refers at runtime to
98+
the object that implements the `common` declarations.
99+
100+
### Relationship with Parameterization
101+
102+
There are special rules for common declarations in parameterized traits or classes. Parameterized traits and classes can both contain common declarations, but they have different visibility rules. Common declarations in a trait do _not_ see the type parameters of the enclosing trait. So the following is illegal:
103+
104+
```scala
105+
trait T[A] {
106+
/*!*/ common def f: T[A] // error: not found: A
107+
}
108+
```
109+
110+
On the other hand, common definitions in a class or an extension clause _can_ refer to the type parameters of that class. Consequently, actual type arguments have to be specified when accessing such a common member. Example:
111+
112+
```scala
113+
extension SetMonoid[T] for Set[T] : Monoid {
114+
def add(that: Set[T]) = this ++ that
115+
common def unit: Set[T] = Set()
116+
}
117+
118+
Set[Int].unit
119+
Set[Set[String]].unit
120+
```
121+
122+
**Note:** Common definitions in a parameterized class `C[T]` cannot be members of the companion object of `C` because that would lose the visibility of the type parameter `T`. Instead they are members of a separate class that is the result type of an `apply` method
123+
in the companion object. For instance, the `SetMonoid` extension above would be expanded
124+
along the following lines:
125+
126+
```scala
127+
object SetMonoid {
128+
def apply[T] = new Monoid.Common {
129+
def unit: Set[T] = Set()
130+
}
131+
}
132+
```
133+
Then, as always, `Set[Int].unit` expands to `Set.apply[Int].unit`.

0 commit comments

Comments
 (0)