Switch order of type and term in `as` definitions?

That would violate the definition before use principle for type parameters and using clauses.

2 Likes

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
18 Likes

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).

9 Likes

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."
    | _ -> ()
4 Likes

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]]
4 Likes

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])
1 Like

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) => ???
1 Like

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.
8 Likes

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;

:confused:

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.

1 Like

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

4 Likes

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 :^)

3 Likes

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.

5 Likes

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.

2 Likes

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.

7 Likes

I agree with others this syntax works better for givens and case patterns:

  1. In givens, it’s the type that matters more not the name
  2. 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
  3. It makes the syntax given Type = ??? more regular
  4. 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.

3 Likes

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 = ???