I decided to grab some random bits of code to stress-test indentation.
Random stress test one: class with long parameter list:
class TierTwo(
now: Instant,
one: Path, two: Path, net: Path,
parallelism: Int, stopBy: Instant,
quiet: Boolean = false,
policies: Tiering.BackupPolicies = Tiering.BackupPolicies.default,
parameters: RunMwt.Parameters = RunMwt.Parameters.default
) { outer =>
import Tiering._
...
def run() = {
...
(tn, tw, tr, bn, bw, br, an, aw, ar)
}
}
With a colon:
class TierTwo(
now: Instant,
one: Path, two: Path, net: Path,
parallelism: Int, stopBy: Instant,
quiet: Boolean = false,
policies: Tiering.BackupPolicies = Tiering.BackupPolicies.default,
parameters: RunMwt.Parameters = RunMwt.Parameters.default
):
val outer = this
import Tiering._
...
def run() =
...
(tn, tw, tr, bn, bw, br, an, aw, ar)
Verdict: ): says it all. Sad face, pointing backwards. It’s a step back to readability to me just because it’s so not-there.
class TierTwo(
now: Instant,
one: Path, two: Path, net: Path,
parallelism: Int, stopBy: Instant,
quiet: Boolean = false,
policies: Tiering.BackupPolicies = Tiering.BackupPolicies.default,
parameters: RunMwt.Parameters = RunMwt.Parameters.default
) with outer =>
import Tiering._
...
def run() =
...
(tn, tw, tr, bn, bw, br, an, aw, ar)
Verdict: slightly better, but still not very visually clear. Would probably use braces regardless. Prefer with over :. (Not shown: .. also gets lost.
Random stress test two: complex method with lamba(s):
val (contested, uncontested) = unsaved.map{
case (f, t, _, Some(sf)) => No((f, t, sf))
case (f, t, c, _) =>
val g = targetFrom(c, remote) \: (f % "zip").getName
if (safe{ g.exists }.yesOr(_ => false)) No((f, t, g))
else {
val ga = targetFrom(c, remote) \: (f % "zip.atomic").getName
if (safe{ ga.exists }.yesOr(_ => false)) No((f, t, ga))
else Yes((f, t, c))
}
}.unmix
With colons:
val (contested, uncontested) = unsaved
.map:
case (f, t, _, Some(sf)) => No((f, t, sf))
case (f, t, c, _) =>
val g = targetFrom(c, remote) \: (f % "zip").getName
if (safe{ g.exists }.yesOr(_ => false)) No((f, t, g))
else
val ga = targetFrom(c, remote) \: (f % "zip.atomic").getName
if (safe{ ga.exists }.yesOr(_ => false)) No((f, t, ga))
else Yes((f, t, c))
.unmix
Verdict: Colon works fine.
With dots:
val (contested, uncontested) = unsaved
.map ..
case (f, t, _, Some(sf)) => No((f, t, sf))
case (f, t, c, _) =>
val g = targetFrom(c, remote) \: (f % "zip").getName
if (safe{ g.exists }.yesOr(_ => false)) No((f, t, g))
else
val ga = targetFrom(c, remote) \: (f % "zip.atomic").getName
if (safe{ ga.exists }.yesOr(_ => false)) No((f, t, ga))
else Yes((f, t, c))
.unmix
Equally good.
Not shown: with is clunky.
Stress test three: various small objects and classes.
class Zip(home: Path) extends Three(home) with Archived { zip =>
type S = Record.Three.Zip[this.type]
val creator = new InPlateSlot[S, zip.type] {
val owner = zip
protected def uncheckedCreate(home: Path, sd: SpannerDir): S =
new Record.Three.Zip[zip.type](zip, home, sd)
protected val decider =
(s: String) => Yes(s == "zip")
}
}
object Zip {
def apply(home: Path) = new Zip(home)
def old(home: Path) = new OldZip(home)
class OldZip(home: Path) extends Zip(home) {
override def versioning = Versioning.OhOneA
}
}
Colons:
class Zip(home: Path) extends Three(home) with Archived:
private[this] def zip = this // Is there a better way?
type S = Record.Three.Zip[this.type]
val creator = new InPlateSlot[S, zip.type]:
val owner = zip
protected def uncheckedCreate(home: Path, sd: SpannerDir): S =
new Record.Three.Zip[zip.type](zip, home, sd)
protected val decider =
(s: String) => Yes(s == "zip")
object Zip:
def apply(home: Path) = new Zip(home)
def old(home: Path) = new OldZip(home)
class OldZip(home: Path) extends Zip(home):
override def versioning = Versioning.OhOneA
Nice and compact, but the :'s are really important and tend to vanish into the clutter. Verdict: an improvement over the status quo.
With with:
class Zip(home: Path) extends Three(home) with Archived with
private[this] def zip = this // Is there a better way?
type S = Record.Three.Zip[this.type]
val creator = new InPlateSlot[S, zip.type] with
val owner = zip
protected def uncheckedCreate(home: Path, sd: SpannerDir): S =
new Record.Three.Zip[zip.type](zip, home, sd)
protected val decider =
(s: String) => Yes(s == "zip")
object Zip with
def apply(home: Path) = new Zip(home)
def old(home: Path) = new OldZip(home)
class OldZip(home: Path) extends Zip(home) with
override def versioning = Versioning.OhOneA
Considerably easier to spot object boundaries. Equally compact to :.
Verdict: an even bigger improvement over the status quo, and an improvement over :.
Not shown: ... Slightly easier to spot than : but suffers the same problems.
Stress test four: chained/fluent style
broke.foreach{ case (_, msg) =>
alertables.
map(a => (a, a alert msg)).
collect{ case (a, No(e)) =>
s"Failed to send alert $a\n=========\n$e\n==========\n\n"
}.
mkString.
tap(x => if (x.nonEmpty) println(x))
}
With colons:
broke.foreach:
case (_, msg) =>
alertables
.map:
a => (a, a alert msg)
.collect:
case (a, No(e)) =>
s"Failed to send alert $a\n=========\n$e\n==========\n\n"
.mkString
.tap:
x => if (x.nonEmpty) println(x)
Verdict: requirements for : render this no better and possibly worse than the brace style for me (since : must end a line). Wouldn’t use. Might use with mixed style (braces in-line), but mixed style clashes.
Dots:
broke.foreach ..
case (_, msg) =>
alertables
.map .. a =>
(a, a alert msg)
.collect .. case (a, No(e)) =>
s"Failed to send alert $a\n=========\n$e\n==========\n\n"
.mkString
.tap .. x =>
if (x.nonEmpty) println(x)
Lovely! A big improvement over both status quo and :. The method .. varname => pattern makes what is happening really stand out, but without extra space.
Overall–I’m now less excited by the feature than I thought I would be after the discussion above. (I originally wasn’t very excited, but the discussion had gotten me more excited.)
I don’t think dots work for template definitions. with is perfect.
However, I am quite enthusiastic about .. instead of -Yindent-colons. As far as I can tell it solves all problems associated with the syntax. It also makes sense that template definitions are not the same as arguments.
In particular, I think .. works so well because it is a different symbol in a context where : is really expected to be a type ascription, and it enables inline use as well as block-starting use. So you can either start your block with .. or with => depending on what makes sense for the formatting.
If it were up to me, I’d
- Remove -Yindent-colons and have
-Yblock-dots instead, or simply just include it by default. I think it works well enough to not need to go behind a Y-flag.
- Allow
with to open template definitions, including with braces.
- Allow
: as an alternative to with for opening template definitions. with: would be okay too.
This would preserve existing tutorial materials, have a highly regular syntax for people who like regularity, and allow usage to determine the amount of optionality people accept. Furthermore, it would make braceless style first-class as you could write lambdas bracelessly.