Skip to content

Commit d77ce94

Browse files
authored
Merge pull request #674 from soronpo/master
SIP-NN - Match infix & prefix types to meet expression rules
2 parents 3ec289f + 9d24b3c commit d77ce94

File tree

1 file changed

+189
-0
lines changed

1 file changed

+189
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
---
2+
layout: sip
3+
disqus: true
4+
title: SIP-NN - Match infix & prefix types to meet expression rules
5+
---
6+
7+
**By: Oron Port**
8+
9+
## History
10+
11+
| Date | Version |
12+
|---------------|--------------------------|
13+
| Feb 7th 2017 | Initial Draft |
14+
| Feb 9th 2017 | Updates from feedback |
15+
| Feb 10th 2017 | Updates from feedback |
16+
17+
Your feedback is welcome! If you're interested in discussing this proposal, head over to [this](https://contributors.scala-lang.org/t/sip-nn-make-infix-type-alias-precedence-like-expression-operator-precedence/471) Scala Contributors thread and let me know what you think.
18+
19+
---
20+
## Introduction
21+
Currently scala allows symbol operators (`-`, `*`, `~~>`, etc.) for both type names and definition names.
22+
Unfortunately, there is a 'surprise' element since the two differ in behaviour:
23+
24+
###Infix operator precedence and associativity
25+
Infix types are 'mostly' left-associative,
26+
while the expression operation precedence is determined by the operator's first character (e.g., `/` is precedent to `+`).
27+
Please see [Infix Types](http://scala-lang.org/files/archive/spec/2.12/03-types.html#infix-types) and [Infix Operations](http://scala-lang.org/files/archive/spec/2.12/06-expressions.html#infix-operations) sections of the Scala specifications for more details.
28+
29+
**Example**:
30+
```scala
31+
object InfixExpressionPrecedence {
32+
case class Nummy(expand : String) {
33+
def + (that : Nummy) : Nummy = Nummy(s"Plus[$this,$that]")
34+
def / (that : Nummy) : Nummy = Nummy(s"Div[$this,$that]")
35+
}
36+
object N1 extends Nummy("N1")
37+
object N2 extends Nummy("N2")
38+
object N3 extends Nummy("N3")
39+
object N4 extends Nummy("N4")
40+
//Both expand to Plus[Plus[N1,Div[N2,N3]],N4]
41+
assert((N1 + N2 / N3 + N4).expand == (N1 + (N2 / N3) + N4).expand)
42+
}
43+
object InfixTypePrecedence {
44+
trait Plus[N1, N2]
45+
trait Div[N1, N2]
46+
type +[N1, N2] = Plus[N1, N2]
47+
type /[N1, N2] = Div[N1, N2]
48+
trait N1
49+
trait N2
50+
trait N3
51+
trait N4
52+
//Error!
53+
//Left expands to Plus[Plus[N1,Div[N2,N3]],N4] (Surprising)
54+
//Right expands to Plus[Div[Plus[N1,N2],N3],N4]
55+
implicitly[(N1 + N2 / N3 + N4) =:= (N1 + (N2 / N3) + N4)]
56+
}
57+
```
58+
59+
###Prefix operators bracketless unary use
60+
While expressions have prefix unary operators, there are none for types. See the [Prefix Operations](http://scala-lang.org/files/archive/spec/2.12/06-expressions.html#prefix-operations) section of the Scala specification.
61+
This is a lacking feature of the type language Scala offers. See also interactions of this feature with other Scala features, further down this text.
62+
63+
64+
**Example**:
65+
```scala
66+
object PrefixExpression {
67+
case class Nummy(expand : String) {
68+
def unary_- : Nummy = Nummy(s"-$this")
69+
def unary_~ : Nummy = Nummy(s"~$this")
70+
def unary_! : Nummy = Nummy(s"!$this")
71+
def unary_+ : Nummy = Nummy(s"+$this")
72+
}
73+
object N extends Nummy("N")
74+
val n1 = -N
75+
val n2 = ~N
76+
val n3 = !N
77+
val n4 = +N
78+
}
79+
object NonExistingPrefixTypes {
80+
trait unary_-[A]
81+
trait unary_~[A]
82+
trait unary_![A]
83+
trait unary_+[A]
84+
trait N
85+
type N1 = -N //Not working
86+
type N2 = ~N //Not working
87+
type N3 = !N //Not working
88+
type N4 = +N //Not working
89+
}
90+
```
91+
92+
---
93+
## Proposal
94+
The proposal is split into two; type infix precedence, and prefix unary types. Note to the SIP committee: It might be better to vote on the two parts separately.
95+
96+
### Proposal, Part 1: Infix type precedence & associativity
97+
Make infix types conform to the same precedence and associativity traits as expression operations.
98+
### Proposal, Part 2: Prefix unary types
99+
Add prefix types, exactly as specified for prefix expression.
100+
101+
102+
---
103+
## Motivation
104+
The general motivation is developers expect terms and types to behave equally regarding operation precedence and availability of unary types.
105+
106+
### Motivating examples
107+
#### Dotty infix type similarity
108+
Dotty infix type associativity and precedence seem to act the same as expressions.
109+
No documentation available to prove this, but the infix example above works perfectly in dotty.
110+
111+
Dotty has no prefix types, same as Scalac.
112+
113+
#### Singleton-ops library example
114+
The [singleton-ops library](https://github.com/fthomas/singleton-ops) with [Typelevel Scala](https://github.com/typelevel/scala) (which implemented [SIP-23](http://docs.scala-lang.org/sips/pending/42.type.html)) enables developers to express literal type operations more intuitively.
115+
For example:
116+
```scala
117+
import singleton.ops._
118+
119+
val four1 : 4 = implicitly[2 + 2]
120+
val four2 : 2 + 2 = 4
121+
val four3 : 1 + 3 = implicitly[2 + 2]
122+
123+
class MyVec[L] {
124+
def doubleSize = new MyVec[2 * L]
125+
def nSize[N] = new MyVec[N * L]
126+
}
127+
object MyVec {
128+
implicit def apply[L](implicit check : Require[L > 0]) : MyVec[L] = new MyVec[L]()
129+
}
130+
val myVec : MyVec[10] = MyVec[4 + 1].doubleSize
131+
val myBadVec = MyVec[-1] //fails compilation, as required
132+
```
133+
We currently loose some of the intuitive appeal due to the precedence issue:
134+
```scala
135+
val works : 1 + (2 * 3) + 4 = 11
136+
val fails : 1 + 2 * 3 + 4 = 11 //left associative:(((1+2)*3)+4))) = 13
137+
```
138+
139+
#### Developer issues example
140+
The following stackoverflow question demonstrate developers are 'surprised' by the difference in infix precedence, expecting infix type precedence to act the same as expression operations.
141+
http://stackoverflow.com/questions/23333882/scala-infix-type-aliasing-for-2-type-parameters
142+
143+
144+
145+
## Interactions with other language features
146+
147+
#### Variance Annotation
148+
Variance annotation uses the `-` and `+` symbols to annotate contravariant and covariant subtyping, respectively. Introducing unary prefix types may lead to some developer confusion.
149+
E.g.
150+
```scala
151+
trait Negate[A]
152+
trait Positive[A]
153+
type unary_-[A] = Negate[A]
154+
type unary_+[A] = Positive[A]
155+
trait Contravariant[B, -A <: -B] //contravariant A subtype upper-bounded by Negate[B]
156+
trait Covariant[B, +A <: +B] //covariant A subtype upper-bounded by Positive[B]
157+
```
158+
159+
#### Negative Literal Types
160+
Negative literal types are annotated using the `-` symbol. This can lead to the following confusion:
161+
```scala
162+
trait Negate[A]
163+
type unary_-[A] = Negate[A]
164+
trait MyTrait[B]
165+
166+
type MinusFortyTwo = MyTrait[-42]
167+
type NegateFortyTwo = MyTrait[Negate[42]]
168+
```
169+
The above example demonstrates a case of two types `MinusFortyTwo` and `NegateFortyTwo` which are different. They may be equivalent in view (implicit conversion between the two type instances), but they are not equal.
170+
171+
Note: It is not possible to annotate a positive literal type in Scala (checked both in TLS and Dotty):
172+
```scala
173+
val a : 42 = +42 //works
174+
val b : -42 = -42 //works
175+
val c : +42 = 42 //error: ';' expected but integer literal found
176+
```
177+
This means that if unary prefix types are added, then `+42` will be a type expansion of `unary_+[42]`.
178+
179+
---
180+
181+
## Backward Compatibility
182+
Changing infix type associativity and precedence affects code that uses type operations and conforms to the current specification.
183+
184+
---
185+
186+
### Bibliography
187+
[Scala Contributors](https://contributors.scala-lang.org/t/sip-nn-make-infix-type-alias-precedence-like-expression-operator-precedence/471)
188+
189+
[scala-sips](https://groups.google.com/forum/#!topic/scala-sips/ARVf1RLDw9U)

0 commit comments

Comments
 (0)