Here’s an example bit of code from Mill where I think a []
syntax would help:
object eval extends build.MillStableScalaModule {
def moduleDeps = Seq(define)
}
object resolve extends build.MillStableScalaModule {
def moduleDeps = Seq(define)
}
object client extends build.MillPublishJavaModule with BuildInfo {
def buildInfoPackageName = "mill.main.client"
def buildInfoMembers = Seq(BuildInfo.Value("millVersion", build.millVersion(), "Mill version."))
object test extends JavaModuleTests with TestModule.Junit4 {
def ivyDeps = Agg(build.Deps.junitInterface, build.Deps.commonsIo)
}
}
object server extends build.MillPublishScalaModule {
def moduleDeps = Seq(client, api)
}
object graphviz extends build.MillPublishScalaModule {
def moduleDeps = Seq(build.main, build.scalalib)
def ivyDeps = Agg(build.Deps.jgraphtCore) ++ build.Deps.graphvizJava ++ build.Deps.javet
}
object maven extends build.MillPublishScalaModule {
def moduleDeps = Seq(build.runner)
def ivyDeps = Agg(
build.Deps.mavenEmbedder,
build.Deps.mavenResolverConnectorBasic,
build.Deps.mavenResolverSupplier,
build.Deps.mavenResolverTransportFile,
build.Deps.mavenResolverTransportHttp,
build.Deps.mavenResolverTransportWagon
)
def testModuleDeps = super.testModuleDeps ++ Seq(build.scalalib)
}
def testModuleDeps = super.testModuleDeps ++ Seq(build.testkit)
Nothing fancy, just a bunch of object
s and def
s defining collections. Right now they’re a mix of Seq
s and Aggs
, in the next breaking version they’ll all be Seq
s, but that doesn’t affect the example.
With []
syntax, it looks like this:
object eval extends build.MillStableScalaModule {
def moduleDeps = [define]
}
object resolve extends build.MillStableScalaModule {
def moduleDeps = [define]
}
object client extends build.MillPublishJavaModule with BuildInfo {
def buildInfoPackageName = "mill.main.client"
def buildInfoMembers = [BuildInfo.Value("millVersion", build.millVersion(), "Mill version.")]
object test extends JavaModuleTests with TestModule.Junit4 {
def ivyDeps = [build.Deps.junitInterface, build.Deps.commonsIo]
}
}
object server extends build.MillPublishScalaModule {
def moduleDeps = [client, api]
}
object graphviz extends build.MillPublishScalaModule {
def moduleDeps = [build.main, build.scalalib]
def ivyDeps = [build.Deps.jgraphtCore] ++ build.Deps.graphvizJava ++ build.Deps.javet
}
object maven extends build.MillPublishScalaModule {
def moduleDeps = [build.runner]
def ivyDeps = [
build.Deps.mavenEmbedder,
build.Deps.mavenResolverConnectorBasic,
build.Deps.mavenResolverSupplier,
build.Deps.mavenResolverTransportFile,
build.Deps.mavenResolverTransportHttp,
build.Deps.mavenResolverTransportWagon
]
def testModuleDeps = super.testModuleDeps ++ [build.scalalib]
}
def testModuleDeps = super.testModuleDeps ++ [build.testkit]
It’s not a quantum leap forward, but it does help the reader focus on what’s important - the configuration values - rather than the auxiliary Seq
wrappers. In this case it doesn’t really matter what collection type it is, hence the usage of Seq
, which I would expect to be the common case since most parts of most programs are not performance sensitive.
Another example is from uPickle, which lets you define JSON literals as follows:
val json: ujson.Value = ujson.Obj(
"declarationMap" -> true,
"esModuleInterop" -> true,
"baseUrl" -> ".",
"rootDir" -> "typescript",
"declaration" -> true,
"outDir" -> pubBundledOut(),
"plugins" -> ujson.Arr(
ujson.Obj("transform" -> "typescript-transform-paths"),
ujson.Obj(
"transform" -> "typescript-transform-paths",
"afterDeclarations" -> true
)
),
"moduleResolution" -> "node",
"module" -> "CommonJS",
"target" -> "ES2020"
)
This would look a lot nicer if written with square brackets
val json: ujson.Value = [
"declarationMap" -> true,
"esModuleInterop" -> true,
"baseUrl" -> ".",
"rootDir" -> "typescript",
"declaration" -> true,
"outDir" -> pubBundledOut(),
"plugins" -> [
["transform" -> "typescript-transform-paths"],
[
"transform" -> "typescript-transform-paths",
"afterDeclarations" -> true
]
],
"moduleResolution" -> "node",
"module" -> "CommonJS",
"target" -> "ES2020"
]
Again, not groundbreaking, but it’s a significant reduction in boilerplate names that the user doesn’t care about in these contexts, to let the reader focus on the data that is what actually matters.
Moving data out into config files is always an option. In the old days, people found Java too verbose, and so data was moved into XML, YAML, and other formats. But there is a real cost for introducing a separate-file and separate-language barrier: you lose type safety, editor support, performance, add indirection, etc. etc… Being able to inline important bits of hierarchical data with minimal boilerplate is table stakes for most modern languages today for good reason.
A third scenarios is the OS-Lib subprocess syntax. Currently, you can do
os.call(Seq("curl", "www.google.com"))
Using the Seq
constructor, or
os.call(("curl", "www.google.com"))
which we accomplish via implicit conversion hacks on the tuple data types, which are non-standard and fragile. Neither of these is great, and it would be nice to write
os.call(["curl", "www.google.com"])
To be able to pass collections of strings to a subprocess invocation
A fourth example is from Requests-Scala:
requests.get(
"https://api.github.com/some/endpoint",
params = Map("q" -> "http language:scala", "sort" -> "stars")
headers = Map("user-agent" -> "my-app/0.0.1,other-app/0.0.2")
)
requests.get(
"https://api.github.com/some/endpoint",
params = ["q" -> "http language:scala", "sort" -> "stars"]
headers = ["user-agent" -> "my-app/0.0.1,other-app/0.0.2"]
)
The Map
s we see in the first snippet provide no meaning. The user doesn’t care about them. The important part is "q" -> "http language:scala"
, "user-agent" -> "my-app/0.0.1,other-app/0.0.2"
, etc… There is also a target type, so there’s no ambiguity as to what the type of the expression is. Being able to elide the Map
would be a nice boon in this sort of code