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

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

sorry, but that’s horrible :grimacing:

that way we’re moving towards AppleScript.

By the same logic we should write (hyperbolic)

def Unit as ap(Config as config) = ???
1 Like

The point is to be able to name some of the arguments while leaving others unnamed.

Compare it to this (current syntax):

def ap(config: Config)(using ctx: Context, Dao, m: Meta): Unit = ???

Both are not perfect but the first one is clearer to me.

Yes, you should be able to omit names, but you can also distinguish given Ord[Int] and using Context versus given intOrd: Ord[Int] and using ctx: Context.

I think the simple cases look maybe a bit better, but I find that the more complex cases where givens also have using clauses are more difficult to read with the arrows. I think I like the current syntax better.

edit: for example, I find the following harder than what it was before:

given [A] => (inst: => K0.CoproductInstances[Eq, A]) => Eq[A] as eqSum
2 Likes

@bmeesters In

In fairness the community build examples were translated mostly automatically, without looking at formatting. This example is admittedly more tricky because of the call by name parameter, which is a rare special case in general. If it was not for that, it would look like this:

given [A] => K0.CoproductInstances[Eq, A] => Eq[A] as eqSum

Whereas the current syntax would be this:

given eqSum[A](using K0.CoproductInstances[Eq, A]) as Eq[A]

I find the new syntax better since it concentrates on the essential: I can get an Eq from ProductInstances. By contrast, the name of this given does not matter, and the fact that
the parameters are a using clause is redundant since no other clauses would be allowed.

8 Likes

The problem with that I think is that you can’t even read which type is implicitly given; Eq[A] is buried somewhere in the middle and hard to spot.

4 Likes

I agree with the line of reasoning, and find both the newly proposed syntax and the older proposed syntax vastly better than the Scala 2 syntax. So won’t complain much. But I also agree with @Sciss that it is harder to spot the actual ‘final type’ of the given.

2 Likes

True, but there is a vertical layout option as well:

given [A] 
  => K0.CoproductInstances[Eq, A] 
  => Eq[A] 
as eqSum

That would work well if the parts get large.

6 Likes

Putting the name at the end is inconsistent with everything else.

Scala’s greatest strength is its internal consistency. The most persistent criticisms of Scala are ways in which it is inconsistent or too flexible leading to inconsistency.

I still have no idea why any of this is an improvement over

implicit eqSum[A](implicit K0.CoproductInstances[Eq, A]): Eq[A]

(Making “def” and the parameter name optional is nice and has potential application beyond implicits.)

6 Likes

I find the argument of consistency quite convincing. Even though it might be already late, may I ask, why was the : dropped? I’m asking, because if we kept : for givens, we wouldn’t have to deal with as and how it’s used in pattern matching – very different use case.

It would the look like this, staying close to def and val work and thus staying consistent.

trait Ord[T] {
  ...
}

given intOrd: Ord[Int] {
  ...
}
// or
given Ord[Int] {
  ...
}

given listOrd[T](using Ord[T]): Ord[List[T]] {
  ...
}
// or
given [T](using ord: Ord[T]): Ord[List[T]] {
  ...
}

This approach basically only removes the names of the variables and uses given instead of val/def/… and so it should be easy to teach/explain.

4 Likes

There are two issues with using : instead of as

  • it does not distinguish visually well enough between the parameters and the thing we are defining.
  • it looks weird with indentation syntax which by now is a given (forgive the pun).

Here’s a screenshot that shows the difference.

And here’s the same with named instances:

My takeaway is that prefix as is not that bad, really. It’s biggest downside is for anonymous conditional instances, where the as connective does not really make sense. But one might get used to that.

So rather than opening up the discussion again, let’s do a poll. If you prefer the existing syntax, please like this post.

17 Likes

If you prefer the syntax with => and postfix as instead, please add a like to this post instead.

20 Likes

My takeaway from that screenshot is that colon as line continuation character can sometimes be in competition with colon as type introducer

10 Likes

There is a semantic ambiguity with =>: In given S => T => U, it is impossible to know whether the given instance type is S => T => U or T => U or U.

The type ?=> does not help: In given S ?=> T ?=> U could mean a given instance of the type S ?=> T ?=> U or T ?=> U or U.

We need to separate the instance type from its requirements explicitly with keywords.

The as approach does not suffer from the problem. However, the anonymous version needs improvement and the using in the condition block is unnecessary.

5 Likes

I like the new syntax. I think all the argumentation for it makes sense and the screenshots very vividly demonstrate the importance of syntax highlighting: the final type of given is easy to spot even if it’s followed by as with a name. And overall, IMO it looks lighter, easier on the eyes.

At this late stage, I don’t have strong feelings about which I personally prefer, though I’ve disliked Scala’s syntax for self-types for as long as it’s existed in its current form, and “[type] as [name]” style might offer a possible improvement here.

Here’s the current syntax:

trait Orange() { orange: Fruit =>
  // ...
}

which allows this to be renamed to orange (and hence not be shadowed by other potential thiss in nested classes), and also assigned a type Fruit as a “promise” that Orange must have Fruit, or one of its subtypes, mixed-in before it is instantiated.

I think that could be better written as,

trait Orange() as orange requires Fruit:
  // ...

which would avoid the syntax where the body of a template looks like a lambda. The requires keyword would be optional if only a self-name is required, and not a self-type, and it was the syntax used for self-types up to roughly Scala 2.2ish, before the current style was introduced.

I don’t want to complicate this discussion, but I think that even though self-types are not so frequently used any more, they fall into the category of things that are optionally named, and could benefit from having syntax that is more consistent with other optionally-named things in the language.

3 Likes

3 posts were split to a new topic: Scala 3 significant indentation