Given Instances
The docs give this example:
given (given outer: Context): Context = outer.withOwner(currentOwner)
This makes anything about existing implicits look like the paragon of clarity in comparison. It looks like the “Buffalo buffalo buffalo” puzzles sometimes given in e.g. philosophy of language classes.
Something should change to make this impossible. For instance, making give
provide things and given
ask for them would fit naturally into common language usage instead of admitting mindboggling things that read like “given given Foo Foo”.
The syntax of given instances and aliases are both weird special cases. Nothing else in the language works like that. I understand what the point of each is, but the instances look like a weird hybrid of what you’d do for a def and what you’d do for a class, making it anything but clear what the point is. That regular instances use =
, e.g. val five = 5
, while given instances do not, is especially befuddling. (That object O
does not use =
is already befuddling enough, but there at least since it isn’t parameterized you can reason that it both declares the singleton class type and automatically gives it a unique name–that is, it’s a sugar for sealed abstract class O.type { ... }; lazy val O: O.type = new O.type {}
.)
Since given instances can be parameterized, and aliases can alias to defs, it’s also quite a bit of work to puzzle out what is gained by having both constructs.
Given Clauses
Given clauses are pretty nice, though I found the foo(x) given y
syntax vastly superior visually in simple cases (but untenable in complex cases). However, the document doesn’t explain whether the (implicit thing: Thing = Companion.defaultThing)
pattern works any longer. It looks like no, given the grammar. It also isn’t clear to me whether def f(given foo: Foo, i: Int)
can be called as f(5)
. I don’t think I missed the example?
It’s not clear why they are called “given clauses”. Parameters aren’t called clauses in other places.
It’s not clear how the values are filled in with multiple relevant givens in scope. Presumably something like implicit resolution, which right now forces awkward tricks like creating superfluous traits just to drive resolution? Or…? Should be spelled out.
Context Bounds
Looks fine! Seems pretty clear to me. Also doesn’t seem to have changed.
Given Imports
This is a solution to only half the problem (actually, only half of half of the problem). Not knowing that import mylib._
brought in implicits is fixed by givens having to be import mylib.{given, _}
. So that’s something. But you still don’t actually know what you’re getting from there. So what you’re required to do doesn’t actually solve even this half of the problem; you have to take the explicit-type option and add all that boilerplate.
But import mylib._
not working because you forgot to bring in implicits is even worse because now they can’t conveniently be inside mylib
any more. This suggests that to recover parity with the old way, best practice will be to spam import mylib{given. _}
everywhere, whether or not you need it…which then gives you the first problem right back again.
Instead, what we need is a less awkward way to specify precedence, along with deprecation messages. So you could
import mylib._
and if you’d get the implicits (givens) that would make things work, but if you marked them all deprecated, you could have a message like, “This invocation uses a default given from mylib. Please import mylib.fast._
to select the fast versions of these implicits, or import mylib.accurate_
to select the accurate ones.”
Maybe it’s too late for a change like this, but I think the new version is strictly worse than the old version as it stands.
It seems like a tooling problem is being hoisted onto the programmer in an unwelcome way. I’d rather the compiler do more for me e.g. by suggesting places to find givens that I need, than force me to manually do that kind of work myself.
Finally, it’s not clear that the givens can be brought in by type with the full types that could appear in the given alias or instance.
Extension Methods
I love the idea, but the syntax is rather ugly. I wouldn’t assume that I’ll just get used to the syntax and eventually love it, because after like a decade of experience with C++ I still find the lambdas impossible, and after a couple of years of Rust I still find |x| bar(x)
lambda syntax way less readable than Scala’s x => bar(x)
.
I’d prefer something like
extend (c: Circle) with {
def circumference: Double = c.radius * math.Pi
}
which could be shortened to
extend (c: Circle) with circumference: Double = c.radius + math.Pi
in the case where you wanted a one-liner.
If you complain that paring this down to be more refined and elegant will just get you back at the current extension syntax, I plead guilty. Yes, it does. Sometimes the extra fluff helps. I think this is one of those times.
Implicit Conversions
This is so awesome! (Note: still need clarification on how givens are resolved when potentially conflicting.)
Implicit By-Name Parameters
This seems like kind of a weird and ugly hack. Isn’t there some better way to get around the problem so we don’t have to worry about it? Like, what’s wrong with always having the by-name behavior to avoid circularity?
It’s especially weird because by-name suggest lazy runtime behavior, but here it’s being used to drive compile-time differences. This is a fundamental shift in how by-name parameters work.
Relationship with Scala 2 Implicits
Seems reasonable, save for not mentioning how to migrate (implicit thing: Thing = defaultThing)
, as I brought up earlier.