Skip to content

Commit d180fc5

Browse files
committed
Added function converter capability based on initial reflection work from Jason Zaugg.
If you have something from java.util.functions, you should be able to ``` import scala.compat.java8.FunctionConverters._ ``` and then use `.asScala` to convert to the corresponding Scala function. If you have a Scala function, use `.asJava` to convert to the best-matching item from java.util.functions. The code is created with a code generator that needs to run in Scala 2.11+, so build.sbt was reworked to be multi-module (the sub-module is the code generator).
1 parent 8de41be commit d180fc5

File tree

3 files changed

+266
-82
lines changed

3 files changed

+266
-82
lines changed

build.sbt

Lines changed: 105 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,112 @@
1-
scalaModuleSettings
2-
3-
scalaVersion := "2.11.6"
4-
5-
organization := "org.scala-lang.modules"
6-
7-
name := "scala-java8-compat"
8-
9-
version := "0.6.0-SNAPSHOT"
10-
11-
// important!! must come here (why?)
12-
scalaModuleOsgiSettings
13-
14-
OsgiKeys.exportPackage := Seq(s"scala.compat.java8.*;version=${version.value}")
15-
16-
OsgiKeys.privatePackage := List("scala.concurrent.java8.*")
17-
18-
libraryDependencies += "junit" % "junit" % "4.11" % "test"
19-
20-
libraryDependencies += "com.novocode" % "junit-interface" % "0.10" % "test"
21-
22-
mimaPreviousVersion := None
23-
24-
testOptions += Tests.Argument(TestFrameworks.JUnit, "-v", "-a")
25-
26-
sourceGenerators in Compile <+= sourceManaged in Compile map { dir =>
27-
def write(name: String, content: String) = {
28-
val f = dir / "scala" / "compat" / "java8" / s"${name}.java"
29-
IO.write(f, content)
30-
f
31-
}
32-
(
33-
Seq(write("JFunction", CodeGen.factory)) ++
34-
(0 to 22).map(n => write("JFunction" + n, CodeGen.fN(n))) ++
35-
(0 to 22).map(n => write("JProcedure" + n, CodeGen.pN(n))) ++
36-
CodeGen.specializedF0.map((write _).tupled) ++
37-
CodeGen.specializedF1.map((write _).tupled) ++
38-
CodeGen.specializedF2.map((write _).tupled)
39-
)
40-
}
41-
42-
sourceGenerators in Test <+= sourceManaged in Test map { dir =>
43-
def write(name: String, content: String) = {
44-
val f = dir / "scala" / "compat" / "java8" / s"${name}.java"
45-
IO.write(f, content)
46-
f
47-
}
48-
Seq(write("TestApi", CodeGen.testApi))
49-
}
50-
51-
initialize := {
52-
// Run previously configured inialization...
53-
initialize.value
54-
// ... and then check the Java version.
55-
val specVersion = sys.props("java.specification.version")
56-
if (Set("1.5", "1.6", "1.7") contains specVersion)
57-
sys.error("Java 8 or higher is required for this project.")
58-
}
59-
601
val disableDocs = sys.props("nodocs") == "true"
612

62-
publishArtifact in packageDoc := !disableDocs
63-
643
lazy val JavaDoc = config("genjavadoc") extend Compile
654

66-
sources in (Compile, doc) := {
67-
val orig = (sources in (Compile, doc)).value
68-
orig.filterNot(_.getName.endsWith(".java")) // raw types not cooked by scaladoc: https://issues.scala-lang.org/browse/SI-8449
5+
def jwrite(dir: java.io.File)(name: String, content: String) = {
6+
val f = dir / "scala" / "compat" / "java8" / s"${name}.java"
7+
IO.write(f, content)
8+
f
699
}
7010

71-
inConfig(JavaDoc)(Defaults.configSettings) ++ (if (disableDocs) Nil else Seq(
72-
packageDoc in Compile <<= packageDoc in JavaDoc,
73-
sources in JavaDoc <<= (target, compile in Compile, sources in Compile) map {(t, c, s) =>
74-
val allJavaSources = (t / "java" ** "*.java").get ++ s.filter(_.getName.endsWith(".java"))
75-
allJavaSources.filterNot(_.getName.contains("FuturesConvertersImpl.java")) // this file triggers bugs in genjavadoc
76-
},
77-
javacOptions in JavaDoc := Seq(),
78-
artifactName in packageDoc in JavaDoc := ((sv, mod, art) => "" + mod.name + "_" + sv.binary + "-" + mod.revision + "-javadoc.jar"),
79-
libraryDependencies += compilerPlugin("com.typesafe.genjavadoc" % "genjavadoc-plugin" % "0.8" cross CrossVersion.full),
80-
scalacOptions in Compile <+= target map (t => "-P:genjavadoc:out=" + (t / "java"))
81-
))
82-
83-
initialCommands :=
84-
"""|import scala.concurrent._
85-
|import ExecutionContext.Implicits.global
86-
|import java.util.concurrent.{CompletionStage,CompletableFuture}
87-
|import scala.compat.java8.FutureConverter._
88-
|""".stripMargin
11+
lazy val commonSettings = Seq(
12+
scalaVersion := "2.11.6",
13+
organization := "org.scala-lang.modules",
14+
version := "0.6.0-SNAPSHOT",
15+
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value,
16+
libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value
17+
)
18+
19+
lazy val fnGen = (project in file("fnGen")).
20+
settings(commonSettings: _*).
21+
settings(
22+
fork in run := true // Needed if you run this project directly
23+
)
8924

25+
lazy val root = (project in file(".")).
26+
dependsOn(fnGen).
27+
settings(scalaModuleSettings: _*).
28+
settings(commonSettings: _*).
29+
settings(
30+
name := "scala-java8-compat"
31+
).
32+
settings(
33+
// important!! must come here (why?)
34+
scalaModuleOsgiSettings: _*
35+
).
36+
settings(
37+
fork := true, // This must be set so that runner task is forked when it runs fnGen and the compiler gets a proper classpath
38+
39+
OsgiKeys.exportPackage := Seq(s"scala.compat.java8.*;version=${version.value}"),
40+
41+
OsgiKeys.privatePackage := List("scala.concurrent.java8.*"),
42+
43+
libraryDependencies += "junit" % "junit" % "4.11" % "test",
44+
45+
libraryDependencies += "com.novocode" % "junit-interface" % "0.10" % "test",
46+
47+
mimaPreviousVersion := None,
48+
49+
testOptions += Tests.Argument(TestFrameworks.JUnit, "-v", "-a"),
50+
51+
(sourceGenerators in Compile) += Def.task {
52+
val out = (sourceManaged in Compile).value
53+
if (!out.exists) IO.createDirectory(out)
54+
val canon = out.getCanonicalPath
55+
val args = (new File(canon, "FunctionWrappers.scala")).toString :: (new File(canon, "FunctionConverters.scala")).toString :: Nil
56+
val runTarget = (mainClass in Compile in fnGen).value getOrElse "No main class defined for function conversion generator"
57+
val classPath = (fullClasspath in Compile in fnGen).value
58+
toError(runner.value.run(runTarget, classPath.files, args, streams.value.log))
59+
(out ** "*.scala").get
60+
}.taskValue,
61+
62+
sourceGenerators in Compile <+= sourceManaged in Compile map { dir =>
63+
val write = jwrite(dir) _
64+
Seq(write("JFunction", CodeGen.factory)) ++
65+
(0 to 22).map(n => write("JFunction" + n, CodeGen.fN(n))) ++
66+
(0 to 22).map(n => write("JProcedure" + n, CodeGen.pN(n))) ++
67+
CodeGen.specializedF0.map(write.tupled) ++
68+
CodeGen.specializedF1.map(write.tupled) ++
69+
CodeGen.specializedF2.map(write.tupled)
70+
},
71+
72+
sourceGenerators in Test <+= sourceManaged in Test map { dir =>
73+
Seq(jwrite(dir)("TestApi", CodeGen.testApi))
74+
},
75+
76+
initialize := {
77+
// Run previously configured inialization...
78+
initialize.value
79+
// ... and then check the Java version.
80+
val specVersion = sys.props("java.specification.version")
81+
if (Set("1.5", "1.6", "1.7") contains specVersion)
82+
sys.error("Java 8 or higher is required for this project.")
83+
},
84+
85+
publishArtifact in packageDoc := !disableDocs,
86+
87+
sources in (Compile, doc) := {
88+
val orig = (sources in (Compile, doc)).value
89+
orig.filterNot(_.getName.endsWith(".java")) // raw types not cooked by scaladoc: https://issues.scala-lang.org/browse/SI-8449
90+
}
91+
).
92+
settings(
93+
(inConfig(JavaDoc)(Defaults.configSettings) ++ (if (disableDocs) Nil else Seq(
94+
packageDoc in Compile <<= packageDoc in JavaDoc,
95+
sources in JavaDoc <<= (target, compile in Compile, sources in Compile) map {(t, c, s) =>
96+
val allJavaSources = (t / "java" ** "*.java").get ++ s.filter(_.getName.endsWith(".java"))
97+
allJavaSources.filterNot(_.getName.contains("FuturesConvertersImpl.java")) // this file triggers bugs in genjavadoc
98+
},
99+
javacOptions in JavaDoc := Seq(),
100+
artifactName in packageDoc in JavaDoc := ((sv, mod, art) => "" + mod.name + "_" + sv.binary + "-" + mod.revision + "-javadoc.jar"),
101+
libraryDependencies += compilerPlugin("com.typesafe.genjavadoc" % "genjavadoc-plugin" % "0.8" cross CrossVersion.full),
102+
scalacOptions in Compile <+= target map (t => "-P:genjavadoc:out=" + (t / "java"))
103+
))): _*
104+
).
105+
settings(
106+
initialCommands :=
107+
"""|import scala.concurrent._
108+
|import ExecutionContext.Implicits.global
109+
|import java.util.concurrent.{CompletionStage,CompletableFuture}
110+
|import scala.compat.java8.FutureConverter._
111+
|""".stripMargin
112+
)

fnGen/WrapFnGen.scala

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright (C) 2015 Typesafe Inc. <http://www.typesafe.com>
3+
*/
4+
5+
object WrapFnGen {
6+
val copyright =
7+
s"""
8+
|/*
9+
| * Copyright (C) 2015, Typesafe Inc. <http://www.typesafe.com>
10+
| * This file auto-generated by WrapFnGen.scala. Do not modify directly.
11+
| */
12+
|""".stripMargin
13+
14+
val packaging = "package scala.compat.java8"
15+
16+
private def buildWrappersViaReflection: Seq[(String, String, String, String, Int)] = {
17+
import scala.tools.nsc._
18+
import scala.reflect.internal._
19+
val settings = new Settings(msg => sys.error(msg))
20+
settings.usejavacp.value = true
21+
val compiler = new Global(settings)
22+
val run = new compiler.Run
23+
24+
import compiler._, definitions._
25+
26+
val pack: Symbol = rootMirror.getPackageIfDefined(TermName("java.util.function"))
27+
28+
case class Jfn(iface: Symbol, sam: Symbol) {
29+
lazy val genericCount = iface.typeParams.length
30+
lazy val name = sam.name.toTermName
31+
lazy val title = iface.name.encoded
32+
lazy val params = sam.info.params
33+
lazy val sig = sam typeSignatureIn iface.info
34+
lazy val pTypes = sig.params.map(_.info)
35+
lazy val rType = sig.resultType
36+
def arity = params.length
37+
}
38+
39+
val sams = pack.info.decls.
40+
map(d => (d, d.typeSignature.members.filter(_.isAbstract).toList)).
41+
collect{ case (d, m :: Nil) if d.isAbstract => Jfn(d, m) }
42+
43+
case class Compat(asScala: Tree, scalaDef: Tree, asJava: Tree, javaDef: Tree, source: Jfn) {}
44+
45+
def compatibilize(jfn: Jfn): Compat = {
46+
val parent = gen.mkAttributedRef(FunctionClass(jfn.arity))
47+
val jType = gen.mkAttributedRef(jfn.iface)
48+
val tnParams: List[TypeName] = jfn.iface.typeParams.map(_.name.toTypeName)
49+
val tdParams: List[TypeDef] = tnParams.map(TypeDef(NoMods, _, Nil, EmptyTree))
50+
51+
val j2sWrapperName = TypeName(jfn.title + "As" + parent.name.encoded)
52+
val j2sDefName = TermName("wrap_" + jfn.title + "_asScala")
53+
val s2jWrapperName = TypeName(parent.name.encoded + "As" + jfn.title)
54+
val s2jDefName = TermName("wrap_asJava_" + jfn.title)
55+
56+
val selfTargs: List[Tree] = tdParams.map(_.name).map(Ident(_))
57+
def mkRef(tp: Type): Tree = if (tp.typeSymbol.isTypeParameter) Ident(tp.typeSymbol.name.toTypeName) else tq"$tp"
58+
val parentTargs: List[Tree] = jfn.pTypes.map(mkRef) :+ mkRef(jfn.rType)
59+
val wrapperVParams = (jfn.params zip jfn.pTypes).map{ case (p,t) =>
60+
ValDef(NoMods, p.name.toTermName, if (t.typeSymbol.isTypeParameter) Ident(t.typeSymbol.name) else gen.mkAttributedRef(t.typeSymbol), EmptyTree)
61+
}
62+
val wrapperVParamRefs = wrapperVParams.map(_.name).map(Ident(_))
63+
64+
Compat(
65+
q"""class $j2sWrapperName[..$tdParams](self: $jType[..$selfTargs]) extends $parent[..$parentTargs] with WrappedAsScala[$parent[..$parentTargs]] {
66+
def apply(..$wrapperVParams) = self.${jfn.name}(..${wrapperVParamRefs})
67+
def asScala: $parent[..$parentTargs] = this
68+
}
69+
""",
70+
q"""implicit def $j2sDefName[..$tdParams](javaFn: $jType[..$selfTargs]): WrappedAsScala[$parent[..$parentTargs]] = new $j2sWrapperName[..$tnParams](javaFn)""",
71+
q"""class $s2jWrapperName[..$tdParams](self: $parent[..$parentTargs]) extends $jType[..$selfTargs] with WrappedAsJava[$jType[..$selfTargs]] {
72+
def ${jfn.name}(..$wrapperVParams) = self.apply(..$wrapperVParamRefs)
73+
def asJava: $jType[..$selfTargs] = this
74+
}
75+
""",
76+
q"""implicit def $s2jDefName[..$tdParams](scalaFn: $parent[..$parentTargs]): WrappedAsJava[$jType[..$selfTargs]] = new $s2jWrapperName[..$tnParams](scalaFn)""",
77+
jfn
78+
)
79+
}
80+
81+
def text(t: Tree) = showCode(t).replace("$", "")
82+
83+
sams.toSeq.map(compatibilize).map{
84+
case Compat(asScala, scalaDef, asJava, javaDef, source) =>
85+
(text(asScala), text(scalaDef), text(asJava), text(javaDef), source.genericCount)
86+
}
87+
}
88+
89+
lazy val wrappers = buildWrappersViaReflection
90+
91+
lazy val groupedWrappers = wrappers.
92+
groupBy(_._5).
93+
toList.sortBy(_._1) match {
94+
case first :: rest =>
95+
val names = ("object FunctionConverters" -> first._2) :: rest.map{ case (i,x) => s"trait Priority${i}FunctionConverters" -> x }
96+
val opNames = names.map(x => Option(x))
97+
opNames.zipAll(opNames.drop(1), None, None).flatMap{
98+
case (ox, None) => ox
99+
case (Some(x), Some(y)) => Option(s"${x._1} extends ${y._1.stripPrefix("trait ")}" -> x._2)
100+
case (None, oy) => None // Shouldn't ever hit this case, but the compiler doesn't know
101+
}
102+
case Nil => throw new NoSuchElementException
103+
}
104+
105+
lazy val classFileContents = {
106+
s"""
107+
|$copyright
108+
|
109+
|$packaging
110+
|package functionWrappers
111+
|
112+
|""".stripMargin +
113+
wrappers.map{ case (s, _, j, _, _) => s"$s\n\n$j\n\n" }.mkString("\n")
114+
}
115+
116+
lazy val converterContents = {
117+
s"""
118+
|$copyright
119+
|
120+
|$packaging
121+
|
122+
|import functionWrappers._
123+
|import language.implicitConversions
124+
|
125+
|
126+
|""".stripMargin +
127+
groupedWrappers.reverse.map{ case (name, cvs) =>
128+
s"""
129+
|$name {
130+
|${cvs.map{ case (_, s, _, j, _) => s" $s\n $j" }.mkString("\n\n")}
131+
|}""".stripMargin
132+
}.mkString("\n\n\n")
133+
}
134+
135+
def write(f: java.io.File, text: String) {
136+
if (!f.exists || { val x = scala.io.Source.fromFile(f); try { x.getLines.toVector } finally { x.close } }.filter(_.nonEmpty) != text.linesIterator.filter(_.nonEmpty).toSeq) {
137+
val p = new java.io.PrintWriter(f)
138+
try { p.println(text) }
139+
finally { p.close() }
140+
}
141+
}
142+
143+
def main(args: Array[String]) {
144+
val names = args.iterator.map(x => new java.io.File(x))
145+
write(names.next, classFileContents)
146+
write(names.next, converterContents)
147+
}
148+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package scala.compat.java8
2+
3+
/** A trait that indicates that the class is or can be converted to a Scala version by wrapping a Java class */
4+
trait WrappedAsScala[S] {
5+
/** Returns an appropriate Scala version */
6+
def asScala: S
7+
}
8+
9+
/** A trait that indicates that the class is or can be converted to a Java version by wrapping a Scala class */
10+
trait WrappedAsJava[J] {
11+
/** Returns an appropriate Java version */
12+
def asJava: J
13+
}

0 commit comments

Comments
 (0)