Skip to content

Commit 44643f4

Browse files
Ichoranretronym
authored andcommitted
Tests, manually written, for function converters.
These were written manually to avoid making the same assumptions in code generation for the converters and the tests, and thereby missing important bugs. As a downside, though, they do not have complete code coverage. This should not matter given how the code generator works, but it is a weak point.
1 parent de7d42c commit 44643f4

File tree

3 files changed

+959
-53
lines changed

3 files changed

+959
-53
lines changed

README.md

+65-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,69 @@ class Test {
2525

2626
[More Examples / Documentation](src/test/java/scala/compat/java8/LambdaTest.java)
2727

28+
## Converters between `scala.FunctionN` and `java.util.function`
29+
30+
A set of converters that enable interconversion between Java's standard
31+
Functional Interfaces defined in `java.util.function` and Scala's `Function0`,
32+
`Function1`, and `Function2` traits. These are intended for use when you
33+
already have an instance of a `java.util.function` and need a Scala function,
34+
or have a Scala function and need an instance of a `java.util.function`.
35+
36+
The `.asScala` extension method will convert a `java.util.function` to the corresponding
37+
Scala function. The `.asJava` extension method will convert a Scala function to
38+
the most specific corresponding Java functional interface. If you wish to obtain
39+
a less specific functional interface, there are named methods that start with `asJava`
40+
and continue with the name of the Java functional interface. For instance, the
41+
most specific interface corresponding to the Scala function `val rev = (s: String) => s.reverse`
42+
is `UnaryOperator[String]`, and that is what `rev.asJava` will produce. However,
43+
`asJavaFunction(rev)` will return a `java.util.function.Function[String, String]` instead.
44+
45+
The `asJava` methods can also be called conveniently from Java. There are additional
46+
`asScalaFrom` methods (e.g. `asScalaFromUnaryOperator`) that will perform the
47+
functional-interface-to-Scala-function conversion; this is primarily of use when calling
48+
from Java since the `.asScala` extension method is more convenient in Scala.
49+
50+
#### Usage examples
51+
52+
In Scala:
53+
54+
```scala
55+
import java.util.function._
56+
import scala.compat.java8.FunctionConverters._
57+
58+
val foo: Int => Boolean = i => i > 7
59+
def testBig(ip: IntPredicate) = ip.test(9)
60+
println(testBig(foo.asJava)) // Prints true
61+
62+
val bar = new UnaryOperator[String]{ def apply(s: String) = s.reverse }
63+
List("cod", "herring").map(bar.asScala) // List("doc", "gnirrih")
64+
65+
def testA[A](p: Predicate[A])(a: A) = p.test(a)
66+
println(testA(asJavaPredicate(foo))(4)) // Prints false
67+
68+
// println(testA(foo.asJava)(4)) <-- doesn't work
69+
// IntPredicate does not extend Predicate!
70+
```
71+
72+
In Java:
73+
74+
```java
75+
import java.util.function.*;
76+
import scala.compat.java8.FunctionConverters;
77+
78+
class Example {
79+
String foo(UnaryOperator<String> f) {
80+
return f.apply("halibut");
81+
}
82+
String bar(scala.Function1<String, String> f) {
83+
return foo(functionConverters.asJavaUnaryOperator(f));
84+
}
85+
String baz(Function<String, String> f) {
86+
return bar(functionConverters.asScalaFromFunction(f));
87+
}
88+
}
89+
```
90+
2891
## Converters between `scala.concurrent` and `java.util.concurrent`
2992

3093
- [API](src/main/scala/scala/compat/java8/FutureConverters.scala)
@@ -53,6 +116,7 @@ class Test {
53116
}
54117
```
55118

119+
56120
## Future work
57-
- Converters for `java.util.function`, `java.util.stream`
121+
- Converters for `java.util.stream`
58122
- [`Spliterator`](https://docs.oracle.com/javase/8/docs/api/java/util/Spliterator.html)s for Scala collections

fnGen/WrapFnGen.scala

+60-52
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ object WrapFnGen {
1010
| * This file auto-generated by WrapFnGen.scala. Do not modify directly.
1111
| */
1212
|""".stripMargin
13-
13+
1414
val packaging = "package scala.compat.java8"
15-
15+
1616
import scala.tools.nsc._
1717
import scala.reflect.internal._
1818
val settings = new Settings(msg => sys.error(msg))
@@ -21,12 +21,12 @@ object WrapFnGen {
2121
val run = new compiler.Run
2222

2323
import compiler._, definitions._
24-
25-
24+
25+
2626
implicit class IndentMe(v: Vector[String]) {
2727
def indent: Vector[String] = v.map(" " + _)
2828
}
29-
29+
3030
implicit class FlattenMe(v: Vector[Vector[String]]) {
3131
def mkVec(join: String = ""): Vector[String] = {
3232
val vb = Vector.newBuilder[String]
@@ -39,7 +39,7 @@ object WrapFnGen {
3939
vb.result()
4040
}
4141
}
42-
42+
4343
implicit class DoubleFlattenMe(v: Vector[Vector[Vector[String]]]) {
4444
def mkVecVec(join: String = ""): Vector[String] = {
4545
val vb = Vector.newBuilder[String]
@@ -57,39 +57,39 @@ object WrapFnGen {
5757
vb.result()
5858
}
5959
}
60-
60+
6161
implicit class SplitMyLinesAndStuff(s: String) {
6262
def toVec = s.linesIterator.toVector
6363
def nonBlank = s.trim.length > 0
6464
}
65-
65+
6666
implicit class TreeToText(t: Tree) {
6767
def text = showCode(t).replace("$", "").linesIterator.toVector
6868
}
69-
69+
7070
case class Prioritized(lines: Vector[String], priority: Int) {
7171
def withPriority(i: Int) = copy(priority = i)
7272
}
73-
73+
7474
case class SamConversionCode(
7575
base: String,
7676
wrappedAsScala: Vector[String],
77+
asScalaAnyVal: Vector[String],
7778
implicitToScala: Vector[String],
7879
asScalaDef: Vector[String],
7980
wrappedAsJava: Vector[String],
8081
asJavaAnyVal: Vector[String],
8182
implicitToJava: Prioritized,
8283
asJavaDef: Vector[String]
8384
) {
84-
def impls: Vector[Vector[String]] = Vector(wrappedAsScala, wrappedAsJava, asJavaAnyVal)
85+
def impls: Vector[Vector[String]] = Vector(wrappedAsScala, asScalaAnyVal, wrappedAsJava, asJavaAnyVal)
8586
def defs: Vector[Vector[String]] = Vector(asScalaDef, asJavaDef)
86-
def convs: Vector[Vector[String]] = Vector(implicitToScala, implicitToJava.lines)
8787
def withPriority(i: Int): SamConversionCode = copy(implicitToJava = implicitToJava.withPriority(i))
8888
}
8989
object SamConversionCode {
9090
def apply(scc: SamConversionCode*): (Vector[String], Vector[Vector[String]]) = {
9191
val sccDepthSet = scc.map(_.implicitToJava.priority).toSet
92-
val codes =
92+
val codes =
9393
{
9494
if (sccDepthSet != (0 to sccDepthSet.max).toSet) {
9595
val sccDepthMap = sccDepthSet.toList.sorted.zipWithIndex.toMap
@@ -98,43 +98,46 @@ object WrapFnGen {
9898
else scc
9999
}.toVector.sortBy(_.base)
100100
def priorityName(n: Int, pure: Boolean = false): String = {
101-
val pre =
101+
val pre =
102102
if (n <= 0)
103-
if (pure) "functionConverters"
103+
if (pure) "FunctionConverters"
104104
else s"package object ${priorityName(n, pure = true)}"
105105
else
106106
if (pure) s"Priority${n}FunctionConverters"
107107
else s"trait ${priorityName(n, pure = true)}"
108-
if (!pure && n < sccDepthSet.size) s"$pre extends ${priorityName(n+1, pure = true)}" else pre
108+
if (!pure && n < (sccDepthSet.size-1)) s"$pre extends ${priorityName(n+1, pure = true)}" else pre
109109
}
110-
val impls =
110+
val impls =
111111
"package functionConverterImpls {" +: {
112112
codes.map(_.impls).mkVecVec().indent
113113
} :+ "}"
114114
val traits = codes.filter(_.implicitToJava.priority > 0).groupBy(_.implicitToJava.priority).toVector.sortBy(- _._1).map{ case (k,vs) =>
115-
s"trait Priority${k}FunctionConverters {" +:
115+
s"${priorityName(k)} {" +:
116116
s" import functionConverterImpls._" +:
117117
s" " +:
118118
vs.map(_.implicitToJava.lines).mkVec().indent :+
119119
s"}"
120120
}
121121
val explicitDefs = codes.map(_.defs).mkVecVec()
122122
val packageObj =
123-
s"${priorityName(0)} {" +:
123+
s"${priorityName(0)} {" +:
124124
s" import functionConverterImpls._" +:
125125
s" " +:
126126
{
127127
explicitDefs.indent ++
128-
codes.filter(_.implicitToJava.priority == 0).map(_.convs).mkVecVec().indent
128+
Vector.fill(3)(" ") ++
129+
codes.filter(_.implicitToJava.priority == 0).map(_.implicitToJava.lines).mkVec().indent ++
130+
Vector.fill(3)(" ") ++
131+
codes.map(_.implicitToScala).mkVec().indent
129132
} :+ "}"
130133
(impls, traits :+ packageObj)
131134
}
132135
}
133-
136+
134137
private def buildWrappersViaReflection: Seq[SamConversionCode] = {
135138

136139
val pack: Symbol = rootMirror.getPackageIfDefined(TermName("java.util.function"))
137-
140+
138141
case class Jfn(iface: Symbol, sam: Symbol) {
139142
lazy val genericCount = iface.typeParams.length
140143
lazy val name = sam.name.toTermName
@@ -145,77 +148,81 @@ object WrapFnGen {
145148
lazy val rType = sig.resultType
146149
def arity = params.length
147150
}
148-
151+
149152
val sams = pack.info.decls.
150153
map(d => (d, d.typeSignature.members.filter(_.isAbstract).toList)).
151154
collect{ case (d, m :: Nil) if d.isAbstract => Jfn(d, m) }
152-
155+
153156
def generate(jfn: Jfn): SamConversionCode = {
154157
def mkRef(tp: Type): Tree = if (tp.typeSymbol.isTypeParameter) Ident(tp.typeSymbol.name.toTypeName) else tq"$tp"
155-
158+
156159
// Types for the Java SAM and the corresponding Scala function, plus all type parameters
157160
val scalaType = gen.mkAttributedRef(FunctionClass(jfn.arity))
158161
val javaType = gen.mkAttributedRef(jfn.iface)
159162
val tnParams: List[TypeName] = jfn.iface.typeParams.map(_.name.toTypeName)
160163
val tdParams: List[TypeDef] = tnParams.map(TypeDef(NoMods, _, Nil, EmptyTree))
161164
val javaTargs: List[Tree] = tdParams.map(_.name).map(Ident(_))
162165
val scalaTargs: List[Tree] = jfn.pTypes.map(mkRef) :+ mkRef(jfn.rType)
163-
166+
164167
// Conversion wrappers have three or four components that we need to name
165168
// (1) The wrapper class that wraps a Java SAM as Scala function, or vice versa (ClassN)
166169
// (2) A value class that provides .asJava or .asScala to request the conversion (ValCN)
167170
// (3) A name for an explicit conversion method (DefN)
168-
// (4) If nested-trait lookup is needed to pick types, an implicit conversion method name (ImpN)
169-
171+
// (4) An implicit conversion method name (ImpN) that invokes the value class
172+
170173
// Names for Java conversions to Scala
171174
val j2sClassN = TypeName("FromJava" + jfn.title)
172175
val j2sValCN = TypeName("Rich" + jfn.title + "As" + scalaType.name.encoded)
173176
val j2sDefN = TermName("asScalaFrom" + jfn.title)
174-
177+
val j2sImpN = TermName("enrichAsScalaFrom" + jfn.title)
178+
175179
// Names for Scala conversions to Java
176180
val s2jClassN = TypeName("AsJava" + jfn.title)
177181
val s2jValCN = TypeName("Rich" + scalaType.name.encoded + "As" + jfn.title)
178182
val s2jDefN = TermName("asJava" + jfn.title)
179183
val s2jImpN = TermName("enrichAsJava" + jfn.title)
180-
184+
181185
// Argument lists for the function / SAM
182-
val vParams = (jfn.params zip jfn.pTypes).map{ case (p,t) =>
186+
val vParams = (jfn.params zip jfn.pTypes).map{ case (p,t) =>
183187
ValDef(NoMods, p.name.toTermName, if (t.typeSymbol.isTypeParameter) Ident(t.typeSymbol.name) else gen.mkAttributedRef(t.typeSymbol), EmptyTree)
184188
}
185189
val vParamRefs = vParams.map(_.name).map(Ident(_))
186-
187-
val j2sClassTree =
190+
191+
val j2sClassTree =
188192
q"""class $j2sClassN[..$tdParams](jf: $javaType[..$javaTargs]) extends $scalaType[..$scalaTargs] {
189193
def apply(..$vParams) = jf.${jfn.name}(..$vParamRefs)
190194
}"""
191-
195+
192196
val j2sValCTree =
193-
q"""implicit class $j2sValCN[..$tdParams](private val underlying: $javaType[..$javaTargs]) extends AnyVal {
197+
q"""class $j2sValCN[..$tdParams](private val underlying: $javaType[..$javaTargs]) extends AnyVal {
194198
@inline def asScala: $scalaType[..$scalaTargs] = new $j2sClassN[..$tnParams](underlying)
195199
}"""
196-
200+
197201
val j2sDefTree =
198-
q"""def $j2sDefN[..$tdParams](jf: $javaType[..$javaTargs]): $scalaType[..$scalaTargs] = new $j2sClassN[..$tnParams](jf)"""
199-
202+
q"""@inline def $j2sDefN[..$tdParams](jf: $javaType[..$javaTargs]): $scalaType[..$scalaTargs] = new $j2sClassN[..$tnParams](jf)"""
203+
204+
val j2sImpTree =
205+
q"""@inline implicit def $j2sImpN[..$tdParams](jf: $javaType[..$javaTargs]): $j2sValCN[..$tnParams] = new $j2sValCN[..$tnParams](jf)"""
206+
200207
val s2jClassTree =
201208
q"""class $s2jClassN[..$tdParams](sf: $scalaType[..$scalaTargs]) extends $javaType[..$javaTargs] {
202209
def ${jfn.name}(..$vParams) = sf.apply(..$vParamRefs)
203210
}"""
204-
211+
205212
val s2jValCTree =
206213
q"""class $s2jValCN[..$tdParams](private val underlying: $scalaType[..$scalaTargs]) extends AnyVal {
207214
@inline def asJava: $javaType[..$javaTargs] = new $s2jClassN[..$tnParams](underlying)
208215
}"""
209-
216+
210217
val s2jDefTree =
211-
q"""def $s2jDefN[..$tdParams](sf: $scalaType[..$scalaTargs]): $javaType[..$javaTargs] = new $s2jClassN[..$tnParams](sf)"""
212-
218+
q"""@inline def $s2jDefN[..$tdParams](sf: $scalaType[..$scalaTargs]): $javaType[..$javaTargs] = new $s2jClassN[..$tnParams](sf)"""
219+
213220
// This is especially tricky because functions are contravariant in their arguments
214221
// Need to prevent e.g. Any => String from "downcasting" itself to Int => String; we want the more exact conversion
215222
val s2jImpTree: (Tree, Int) =
216223
if (jfn.pTypes.forall(! _.isFinalType) && jfn.sig == jfn.sam.typeSignature)
217224
(
218-
q"""implicit def $s2jImpN[..$tdParams](sf: $scalaType[..$scalaTargs]): $s2jValCN[..$tnParams] = new $s2jValCN[..$tnParams](sf)""",
225+
q"""@inline implicit def $s2jImpN[..$tdParams](sf: $scalaType[..$scalaTargs]): $s2jValCN[..$tnParams] = new $s2jValCN[..$tnParams](sf)""",
219226
tdParams.length
220227
)
221228
else {
@@ -242,29 +249,30 @@ object WrapFnGen {
242249
map(TypeDef(NoMods, _, Nil, EmptyTree)).
243250
dropRight(if (jfn.rType.isFinalType) 1 else 0)
244251
val evs = evidences.map{ case (generic, specific) => ValDef(NoMods, TermName("ev"+generic.toString), tq"$generic =:= $specific", EmptyTree) }
245-
val tree =
246-
q"""implicit def $s2jImpN[..$scalafnTdefs](sf: $scalaType[..$scalafnTnames])(implicit ..$evs): $s2jValCN[..$tnParams] =
252+
val tree =
253+
q"""@inline implicit def $s2jImpN[..$scalafnTdefs](sf: $scalaType[..$scalafnTnames])(implicit ..$evs): $s2jValCN[..$tnParams] =
247254
new $s2jValCN[..$tnParams](sf.asInstanceOf[$scalaType[..$scalaTargs]])
248255
"""
249256
val depth = numberedA.size
250257
(tree, tdParams.length)
251258
}
252-
259+
253260
SamConversionCode(
254261
base = jfn.title,
255262
wrappedAsScala = j2sClassTree.text,
256-
implicitToScala = j2sValCTree.text,
263+
asScalaAnyVal = j2sValCTree.text,
264+
implicitToScala = j2sImpTree.text,
257265
asScalaDef = j2sDefTree.text,
258266
wrappedAsJava = s2jClassTree.text,
259267
asJavaAnyVal = s2jValCTree.text,
260268
implicitToJava = s2jImpTree match { case (t,d) => Prioritized(t.text, d) },
261269
asJavaDef = s2jDefTree.text
262270
)
263271
}
264-
272+
265273
sams.toSeq.map(generate)
266274
}
267-
275+
268276
lazy val converterContents =
269277
s"""
270278
|$copyright
@@ -278,21 +286,21 @@ object WrapFnGen {
278286
(SamConversionCode(buildWrappersViaReflection: _*) match {
279287
case (impls, defs) => impls.mkString("\n") + "\n\n\n\n" + defs.map(_.mkString("\n")).mkString("\n\n\n\n")
280288
})
281-
289+
282290
def sameText(f: java.io.File, text: String): Boolean = {
283291
val x = scala.io.Source.fromFile(f)
284292
val lines = try { x.getLines.toVector } finally { x.close }
285293
lines.iterator.filter(_.nonBlank) == text.linesIterator.filter(_.nonBlank)
286294
}
287-
295+
288296
def write(f: java.io.File, text: String) {
289297
if (!f.exists || !sameText(f, text)) {
290298
val p = new java.io.PrintWriter(f)
291299
try { p.println(text) }
292300
finally { p.close() }
293301
}
294302
}
295-
303+
296304
def main(args: Array[String]) {
297305
val names = args.iterator.map(x => new java.io.File(x))
298306
write(names.next, converterContents)

0 commit comments

Comments
 (0)