Skip to content

Commit 320fb0e

Browse files
authored
Merge pull request #1487 from Atry/curried
SIP-NN - Curried varargs
2 parents e5a0495 + ef93707 commit 320fb0e

File tree

1 file changed

+269
-0
lines changed

1 file changed

+269
-0
lines changed
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
---
2+
layout: sips
3+
discourse: true
4+
title: SIP-NN - Curried varargs
5+
---
6+
7+
**By: Yang, Bo**
8+
9+
## History
10+
11+
| Date | Version |
12+
|---------------|---------------|
13+
| Aug 11th 2019 | Initial Draft |
14+
| Aug 12th 2019 | Translating sequence arguments to `applyNextSeq` calls |
15+
16+
## Introduction
17+
18+
The [repeated parameters](https://scala-lang.org/files/archive/spec/2.13/04-basic-declarations-and-definitions.html#repeated-parameters) syntax is widely used in Scala libraries to create collection initializers, string interpolations, and DSLs. Unfortunately, repeated parameters are type unsafe as it erase all arguments to their common supertype, inefficient as it creates a temporary `Seq` that is difficult to be eliminated by optimizer. In practice, all sophisticated string interpolation libraries, including [string formatting](https://github.com/scala/scala/blob/43e040ff7e4ba92ccf223e77540580b32c1473c0/src/library/scala/StringContext.scala#L94) and [quasiquotes](https://github.com/scala/scala/blob/43e040ff7e4ba92ccf223e77540580b32c1473c0/src/reflect/scala/reflect/api/Quasiquotes.scala#L28) in standard library, [scalameta](https://scalameta.org/docs/trees/quasiquotes.html) and my [fastring](https://github.com/Atry/fastring/blob/67ae4eccdb9b7f58416ed90eae85ddb035b1ffb1/shared/src/main/scala/com/dongxiguo/fastring/Fastring.scala#L242) library, are written in macros in order to avoid runtime overhead of repeated parameters.
19+
20+
We propose **curried varargs** to improve both the type safety and the performance. Given a function call `f(a, b, c)`, when `f` is a subtype of `Curried`, the function call should be rewritten to `f.applyBegin.applyNext(a).applyNext(b).applyNext(c).applyEnd`.
21+
22+
## Motivating Examples
23+
24+
### Examples
25+
26+
Recently I was working on the implementation of [Pre SIP: name based XML literals](https://contributors.scala-lang.org/t/pre-sip-name-based-xml-literals/2175). During implementing that proposal, I found that the proposal is inefficiency due to repeated parameters, and it could be improved dramatically with the help of curried functions.
27+
28+
For example, according to the proposal the XML literal `<div title="my-title">line1<br/>line2</div>` will result the following code:
29+
30+
``` scala
31+
xml.tags.div(
32+
xml.attributes.title(xml.text("my-title")),
33+
xml.text("line1"),
34+
xml.tags.br(),
35+
xml.text("line2")
36+
)
37+
```
38+
39+
With the help of this curried varargs proposal and `@inline`, we are able to implement an API to build a DOM tree with no additional overhead over manually written Scala code.
40+
41+
``` scala
42+
import org.scalajs.dom.document
43+
import org.scalajs.dom.raw._
44+
45+
object xml {
46+
type Attribute[-A <: Element] = A => Unit
47+
@inline def text(data: String) = data
48+
object attributes {
49+
@inline def title(value: String): Attribute[Element] = _.setAttribute("title", value)
50+
}
51+
object tags {
52+
class Builder[+E <: Element](private val element: E) extends AnyVal with Curried {
53+
@inline def applyBegin = this
54+
@inline def applyNext(text: String) = {
55+
element.appendChild(document.createTextNode(text))
56+
this
57+
}
58+
@inline def applyNext(node: Node) = {
59+
element.appendChild(node)
60+
this
61+
}
62+
@inline def applyNext[A <: Attribute[E]](attribute: A) = {
63+
attribute(element)
64+
this
65+
}
66+
@inline def applyEnd = element
67+
}
68+
@inline def div = new Builder(document.createElement("div"))
69+
@inline def br = new Builder(document.createElement("br"))
70+
}
71+
}
72+
```
73+
74+
Since `xml.tags.div` returns a `Builder`, which is a subtype of `Curried`, calls on `xml.tags.div` will be translated to the curried form, as shown below:
75+
``` scala
76+
xml.tags.div
77+
.applyBegin
78+
.applyNext(xml.attributes.title(xml.text("my-title")))
79+
.applyNext(xml.text("line1"))
80+
.applyNext(xml.tags.br.applyBegin.applyEnd)
81+
.applyNext(xml.text("line2"))
82+
.applyEnd
83+
```
84+
85+
When the above code is compiled in Scala.js, the builders should be eliminated entirely as a zero cost abstraction layer, and the output JavaScript is tiny as shown below:
86+
87+
``` javascript
88+
var $$this = $m_Lorg_scalajs_dom_package$().document__Lorg_scalajs_dom_raw_HTMLDocument().createElement("div");
89+
$$this.setAttribute("title", "my-title");
90+
$$this.appendChild($m_Lorg_scalajs_dom_package$().document__Lorg_scalajs_dom_raw_HTMLDocument().createTextNode("line1"));
91+
var $$this$1 = $m_Lorg_scalajs_dom_package$().document__Lorg_scalajs_dom_raw_HTMLDocument().createElement("br");
92+
$$this.appendChild($$this$1);
93+
$$this.appendChild($m_Lorg_scalajs_dom_package$().document__Lorg_scalajs_dom_raw_HTMLDocument().createTextNode("line2"));
94+
```
95+
96+
### Comparison Examples
97+
98+
The `Builder` API can be also implemented in repeated parameters:
99+
100+
``` scala
101+
import org.scalajs.dom.document
102+
import org.scalajs.dom.raw._
103+
104+
object xml {
105+
type Attribute[-A <: Element] = A => Unit
106+
@inline def text(data: String) = data
107+
object attributes {
108+
@inline def title(value: String): Attribute[Element] = _.setAttribute("title", value)
109+
}
110+
object tags {
111+
class Builder[+E <: Element](private val element: E) extends AnyVal {
112+
@inline def apply(attributesAndChildren: Any*) = {
113+
attributesAndChildren.foreach {
114+
case text: String =>
115+
element.appendChild(document.createTextNode(text))
116+
case node: Node =>
117+
element.appendChild(node)
118+
case attribute: Attribute[E] =>
119+
attribute(element)
120+
}
121+
element
122+
}
123+
}
124+
@inline def div = new Builder(document.createElement("div"))
125+
@inline def br = new Builder(document.createElement("br"))
126+
}
127+
}
128+
```
129+
130+
However, the Scala compiler is unable to optimize repeated parameters, as a result, the output JavaScript from Scala.js would look like the below code.
131+
132+
``` javascript
133+
var $$this$1 = $m_Lorg_scalajs_dom_package$().document__Lorg_scalajs_dom_raw_HTMLDocument().createElement("div");
134+
var this$3 = $m_LScalaFiddle$xml$attributes$();
135+
var jsx$1 = new $c_sjsr_AnonFunction1().init___sjs_js_Function1((function($this, value) {
136+
return (function(x$1$2) {
137+
x$1$2.setAttribute("title", value)
138+
})
139+
})(this$3, "my-title"));
140+
var $$this = $m_Lorg_scalajs_dom_package$().document__Lorg_scalajs_dom_raw_HTMLDocument().createElement("br");
141+
var array = [jsx$1, "line1", $$this, "line2"];
142+
var i = 0;
143+
var len = $uI(array.length);
144+
while ((i < len)) {
145+
var index = i;
146+
var arg1 = array[index];
147+
if ($is_T(arg1)) {
148+
var x2 = $as_T(arg1);
149+
$$this$1.appendChild($m_Lorg_scalajs_dom_package$().document__Lorg_scalajs_dom_raw_HTMLDocument().createTextNode(x2))
150+
} else if ($uZ((arg1 instanceof $g.Node))) {
151+
$$this$1.appendChild(arg1)
152+
} else if ($is_F1(arg1)) {
153+
var x4 = $as_F1(arg1);
154+
x4.apply__O__O($$this$1)
155+
} else {
156+
throw new $c_s_MatchError().init___O(arg1)
157+
};
158+
i = ((1 + i) | 0)
159+
};
160+
```
161+
162+
Despite of the type safety issue due to the usage of `Any`, the above code are inefficient:
163+
164+
1. Unnecessary temporary object for the `xml.attributes.title(xml.text("my-title"))`.
165+
2. Unnecessary temporary `Seq` to hold repeated parameters.
166+
3. Unnecessary runtime type check for each argument.
167+
168+
The similar issues can be found in many other usage of repeated parameters. For example, Scala string interpolation is inefficient due to its internal vararg function call, unless implementing it in a macro; Scala collection initializers (e.g. `List(1, 2, 3)`) create unnecessary temporary `Seq` before creating the desired collection.
169+
170+
## Design
171+
172+
This proposal introduces a new type `Curried` defined as following:
173+
174+
``` scala
175+
trait Curried extends Any
176+
```
177+
178+
When a function call `f(p1, p2, p3, ... pn)` is being type checked, the compiler will firstly look for `apply` method on `f`. If an applicable `apply` method is not found and `f` is a subtype of `Curried`, the compiler will convert the function call to curried form `f.applyBegin.applyNext(p1).applyNext(p2).applyNext(p3) ... .applyNext(pn).applyEnd`, and continue type checking the translated call.
179+
180+
### Expanding sequence argument
181+
182+
Optionally, some arguments to a `Curried` call may be a sequence argument marked as `_*`. Those are arguments should be translated to `applyNextSeq` calls instead of `applyNext`. For example, `f(p1, s1: _*, p2)` will be translated to the following code.
183+
184+
``` scala
185+
f.applyBegin
186+
.applyNext(p1)
187+
.applyNextSeq(s1)
188+
.applyNext(p2)
189+
.applyEnd
190+
```
191+
192+
Unlike traditional repeated parameters, which restrict the sequence argument at the last position, sequence arguments in a curried call are allowed at any position.
193+
194+
### Builder type shifting
195+
196+
The type of partially applied function might be changed during applying each argument. Given the following type signature:
197+
198+
``` scala
199+
class ListBuilder[A] {
200+
def applyNext[B >: A](b: B): ListBuilder[B] = ???
201+
def applyNextSeq[B >: A](seqB: Seq[B]): ListBuilder[B] = ???
202+
def applyEnd: List[A] = ???
203+
}
204+
object List extends Curried {
205+
def applyBegin[A]: ListBuilder[A] = ???
206+
}
207+
```
208+
209+
`List(42, "a")` should be translated to `List.applyBegin.applyNext(42).applyNext("a").applyEnd`. Then, the typer will infer type parameters as `List.applyBegin[Nothing].applyNext[Int](42).applyNext[Any]("a").applyEnd`, therefore the final return type of `applyEnd` will be `List[Any]`.
210+
211+
### Explicit type parameters
212+
213+
When a `Curried` is invoked with some type arguments, those type arguments will be moved to the `applyBegin` method. Therefore, `List[Int](1 to 3: _*)` should be translated to `List.applyBegin[Int].applyNextSeq(1 to 3).applyEnd`.
214+
215+
### Implicit parameters
216+
217+
A more common form of curried function call would be like `f(a)(b)(c)`. We prefer the explicit named method calls to `applyNext` instead of the common form, in order to support implicit parameters in `applyNext`. Therefore, each explicit parameter might come with an implicit parameter list, resolving the infamous [multiple type parameter lists](https://github.com/scala/bug/issues/4719) issue.
218+
219+
### Multiple curried vararg parameter lists
220+
221+
When a `Curried` is invoked with multiple parameter lists, for example:
222+
``` scala
223+
f(a, b, c)(d, e)
224+
```
225+
226+
Then the first parameter list should be translated to a curried call:
227+
228+
``` scala
229+
f.applyBegin
230+
.applyNext(a)
231+
.applyNext(b)
232+
.applyNext(c)
233+
.applyEnd(d, e)
234+
```
235+
236+
`(d, e)` is translated to the curried form only if `applyEnd` returns a `Curried`.
237+
238+
### Overloaded curried calls
239+
240+
Curried varargs enables overloaded functions for each parameter. Parameters will not be erased to their common supertype.
241+
242+
## Implementation
243+
244+
This proposal can be implemented either in the Scala compiler or in a whitebox macro. [Curried.scala](https://github.com/Atry/Curried.scala) is an implementation of the proposal in a whitebox macro.
245+
246+
## Alternatives
247+
248+
### Repeated parameters
249+
250+
Repeated parameters are packed into a `Seq`, which is then passed to the callee.
251+
252+
#### Pros
253+
254+
* Interoperable with Java
255+
256+
#### Cons
257+
258+
* Always boxing value class parameters
259+
* Unable to inline function parameters
260+
* Unable to inline call-by-name parameters
261+
* Unable to perform implicit conversion for each parameter
262+
* Unable to infer context bound for each parameter
263+
* Erasing all parameters to their common super type
264+
265+
## Reference
266+
* [Existing Implementation (Curried.scala)](https://github.com/Atry/Curried.scala)
267+
* [Discussion on Scala Contributors forum](https://contributors.scala-lang.org/t/pre-sip-curried-varargs/3608)
268+
* [Pre SIP: name based XML literals](https://contributors.scala-lang.org/t/pre-sip-name-based-xml-literals/2175)
269+
* [Scala Language Specification - Repeated Parameters](https://scala-lang.org/files/archive/spec/2.13/04-basic-declarations-and-definitions.html#repeated-parameters)

0 commit comments

Comments
 (0)