Lots of people have said that collection literals should be moved to a separate file. This works for some cases when the data is large and standalone, but not the vast majority of them which are small and integrated deeply into the surrounding context. To see for yourself, run the following bash command in your repo:
git ls-files | grep \\.scala | grep -v test | xargs cat | grep -Ei '[^a-zA-Z](Seq|Vector|List)\('
Here is some example output from the Dotty codebase:
lihaoyi scala3$ git ls-files | grep \\.scala | grep -v test | grep -v integration | xargs cat | grep -E '[^a-zA-Z](Seq|Vector|List)\(' | tail -n30
case inl :: Nil => Some(Body(Seq(Paragraph(inl))))
case inls => Some(Body(Seq(Paragraph(Chain(inls)))))
if (checkParaEnded()) List(s) else List(s, getInline(isInlineEnd = false))
iss ++= List(Text(endOfLine.toString), i2)
Chain(Seq(Text("^"), i))
Chain(List(i, Text(".")))
Seq(m.withOrigin(Origin.ExtensionFrom(source.name, source.dri)).withKind(kind))
val expandedMembers = c.members.map(expandMember(outerMembers ++ Seq(c)))
st.flatMap(s => Vector(s -> ltt) ++ getEdges(s, subtypes))
List(),
prefix: Signature = List(Plain("")),
suffix: Signature = List(Plain("")),
separator: Signature = List(Plain(", ")),
list(params, List(Plain("(")), List(Plain(")")), List(Plain(", "))){ (bdr, param) => bdr.buildAnnotationParameter(param)}
val all = prefixMods.map(_.name) ++ Seq(t.visibility.asSignature) ++ suffixMods.map(_.name)
this.list(paramss, separator = List(Plain(""))) {
this.list(params.parameters, prefix = List(Plain("("), Keyword(params.modifiers)), suffix = List(Plain(")")), forcePrefixAndSuffix = true) { (bld, p) =>
def typeParamList(on: TypeParameterList) = list(on.toList, List(Plain("[")), List(Plain("]"))){ (bdr, e) =>
this.list(paramss, separator = List(Plain(""))) { (bld, pList) => bld.termParamList(pList) }
inspectAllTastyFiles(Nil, List(jar), Nil)(inspector)
List(new ReadTasty) :: // Load classes from tasty
List(new TastyInspectorPhase) :: // Perform a callback for each compilation unit
List(List(new QuotedFrontend))
List(new Inlining) ::
List(new Staging) ::
List(new Splicing) ::
List(new PickleQuotes) ::
inspectAllTastyFiles(Nil, List(jar), Nil)(inspector)
List(new ReadTasty) :: // Load classes from tasty
List(new TastyInspectorPhase) :: // Perform a callback for each compilation unit
Some things worth noting here:
-
People construct collection literals all the time! It isnât just some quirk of test code or com-lihaoyi code.
-
Most of the time, the collection literal appears in a place where there is already a target type! So stating the type again when constructing it is redundant
-
These collection literals tend to be small probably the most common sizes for them are 0
1
and 2
, although other sizes do exist. Thus the âlets provide an alternative worse syntax for 0 and 1-element collectionsâ approach defeats the purpose entirely.
-
Most of these small collections do directly reference things from the enclosing scope! They cannot be simply moved to external data files.
Similar conclusions can be drawn from running this on Ammonite:
lihaoyi Ammonite$ git ls-files | grep \\.scala | grep -v test | grep -v integration | xargs cat | grep -E '[^a-zA-Z](Seq|Vector|List)\(' | tail -n30
@val bugs = Seq("6302", "8971", "9249", "4438", "8603", "6660", "7953", "6659", "8456", "1067", "8307", "9335")
@hl.ref(ammoniteTests/"BuiltinTests.scala", Seq("basicConfig", "@"))
@hl.ref(ammoniteTests/"BuiltinTests.scala", Seq("settings", "@"))
Seq(
Seq("bash", "-i"),
@hl.ref(scriptTests, Seq("loadIvyAdvanced", "@"), "\"\"\"")
start = Seq("specifyMain", "\"\"\"", ""),
start = Seq("specifyMainDoc", "\"\"\"", ""),
case x => Seq(x)
val NewLine = Seq("\n", "\r")
val Up = Seq(DefaultUp, WeirdUp)
val Down = Seq(DefaultDown, WeirdDown)
val Right = Seq(DefaultRight, WeirdRight)
val Left = Seq(DefaultLeft, WeirdLeft)
Seq("sh", "-c", s"$pathedTput $s 2> /dev/tty").!!.trim.toInt
Seq("sh", "-c", s"$pathedStty $s < /dev/tty"): ProcessBuilder
s"LazyList(${(rec(this, Nil).reverse ++ Seq("...")).mkString(",")})"
implicit def stringPrefix(s: String): Strings = Strings(Seq(s))
wrap(ti => ti.ts.inputs.dropPrefix(Seq(-1)).map(_ => Exit))
for ((Seq(l, r), i) <- frags) yield {
val Seq(min, max) = Seq(mark.get, cursor).sorted
val Seq(min, max) = Seq(cursor, mark).sorted
up(Vector(), c)
(Some(start), Vector(), msg, 0)
up(Vector(), c)
searchHistory(historyIndex.max(0), 1, b :+ char, Vector())
searchHistory(historyIndex, 1, b, Vector())
else if (searchTerm.exists(_.isEmpty)) Vector()
case (Vector(), Vector('\n', allAfterNewline @ _*)) =>
append(Vector('\n'))
or Metals
lihaoyi metals$ git ls-files | grep \\.scala | grep -v test | grep -v integration | xargs cat | grep -E '[^a-zA-Z](Seq|Vector|List)\(' | tail -n30
List(
* Consume token stream like "a.b.c" and return List(a, b, c)
List(UnresolvedOverriddenSymbol(rhsName))
List(ResolvedOverriddenSymbol(region.owner))
val indices = text.findIndicesOf(List(c))
s.TextDocuments(List(document))
List(
file <- List(
val metalsDevs = List(
Seq(a, b).join
List(
List(
List(out)
Seq(
val testGroups = List(
val eclipseJdt = Seq(
def deprecatedScala2Versions = Seq(
def nonDeprecatedScala2Versions = Seq(
Seq(scala3, "3.3.1") ++ scala3RC.toSeq
Seq(
List(
val tasks = Seq(
if (isScala3.value || !requiresSemanticdb.value) Seq()
Seq(
val configurations = Seq(
List(
val compilerPackages = List(
List(s"-J--add-exports", s"-Jjdk.compiler/$pkg=ALL-UNNAMED")
Seq(javaHome / "release", javaHome.getParentFile / "release")
Seq(javaHome / "jre" / "lib" / "rt.jar", javaHome / "lib" / "rt.jar")
Or STTP
lihaoyi sttp$ git ls-files | grep \\.scala | grep -v test | grep -v integration | xargs cat | grep -E '[^a-zA-Z](Seq|Vector|List)\(' | tail -n30
Seq(socksWithAuth, socks, httpWithAuth, http, httpsWithAuth, https).find(_.isDefined).flatten
Vector(),
List(digestOut, realmOut, uriOut, nonceOut, qopOut, challengeOut, cnonceOut, nc, algorithmOut, opaqueOut).flatten
val params = List(
private var state: Either[List[Promise[T]], List[T]] = Right(List())
case Left(Nil) => state = Right(List(t))
state = Left(List(p))
val allHeaders = List(contentDisposition) ++ otherHeaders
private var multiPartHeaders: Seq[CurlList] = Seq()
headers = List(Header.contentLength(file.size)),
Seq(array: _*)
CurlCode(CCurl.setopt(handle, option.id, toCVarArgList(Seq(parameter))))
.consumeWith(writeAsync(file.toPath, Seq(StandardOpenOption.WRITE, StandardOpenOption.CREATE)))
Seq(tf.copy(finalFragment = false), tf.copy(payload = ""))
Seq(tf)
Seq(tf.copy(finalFragment = false), tf.copy(payload = ""))
Seq(tf)
List(WebSocketFrame.text(s"response to: $payload"))
val allHeaders = List(contentDisposition) ++ otherHeaders
val allHeaders = List(contentDisposition) ++ otherHeaders
val DefaultBuckets: List[Double] = List(.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10)
In these examples, the the List()
or Seq()
or Vector()
type names mostly fall into two categories:
-
The target type is known. In this case you need to write out the collection type, but thereâs only one right answer! So thereâs only downside in making the wrong choice (you get a compile error) and no upside in being able to do it better (because the target type is fixed!).
-
Nobody cares about the collection type - they just want some collection - and the collection is small and code path sufficiently cold that the performance doesnât matter
In both cases this is pure boilerplate: either you are just writing some collection name to match the target type that you have no choice in choosing, or you are writing a Seq()
that you donât care about the collection at all.
This isnât an acute problem. No program is un-writable because of this problem. But itâs a low-grade boilerplate that occurs in every Scala application, and is entirely unsolvable at the moment