That would violate the definition before use principle for type parameters and using clauses.
There is currently a strange inconsistency with given
and as
, which I encountered several times.
We can write the following:
given[A] as Foo[A]
But then if we try to remove the type parameter, it doesn’t work:
given as Foo[Int] // error: end of statement expected but identifier found
Instead, we have to write:
given Foo[Int]
But that syntax doesn’t work with a parameter:
given[A] Foo[A] // error: `as` expected
That seems very weird to me. Wouldn’t it be better to do it the following way?
// Without parameters:
given Foo[Int]
// With parameters:
given[A] Foo[A]
// Without parameters, named:
given Foo[Int] as foo
// With parameters, named:
given[A] Foo[A] as foo
It seems more elegant and consistent, and does not violate the definition before use principle.
PS: using
clauses would use the same order:
given[A](using Bar[A]) Foo[A] as foo
I want to also note that python import statements use as, and in the order this thread is proposing:
import foo as bar
I think this makes a lot of sense, and I hope Martin carefully considers it.
foo match {
case Bar(_) as bar =>
...
reads more correctly to me (in addition to the given examples above).
I really like this proposal, thank you @LPTK!
For what it’s worth, F# also uses this order, both in pattern matching
let (var1, var2) as tuple1 = (1, 2)
printfn "%d %d %A" var1 var2 tuple1
and in type testing
let RegisterControl(control:Control) =
match control with
| :? Button as button -> button.Text <- "Registered."
| :? CheckBox as checkbox -> checkbox.Text <- "Registered."
| _ -> ()
I like that too! (even if that means I’ll have to rework my current code :-))
It would be a bit crazy to do this at this very last minute. We are 3 weeks away from RC-1. Even so, I believe your arguments have merit. I am not worried about changing as
order in pattern matching, since that’s relatively minor. But changing the syntax of givens again is a big ask!
Just for the sake of the argument, if we do it, then I don’t think we should write
given[A] Foo[A]
The fact that [A]
is attached to the given
keyword is a glaring lexical irregularity. The alternative
given [A] Foo[A]
isn’t any better. Why is Foo
between two [A]
's? Why is there a space on the left but not on the right? So, I don’t think this will fly. If we do it we’d have to make it clearer what is a formal parameter and what is an actual argument. The as
keyword served to distinguish the two. We did explore =>
once before, and I still think that’s the best alternative. I.e. it would be
given [A] => Foo[A]
or
given [A] => Ordering[A] => Ordering[List[A]]
Thanks for seriously considering this!
The arrow looks better than the status quo to me. Less potential for confusion.
given [A] => Foo[A]
given [A] => Foo[A] as FooA
given [A] => Ordering[A] => Ordering[List[A]] as ListOrd
It looks exactly like a polymorphic function type though, but I think that’s conceptually adequate. Maybe in the future Scala can support first-class-polymorphic implicits, so that the following two lines would effectively become equivalent:
given [A] => Foo[A]
given ([A] => Foo[A])
In the context of https://github.com/lampepfl/dotty/issues/10484 your proposal makes even more sense. So using
arguments become type focused unifying syntax of methods and lambdas.
def foo(using Context) = ???
def foo = (using Context) => ???
or named variant
def foo(using Context as ctx) = ???
def foo = (using Context as ctx) => ???
I think something like that could work, but unfortunately we want the lambda syntax to allow omitting the type, so that type inference can, well, infer it. Potentially we could allow that with syntax like (using _ as ctx) => ...
, but that would be yet another use of underscore in Scala.
Or a bit shorter
def foo: Contextual = (using as ctx) => ???
Luckily in such cases one doesn’t have to introduce a name and it often will be defined and called as follows
def foo: Contextual = { body }
foo { body }
where Context
is injected into the scope as a given
.
I have a trial PR with the proposed changes https://github.com/lampepfl/dotty/pull/10489. It implements the syntax proposed earlier with =>
following parameters of given instances.
Overall there are 207 changed files. To get an impression about the impact of the changes, go to https://github.com/lampepfl/dotty/pull/10489/files and look at changes in .md
files and in the community build.
Looking at the changeset of the PR, I have the impression that the new syntax is more regular and also reads better. What do others think?
- is it really better?
- is it important enough to switch at this late stage? If we do it, we will certainly need a try-out period of 4-8 weeks where people can give feedback.
One thing we have to be clear about is that we will not introduce one syntax now and change it in the next release. Whatever we decide now we have to stick with.
The current PR changes given and pattern syntax. It leaves using
clauses alone. They are still written
(using Context)
or (using ctx: Context)
, not (using Context as ctx)
. I was considering changing them as well. The upside would be to have a universal rule that optional identifiers of some specified type are always bound with as
. But there are downsides as well:
- Less concise. This matters because typical programs will have only a few givens but many using clauses.
- Less regular when seen in conjunction with normal parameters. Most methods will have both normal parameters and using clauses. It looks weird to have different syntactic conventions for them.
Changing the order for given
doesn’t make sense IMO. Before we had Scala style (name followed by type), now we have Java style (type followed by name)
given intOrd as Ord[Int]
object intOrd extends Ord[Int]
val intOrd: Ord[Int]
vs
given Ord[Int] as intOrd
public foo (Ord[Int] intOrd) {}
public Ord[Int] intOrd;
I think the problem is that semantically when you say “given foo as bar”, that’s really different from “case bar as foo”. Perhaps the whole @
-> as
replacement is not a good idea.
IMHO both pattern binding and givens look better in this new syntax.
- case c as ClashNoSig(y) => c.copy(y + c._1)
+ case ClashNoSig(y) as c => c.copy(y + c._1)
- given listOrd[T](using Ordering[T]) as Ordering[List[T]]
+ given [T] => Ordering[T] => Ordering[List[T]] as listOrd
The important parts are placed in first position.
By the way, I noticed that given => Int
seems to be a new syntax introduced by the PR, specifically for “by-name” givens, as seen in tests/run/given-mutable.scala
:
given => Int =
var x = 0
x += 1
x
@main def Test =
assert(summon[Int] == 1)
assert(summon[Int] == 2)
It does seems useful to distinguish by-name and by-value givens. For the same reason we distinguish val x = ...
from def x = ...
.
As the person who originally suggested swapping the order, I must say that @Sciss makes a good point that having the order different between basically equivalent constructs in the same Scala language is very inelegant.
I think my opinion has changed since my original suggestion, and currently I think that <name> as <type>
makes the language look more regular than the reverse
Java does not have anything like pattern binding or type-based implicit resolution. In both, the name coming after as
is completely optional and is not the important part. This is to contrast to things like val
bindings, where the type is the optional part and the name is not. So the different orders in these different constructs seems to make sense to me.
I think many people actually suggested it at the rime :^)
Also consider this refactoring
case foo: Foo =>
to
case foo @ Foo(needTheBar) =>
that’s straight forward, and you keep the order name followed by type.
(Is there a reason the parser couldn’t handle case foo: Foo(needTheBar) =>
?)
I guess I would just be happy to write
given intOrd: Ord[Int]
Seems to me for programming languages trying to imitate English is a road to trouble.
I would also be happy with that syntax. With the unnamed version being one of:
given: Ord[Int]
given _: Ord[Int]
But I think the ship has sailed on these ones.
Honestly, I am against any last minute changes based on the criterion of looks better
. This is super subjective and I can guarantee that we can find situations that both options look better in.
We are also already working on scalafmt and any new change is a bit of a wasted effort for us. However, if you decide to indeed change the syntax let us know exactly what is changing, because I am finding it difficult to keep up.
I agree with others this syntax works better for givens and case patterns:
- In givens, it’s the type that matters more not the name
- The name is optional and since we read left to right, adding and removing the name will be much easier on the eyes with the new syntax
- It makes the syntax
given Type = ???
more regular - You don’t have a use the
using
keyword anymore for givens parameters. I always found this redundant since it is implied.
The same goes for using clauses:
I’d rather be able write:
def ap(config: Config)(using Context as ctx, Dao, Meta as m): Unit = ???
Than any of the following:
def ap(config: Config)(using ctx: Context, Dao, m: Meta): Unit = ???
def ap(config: Config)(using ctx: Context)(using Dao)(using m: Meta): Unit = ???
Either way this goes, I think Scala 3 has already won in terms of making implicits right.
Here is yet another consideration (sorry!).
Correct me if I’m wrong, but IIRC the distinct using
keyword was chosen because repeating given
looked too weird in definitions like this:
given [T](given Ord[T]) as Ord[List[T]]
But these definitions would now be written:
given [T] => Ord[T] => Ord[List[T]]
So we could now use the same given
keyword for contextual parameters, reducing the cognitive load of learning another keyword:
def ap(config: Config)(given Context): Unit = ???
def ap(config: Config)(given Context as ctx): Unit = ???