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

I agree with the general sentiment that the new syntax for given definitions (without as) is far superiod to the old one. Not perfect, but the old one honestly wasn’t great (entirely new and unfamiliar, unnecessarily inconsistent with the status quo, claiming new keywords that were already pretty common)

I also agree with @soronpo that do is the ideal block delimiter. Short, already a keyword, generally unused (and trivially re-writable), semantically relevant, entirely unambiguous. As I have mentioned in the other thread, “zero required braces” is the standard to aim for. As much as I like : (as a Python dev) I don’t think it quite reaches that level for Scala. Like it or not, multi-line lambdas are widely considered impossible in Python explicitly due to their choice of indentation-based syntax, while multi-line lambdas are extemely common in Scala.

I also agree with @oscar that leaving @ unchanged is for the best. Any change we make must not only be superior to the status quo, it must be sufficiently superior that:

  • It pays for the migration cost. This is significant, regardless of automated fixes or not. People’s brains and old blog posts and books in libraries or Scala notebooks in a SQL database are not amenable to Scalafix
  • We are confident we will not find a trivially-better alternative anytime soon. We cannot afford the churn of changing a major syntactic feature more than once in the foreseeable future (5-10 years?). If a change is good, but blocks off a better alternative that may appear, then it’s better to hold off. We can pull the trigger when (a) the better alternative appears or (b) we are confident a better alternative isn’t going to appear

IMO @ isn’t great, but neither is it a major factor in someone’s experience using the Scala language, and we can always make the change later as long as there’s a proper migration period.

5 Likes

I think do as a block separator looks really weird in purely functional code:

xs.map do x => x + 1

This “do x to <result>” expression does not make much sense to me, visually.

We could just reuse @, which is currently unclaimed in expressions:

xs.map @ x => x + 1
2 Likes

do is the most imperative word in the English language. IMO it doesn’t make sense as a general block delimiter. Perhaps it could be used for marking side-effecting code, though? (no idea exactly how)

2 Likes

what about double dot notation for such cases?
xs.map .. x => x + 1
or

xs.map..
  x => x + 1
.collect..
  case x if x == 2 => x

or even

xs.fold..
  case Some(x) => x + 1
..
  _ => y()

etc

I think with or in would be the keywords, even if with has the extra meaning of mixing in a trait. Otherwise the @ is concise, or bash-style \ backslash.

People tend to prefer what is familiar over what’s new. A tie for a new syntax indicates, to me, that it is actually preferred.

I’ve barely read any discussions on dotty/scala 3, but I happened to browse this thread and look at the diff, and the proposed new syntax is overwhelmingly easier to read, absent familiarity with either.

4 Likes

Adding to this, I had the impression a number of people preferred the old syntax solely because it’s too late/inconvenient to change things now. I can very much sympathize with how frustrating it must be to be developing tooling, documentation, libraries for a language that is still drastically changing. But it would also be a shame that Scala is stuck with suboptimal constructs for the next 15 years because the current timeline is too strict.

4 Likes

https://github.com/lampepfl/dotty/pull/10538 and https://github.com/lampepfl/dotty/pull/10565 have been merged, so we should be all set for givens and patterns without as.

6 Likes

I see a problem with a new syntax. With was not previously used for optional braces and it seems to create a new issue especially from the parser side.

How to distinguish a situation like this:

given intOrd: Ordering[Int] with
   A

from:

object A
given intOrd: Ordering[Int] with
  A

How can we know that with is supposed to start indentation and how that it should be followed by type?

In a given instance, a with followed by newline and indent is disambiguated to start the definitions. If you do want to write a given for several classes at once (I have not seen that in the wild yet), and you want the classes to be on separate lines, then you have to put the connecting withs first. I.e.

  given a: A
     with B
     with C 

The disambiguation code is in withConstrApps() in Parser.scala.

And, the contrarian in me wants to note, not without good reason. Familiarity is a primary driver of intuition. If something works like how other things you’re used to works, it will feel intuitive. Making use of those expectations or intuitions eases learning and adoption.

Which isn’t to say that should be the overriding concern here, but I find it useful to remember that when the old tricks are acceptable, sometimes it doesn’t pay to change them.

3 Likes

Sure familiarity is very important. But if something is only familiar to people on this forum because they saw it in dotty 0.26.0-RC1, that says very little about the vast majority of people who are not on this forum. Which I thought was the point @dcsobral was making.

1 Like

Although I’m overall happy with the new syntax, I’m sorry to say that I find with instead of colon to be irregular and confusing. I think beginners will have a difficult time understanding when : is used to start a indentation block, and when with is used.

I think this whole discussion shows that the objections raised against introducing new syntactic meaning to colon had merit. Otherwise, this would not have been an issue:

For the same reasons, I think introducing new meaning to with is confusing aswell. This resembles familiar established syntax in the language:

Having with mean one thing if it’s followed by a newline and something else if it’s followed by a class name is only confusing. I would be happy to hear someone expand on what made : better than with, but I would have been very confused by this:

class C
  with D
  with

With all respect for that adding new keywords should not be taken lightly, I think this big change gives merit to introducing a new keyword, for example where. That would make the syntax more clear:

class D extends A with B with C where
  ...
given a: A with B with C where
  ...

It would certainly be possible to give an alternate syntax to anonymous classes this way too:

def foo(x: Int) = new T {<defs>}

could be written

def foo(x: Int) = new T where
  <defs>

dropping the new:

def foo(x: Int) = T where
  <defs>

and if you want a structural return type:

def foo(x: Int): T where
  <defs>

One thing which I don’t really understand in the PR is how are type refinements written?
Can I write def foo(x: Int): T {type M} without braces in the new syntax? Is it possible?

5 Likes

The PR contains a detailed explanation why the difference is necessary, In particular it explains that a structural given must look different from a refinement type.

where was considered instead of : but after extensive experimentation we felt it was too heavy.

Yes, I read the PR and I of course understand that they must look different, but I still don’t understand how it’s possible to write refinement types without braces. If braces are necessary, then I’m fine with that.

Anyway, what I meant is that to the average user, it think it could be confusing.

Too heavy with a new keyword or just too many letters? My opinion is that the cost of writing those extra letters is paid for when reading the code, but I have full respect for different opinions.

If I word myself a bit more concisely, my main criticism is this:

When I see :, I think type. When I see with I think type.

To me it would actually make more sense to me if the syntax

T1 with ... with Tn with
  R

meant a type refinement like T1 with ... with Tn {R}, and

T1 with ... with Tn:
  <defs>

meant an anonymous class instantiation. I don’t know if that change would be too radical, but this is why I would’ve preferred the thing after with and : always being something with types, and another keyword like where being a marker for a block of definitions. It’s simply less confusing.

Would it be unthinkable not to have to write a trailing with {} in the structural instance case, it adds much boilerplate to my code.

1 Like

I think what I actually need is type inference (back). My project is to make a Scala DSL for computer algebra. What I had was:

given r as GenPolynomialRing[BigInteger](ZZ, Array("x", "y", "z"), INVLEX)
val Array(one, x, y, z) = r.gens

What I now have is:

given r: GenPolynomialRing[BigInteger](ZZ, Array("x", "y", "z"), INVLEX) with {}

What I could have alternatively is:

given r: GenPolynomialRing[BigInteger] = new GenPolynomialRing(ZZ, Array("x", "y", "z"), INVLEX)

What I would like to have is:

given r = new GenPolynomialRing(ZZ, Array("x", "y", "z"), INVLEX)

What we have in Python is

r = PolyRing(ZZ(), "(x,y,z)", lex);
[one,x,y,z] = r.gens()

One of the selling points of Scala is that is can be as concise as dynamic languages while being statically typed, thanks to type inference. We are almost there in most of the situations except for givens (type classes) which on the other hand happen to be really well suited for modelling algebraic structure.

In case of nested structures types can grow quite big and the average computer algebraist will not want to bother with them. So, would it be unimaginable to re-introduce type inference for givens ?

4 Likes

I think I’ve found a workaround. I can avoid given definitions by adding this as a given member to each algebraic structure and import it.

class TreePolynomial[C : Ring, M : PowerProduct] extends Polynomial[Element[C, M], C, M] {
  given TreePolynomial[C, M] = this
}

Before:

given r: TreePolynomial[scas.BigInteger, Array[Int]] = new TreePolynomial(using scas.BigInteger, Lexicographic("x", "y", "z"))

After:

val r = new TreePolynomial(using BigInteger, Lexicographic[Int]("x", "y", "z"))
import r.given

1 Like

Is it practical or even feasible to drop the using Keyword when defining a given?

If this was possible I think we should at least gather some feedback. It is more synonymous with defs.

So instead of:

given [T](using Ord[T]): Ord[List[T]] with {
  //...

}

one can just write:

given [T] (Ord[T]): Ord[List[T]] With {

//...

}

1 Like