Agree, this doesn’t automatically make code more safe. But it makes it more regular, easier to read, and less daunting (IMO). I would rather tackle the problem of thread safety in the new setup, than reverting to the old implicit system.
What is the common case? Is it defining a named object and using it implicitly (i.e applying it indirectly to a function) in the same scope it was defined? I would argue that this is not the common case, and if it is then it’s a bad practice.
If one desires to define objects that can be used implicitly by others / in a different scope, one should name them but shouldn’t define them as implicit (the “importer” should make them implicit in his/her scope). Alternatively, if one desires to define objects to be used implicitly in the same scope, one should define them anonymously and make them implicit.
given intOrd: Ord[Int] { defs }
This is confusing. This introduces a new type of definition into the language. This “new type” is indistinguishable from an object
that has been been made implicit. More definitions, steeper learning curve.
What common cases require more than this concise syntax?
def foo(a: Int)(given b: Int): Int = ???
foo(1)(give expr)
give expr
import give Foo
import Foo.{give A, B}
summon Type
(obviously conversions and extension methods would require a different syntax)
In natural language we would probably say “let t be a given Table
”. I think even given t as Table
is confusing, because I would read it as if it were given Table = t
, so an alias. I think what this shows is that using just the word given
is still not solving the whole riddle which didn’t exist with implicit
because we have implicit object t extends Table
and while that’s very wordy, there is no ambiguity what it means. I don’t know what the solution is, only that the problem is not solved yet.
I also think (raised before by AMatveev, soronpo, sciss, mdedetrich) that the non-explicit evaluation semantic is problematic.
Currently [edited after Martin’s clarification]
direct instance, monomorphic: object / lazy valdirect instance, generic: defalias (monomorphic or generic): def- monomorphic: object / lazy val
- generic: def
My old example that was based on wrong assumptions
For example, I could write
given Ordering[Int] { def compare(x: Int, y: Int) = x - y }
Later I realize that I can use a function literal, so I refactor it to
given Ordering[Int] = (x, y) => x - y
which creates a new instance for every use.
Or the other way around: I might start with an alias like the above, but then have to move to a direct instance because the type is no longer a SAM. The enclosing instance suddenly gains a field.
I’m trying to partially delineate what for me constitutes the problem. Perhaps one can find a solution iteratively.
Martin’s “matrix of eight” is a good starting point; I think if this matrix is “solved”, the entire construction is solved. Looking at it, I think one problem is that it works well / is intuitive for the alias cases using the equals sign. So these four
given TC = expr
given x: TC = expr
given [T]: TC = expr
given x[T]: TC = expr
For a seasoned Scala programmer, you can just say “like a type alias, but instead it’s an implicit term alias”. Note now the first element, it’s irregular, it doesn’t have a colon and no term definition on the left side. How about fixing this as follows:
given: TC = expr
Examples:
given: Ordering[File] = Ordering.by(_.lastModified)
given: Ordering[File] {
def compare(a: File, b: File): Int =
math.signum(a.lastModified - b.lastModified).toInt
}
Another idea (sorry, I hope it’s not too much traffic)
// aliases
given[TC] = expr
given[TC] x = expr
given[TC[T]] = expr
given[TC[T]] x = expr
// definitions
given[TC] { defs }
given[TC] x { defs }
given[TC] x
given[TC[T]] { defs }
given[TC[T]] x { defs }
Examples:
given[Ordering[File]] = Ordering.by(_.lastModified)
given[Ordering[File]] {
def compare(a: File, b: File): Int =
math.signum(a.lastModified - b.lastModified).toInt
}
On the downside, given
now looks like it is a method invocation.
alias (monomorphic or generic): def
No, if it’s a monomorphic alias instance, it translates to a lazy val(*).
Essentially, if the given has parameters it’s a def, otherwise it’s a lazy val. I think that’s quite clear. It’s exactly what Haskell does for normal definitions.
(*) Maybe the confusion arose from the clause that said that an alias instance of a simple, pure reference is optimized to a def since in that case the two implementations have the same observable behavior and the additional field would be wasteful.
What do you do for parameters? Right now it’s
(given TC)
(given x: TC)
for parameters and
given TC = expr
given x: TC = expr
for instances. I think it’s very good that the two scenarios are consistent.
Ah, thanks for the clarification. I looked at the output of -Xprint:front
where it’s a def
, but then it gets changed into a lazy val
in a later phase.
In any case, I think making it obvious how instances are encoded would be valuable.
Another question regarding ambiguity. What if a trait defines an abstract implicit / given. Scala 2:
trait Foo[A] {
implicit def ord: Ordering[A]
def bar(a: A, b: A): A = List(a, b).max
}
How would that look in Scala 3:
trait Foo[A] {
given ord: Ordering[A]
def bar(a: A, b: A): A = List(a, b).max
}
Is this not in general ambiguous with form (2) in the 8-matrix (the confusing given t: Table
case)?
I think it works in parameter position too:
def foo(given: TC): R
def foo(given x: TC): R
The same as for instances:
given: TC = expr
given x: TC = expr
So it’s just as regular as your proposal within the given
proposal, but I also find it much more regular within the language as a whole, since it avoids having type names in what looks like term positions.
The general idea with given
is that it does much less than previous implicits (and that’s a good thing!). Whenever you need to achieve something like abstract over a definition, do it on normal definitions and then inject it into the given space. The Foo
example would look like this in Scala 3:
trait Foo[A] {
def ord: Ordering[A]
given Ordering[A] = ord
def bar(a: A, b: A): A = List(a, b).max
}
Bonus points: The given
can be private to Foo
, i.e.
private given Ordering[A] = ord
I still prefer the original syntax, for two reasons: First, it’s a symbol less to type and read (which matters a lot for parameters). Second, the alias form looks wrong to me.
given ExecutionContext = ForkJoinContext()
reads to me: The “given ExecutionContext is a ForkJoinContext”, whereas
given: ExecutionContext = ForkJoinContext()
reads “Given that ExecutionContext is ForkJoinContext” which again has the flaw that it equates a type with a term.
I agree that one has to get used to see sometimes a term name and sometimes a type after a given
. To avoid confusion there should be the style guide rule that if a given is explicitly named the name is always in lower case. That makes it’s easier to distinguish the two forms at a glance.
I also think it will be better for IDEs to offer proper completions this way.
Would something along these lines be more readable/understandable?
Idea is that:
- the definitions are
summonable
- parameters are (already)
summoned
(resolved, known) -
summon
can be used too summon thesummonable
s…
summonable global: ExecutionContext = new ForkJoinPool()
summonable (summoned outer: Context): Context =
outer.withOwner(currentOwner)
summonable [T](summoned Ord[T]): Ord[List[T]] =
{ ... }
Not saying it is more readable, could be something different from “summon”.
But IMHO it’s easier to understand and parse in your head, has better semantics.
Problem with given
is kinda same like with implicit
, it’s hard to see what it is doing…
I also agree that using _ : Something
for anonymous declarations would be fine.
Personally, when I see:
given ExecutionContext = ForkJoinContext()
I read:
Given ExecutionContext is ForkJoinContext
as in:
Given the fact that ExecutionContext is ForkJoinContext
I understand it’s the wrong way of reading it. But it seems problematic when a construct reads ambiguously like that.
To avoid confusion, why not make the chosen keyword behave mostly like existing keywords rather than creating a hybrid of multiple syntax possibilities:
given
can be used as a modifier for parameters.given
can be used inimport
definitions.given
can define a new term / name / identifier (likeval
,def
andobject
).given
can define an anonymous instance, with the type on the left side, making it the only keyword (not completely sure about this) able to have:
to the right of it (should probably be replaced withgiven _: Type = ???
).given
can define an anonymous generic function, which is not currently possible by any other means.
No matter how much styling guides people will follow. The fact that this keyword has so many distinct and unique semantics beyond its core functionality, where’s no other keyword has that many, will continue to cause confusion.
I understand it’s the wrong way of reading it. But it seems problematic when a construct reads ambiguously like that.
Agree it’s not ideal but it looks more like a problem initially that can be overcome when people are more used to the new construct. The difference for me is that
given ExecutionContext = ForkJoinContext()
can be read both ways whereas
given: ExecutionContext = ForkJoinContext()
really can be read only in one way, and that’s the wrong one.
Do we really need the compiler to manage the val/def semantics of givens?
It seems like it’s causing way more confusion than it’s helping.
It’s nice because you get a free lazy val for all givens with type parameters that have no value parameters, e.g:
given[A] Monoid[List[A]] { ... }
In Scala 2 this is really tricky to achieve:
implicit def monoidList[A]: Monoid[List[A]] = monoidList0.asInstanceOf[Monoid[List[A]]
private[this] lazy val monoidList0 = new Monoid[List[Any]] { ... }