Pre-SIP: Improve Syntax for Context Bounds and Givens

See also the enlightening section “Comparison” at the end of the proposal, just before the Summary section.

As I see it, the pros and cons of colon versus arrow is depending on how we value the named and unnamed cases and in combination with the new as syntax where names are by the end, the syntax ergonomics of the unnamed case may be even more important.

Took a look. Thanks!

I use named givens all the time (just did an hour ago), the new as syntax for named givens is quite bad… Current syntax is much better.

From a learner’s perspective, the “simple alias” use case is the earliest and the most common. as will have a cognitive recall of things like asInstanceOf which suggests some sort of “casting” or “transformation” rather than “naming.” Whereas, current syntax is completely regular and in line with how normal vals and defs are written:

// good for learners
// keyword name: type = expression
given jolts: Jolts = getJolts(lines)

This is not good:

// Simple alias
// bad for learners
// keyword type `as` name = expression
given Ord[Int] as intOrd = IntOrd() // massive irregularity
  • The infix : also feels strange. To a newcomer, it’s not clear what it signifies.

I totally disagree with this. : is extremely familiar and helpful for learners.

The proposal seems, admittedly, biased heavily in favor of anonymous givens:

Arguably anonymous is the more important case. …

I think this focus is unfair: it says with is an irregularity, but then that irregularity is pushed from anonymous givens to named givens. Why?

Again from a learner’s perspective, the “removing repeated parameters that never change with using” use-case of givens comes much earlier than type classes. This is a much easier way to “ease” learners into the idea of “context”.

When givens are used along with a matching using clause, we often have to use named givens, because they have to be referenced in multiple places in the same code; anonymous givens are not an option. Here’s an example from a Fibonacci-like Advent of Code puzzle with memoization I just solved, it was a perfect match for this use case:

  private def paths(jolt: Jolt, index: Int)(using memo: Memo, jolts: Jolts): Jolt =
    memo.get((jolt, index)) match
      // ...

// ...
  def solve2(lines: Seq[String]) =
    given jolts: Jolts = getJolts(lines) // name needed for below
    given Memo = collection.mutable.Map((jolts.last, jolts.size - 1) -> 1L)
    paths(jolts.head, 0)

The proposal does improve things for type classes, but “removing repeated parameters with using” is getting very poor treatment in my opinion. Named givens seem to be getting the short end of the stick here (having a big irregularity shoveled on them), and I’m against it.

If there has to be an irregularity somewhere, it makes more sense to have it for type classes, because they are fairly advanced and special. That’s much better from a learner’s perspective. Cognitively, this justifies the irregularity in the learner’s mind. I remember explaining to a learner in Coursera Discussion Forums a few months ago, with with and extension together, and they grasped it fairly easily.

Just my two cents. :v:

5 Likes

I have to agree. I also use named givens often enough to care about them, and anything that makes them weird and irregular would make my code, well, weirder and more irregular.

Either make as ubiquitous–you can use it anywhere, like val Int as x = 15–or please don’t saddle givens with ad-hoc syntax. I would prefer that this not be ubiquitous, because the sense of as used in given Type as varname is kind of unexpected; usually one would think given specific as Category.

5 Likes

:clap:
Yeah I’m banging my head trying to figure out how to explain the new syntax to a learner… :face_with_head_bandage:

Current syntax is so nice, seems like they struck gold with Scala 3 in my opinion. (with is not a problem at all)

2 Likes

It seems like quite a bit of the oddness in the new syntax comes from how odd the old syntax looks if the name is dropped.

given Ord[Int] as intOrd = IntOrd()
given Ord[Int] = IntOrd()

Can you remind me why did we discarded the idea of keeping regularity with methods by allowing devs to use a placeholder?

given intOrd: OrdInt = IntOrd()
given _: OrdInt = IntOrd()

It would mean we can use any improvements around context bounds in givens as well, without having to build a separate mental model for how they’re written.

1 Like

Interesting point!

This would be just fine for an anonymous given! I was just thinking the same thing! (right after I finished my comment)

:+1:

That I am not sure I agree with, type classes are not as hard to understand if uncoupled from implicits (the way it is implemented in other languages) (this is also why there’s efforts in trying to make type classes usable without ever seeing implicits)

3 Likes

Realizing I don’t know what the word “as” means, I looked it up. OED calls it “a worn-down form of all-so.” So, “as bright as gold” was originally “[all-]so bright so gold” (swa breoht swa gold). It works its way into various idioms and usages, including combinations such as “inasmuch” where you wonder is it a real word.

“Starring Fred Astaire as Tony Hunter” establishes an alias for the evening, so that we’re not confused when people refer to the actor Fred using the name “Tony”. He is starring in the role or capacity as Tony.

I am persuaded by given Fred as tony. In the course of the film, I will have forgotten the name and think of him only as Fred Astaire, the dancer and entertainer, just as during “Top Hat”, I don’t think of him as “Jerry Travers”, although mistaken identity drives the plot.

I like the explanation that some things are not meant to bear names; the name is secondary. In suffix form, it is a reminder that the name, if necessary, was an afterthought.

I would propose preserving this typo in the SIP:

the important thing here is the type hat’s implemented.

image

Type Hat’s one of my favorite movies.

The other English word I learned on that page of the dictionary was “arveth” for “difficult”, a cognate of German “Arbeit”.

2 Likes

The default case of givens is nameless. That’s what they are in every other language that implements type classes. One should think of a given clause representing a typeclass instance as analogous to an extends clause in a class declaration. Both establish a relationship between two types. One would not think that an extends clause merits a name of its own, and the same is true at a high level for given clauses.

So to use _: feels to me like it’s the wrong way round. It makes the named case the norm.

2 Likes

It seems the major concern is about switching to as for the optional names of givens. Examples:

given Ord[Int] as intOrd:
  ...
given Context as ctx = ...

vs

given intOrd: Ord[Int]:
  ...
given ctx: Context ...

There are two reasons for using as:

  • It avoids the awkward double “:” in clauses like the one for intOrd. That was the major motivation to replace the second : with with in the current syntax, but unfortunately that introduced an irregularity which I find the more jarring the longer I work with it.
  • It emphasizes what’s important and puts that first. I.e. if I write or read a given clause the first thing I focus on is the type that’s implemented, the name is secondary.

On the other hand, it’s true that as introduces another irregularity. The analogy with as in import renaming and context bound naming makes it stand out less, but it is an irregularity nevertheless. We do use : for other optional names, e.g. in using clauses

  def f(using ctx: Context)

or in function types

  (x: A, y: B) => C

To be completely regular, we’d have to replace these with as as well, but nobody is suggesting that since the change would be too big.

So it might be OK to keep :, after all. The first counter argument, that the double : is weird, holds only for named type classes with a class body after the :, and I believe these should be mostly anonymous anyway. Named context instances like given ctx: Context = ... are probably more common than named type classes, and these look fine with either syntax.

The second counter argument - that the important / mandatory thing should come first - has merit, but is already violated by the other usages for optional “name:” prefixes. So we might just decide to live with the shortcoming.

SIP 64 has at the end a detailed comparison between suffix as and prefix : with a line for each usage category. It would be good to scrutinize that and voice your preference here.

4 Likes

First, I just want to point out that Scala does not make its typeclasses universal. Obviously you don’t need to make a universal typeclass named; there’s only one of it! There’s nothing to name. So you only need to compare Scala to languages that have typeclasses but do not have universal typeclasses when thinking about how important naming is.

Anyway, my aesthetic sense of the nine named options with as vs : is:

  1. Simple typeclass. Both bad. as is irregular. : used for both type declaration and block anonymous class block opening is weird when used in such close proximity. No real distinction for me.
  2. Parameterized typeclass. Both bad. Same reason: as is irregular, : feels like declaring a type. Slight edge for as because the construct already looks weird so the extra weirdness of as doesn’t stand out as much.
  3. Typeclass with using clause. Both bad, slight favor for as, for the same reason.
  4. Simple alias. : is great–just like creating a val! as is irregular, for no apparent reason.
  5. Parameterized alias. : is great, but as is tolerable because, again, of the “huh, [T] => C[T]?” reaction making one more open to alternative syntax.
  6. Alias with using clause. Same as parameterized alias, for same reasons.
  7. Concrete class instance. as is bad but tolerable; it seems like it should be abstract but because as is weird there is some doubt. : is terrible–as mentioned, it looks just like it’s abstract (that’s how you do abstract everything else!). Confusing these two is really bad.
  8. Abstract or deferred given. : looks natural, as looks irregular.
  9. By-name given. By-name already looks weird, so the difference between the two is dominated by the weirdness of by-names. : is slightly more regular, so mild preference for that.

Overall, as a straight choice between as and :, the results are mixed for me but I’d prefer : for regularity. (I do still wish we hadn’t chosen : as the block-opening symbol, however.)

The worst pain point is (7), the concrete given that is indistinguishable in form from an abstract anything else. We really should not introduce this irregularity.

2 Likes

@Ichoran Agreed! Well put :+1: , except for 7. I think : is better. But yes all 3 choices kinda suck.

Yes. Thanks for acknowledging.

Yay! :tada:

In Proposal, Named in Current Style, I think for 1. and 2. the current with is better than both as and :. Given… with… is how mathematicians express it in prose. (I could give hundreds of examples from textbooks. Haskell uses instance… where… which is another common math prose.) The double : is still tolerable despite awkwardness, since both :s are close to their “normal meaning” (type ascription, block opening)

I think what really feels weird is the following:

  // Typeclass with using clause
  given [A](using Ord[A]) => Ord[List[A]]:
    def compare(x: List[A], y: List[A]) = ...

Method syntax would look like:

  def [A](using Ord[A]): Ord[List[A]] =

And function syntax would look like:

  val [A] => (using Ord[A]) => Ord[List[A]] =
  val [A] => (Ord[A]) ?=> Ord[List[A]] =

(I don’t remember which, but either proves my point)

So we see it doesn’t look like either

As for the anonymous by default, I must say I agree with it, for me it doesn’t really make sense to name contextual values, and declaring them separately seems like something we could strive for (example from the SIP)

  val foo: Foo = ...
  given Foo = foo
1 Like

Here’s my counter proposal, whether or not we keep as (I’m in favor), change with for :=, and also allow it for classes etc:

  // Typeclass with using clause
  given [A](using Ord[A]): Ord[List[A]] :=
    def compare(x: List[A], y: List[A]) = ...

  // Parameterized alias
  given [A: Ord]: Ord[List[A]] =
    ListOrd[A]()

For classes and traits

  class Foo[A](using Ord[A]) :=
    def bar = 1
  
  new Foo[Int] :=
    override def bar = 2

Of course we can have something else than :=, but it seems like a good candidate, and it feels like introducing an overhaul of braceless is kinda our best way out of with

May be true… but let’s not overhaul, braceless is great as is, with is also fine to keep (very small irregularity, much smaller than as, but with nice mathematically familiar language).

Again, I think with Scala 3 they struck gold. Current syntax is great! :heart:

2 Likes

When creating givens of a concrete class instance, the " Current, Named" looks awkward
given context: Context with {}
and the variant “Proposal, Named in Current Style” is
given context: Context
which problematic as “this would be a change of meaning from abstract given”.

I wonder if, in the case of concrete class instance, we could require explicit instantiation using a (possibly empty) class param list as in:

// Concrete class instance  (named, colon-style)
  given context: Context()

// Concrete class instance  (named, as-style)
  given Context() as context

// Concrete class instance  (unnamed)
  given Context()

Then the “Proposal, Named in Current Style” would not clash with abstract givens that are proposed to be abolished, but needs to be kept in a transfer grace period. And it makes sense to explicitly show that its an instance. And the error/deprecation messages could inform about the new syntax, to make it less confusing.

What do you think?

2 Likes

@bjornregnell I think that makes sense; it’s a change that’s in line with the current (anonymous) syntax as shown in the SIP
Screenshot from 2024-07-01 16-51-18

So your suggestion would be the named version of the first one. And it avoids the “meaning change” for abstract givens as you said! Great :+1:

(I agree with {} is bad; not so much the awkwardness but the mysteriousness of {}; this is difficult to explain to learners.)

1 Like

I have to disagree ^^’

Don’t get me wrong, I love braceless, in Scala and in other programming languages, but the way it’s done in Scala feels inconsistent.
To only cite one example, people are regularly confused by extensions and wheter or not they need a :

Regardless, this would be a small, potentially optional, change, that would improve given declarations !

1 Like

Yes, I think requiring a () in the case of a concrete class instance is quite acceptable, and certainly the least bad option. Once abstract givens are deprecated, we could even suggest it with a hint when the user writes something like

given ctx: Context
           ^^^^^^^
          Abstract givens are deprecated. You should use a deferred given instead:
          
              given ctx: Context = deferred

          Or, if you mean a concrete given, follow the type with `()`:

             given ctx: Context()
4 Likes

It seems like it would be syntactically possible to allow double : even if they do look a bit ungraceful

The one conflict I found is the following:

given foo:
  bar

// is this 1)
given foo with // foo is a type and bar is an expression
  bar
// or 2)
given foo: bar // foo is a name and bar is a type

To solve it, we can do:
If there is an identifier followed by a collon, followed by a line break, the rest is the with body
If there is no line break or if there is a clause following the identifier, the rest is the given type until the second colon, after that is the with body

Arguably, that’s a bit hacky, what do you think of using double colons now ?
(you being anyone, not necessarily Martin)

Edit: You’ll notice this only happens if abstract givens are allowed, if they are deprecated, it frees up some space