Skip to content

Commit a685a15

Browse files
committed
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 fbce967 commit a685a15

File tree

3 files changed

+920
-14
lines changed

3 files changed

+920
-14
lines changed

README.md

Lines changed: 65 additions & 1 deletion
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

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,16 @@ object WrapFnGen {
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 {
@@ -100,19 +100,19 @@ object WrapFnGen {
100100
def priorityName(n: Int, pure: Boolean = false): String = {
101101
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
}
110110
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 :+
@@ -125,7 +125,10 @@ object WrapFnGen {
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
}
@@ -165,12 +168,13 @@ object WrapFnGen {
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)
171+
// (4) An implicit conversion method name (ImpN) that invokes the value class
169172

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)
177+
val j2sImpN = TermName("enrichAsScalaFrom" + jfn.title)
174178

175179
// Names for Scala conversions to Java
176180
val s2jClassN = TypeName("AsJava" + jfn.title)
@@ -190,12 +194,15 @@ object WrapFnGen {
190194
}"""
191195

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
}"""
196200

197201
val j2sDefTree =
198-
q"""def $j2sDefN[..$tdParams](jf: $javaType[..$javaTargs]): $scalaType[..$scalaTargs] = new $j2sClassN[..$tnParams](jf)"""
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)"""
199206

200207
val s2jClassTree =
201208
q"""class $s2jClassN[..$tdParams](sf: $scalaType[..$scalaTargs]) extends $javaType[..$javaTargs] {
@@ -208,14 +215,14 @@ object WrapFnGen {
208215
}"""
209216

210217
val s2jDefTree =
211-
q"""def $s2jDefN[..$tdParams](sf: $scalaType[..$scalaTargs]): $javaType[..$javaTargs] = new $s2jClassN[..$tnParams](sf)"""
218+
q"""@inline def $s2jDefN[..$tdParams](sf: $scalaType[..$scalaTargs]): $javaType[..$javaTargs] = new $s2jClassN[..$tnParams](sf)"""
212219

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 {
@@ -243,7 +250,7 @@ object WrapFnGen {
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) }
245252
val tree =
246-
q"""implicit def $s2jImpN[..$scalafnTdefs](sf: $scalaType[..$scalafnTnames])(implicit ..$evs): $s2jValCN[..$tnParams] =
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
@@ -253,7 +260,8 @@ object WrapFnGen {
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,

0 commit comments

Comments
 (0)