Pre-SIP: Improve Syntax for Context Bounds and Givens

The benefit of
def showMax[X: {Ordering, Show}](x: X, y: X): String

is that it is a higher level syntax. And it’s a new syntax, which means it can be extended. We can-not really extend the syntax of using clauses, because they are baked in as parameters. I.e. it’s a low-level construct.

With the new syntax for aggregate context bounds, you can introduce specialised syntax for complex type constraints, which may result in a lot more boilerplate reduction down the road. (maybe that’s not the road we go down anyway.)

1 Like

Can you, please, be more specific? What extensions do you have in mind? I feel like I’m missing some important context.

Because currently all I see are method signatures that are maybe a few characters shorter. And that typically isn’t why languages introduce new feature/syntax, right?

5 Likes

Much more important than that, it puts what you need to know about A right next to A. That locality, keeping attention on a particular topic until you’re done with it, is by far the biggest reason to use it. It reduces cognitive load.

def f[A : Ordering](foo: Foo[A], bar: Bar, baz: Baz[A]): Quux[A]

immediately tells you, “This function in principle can work on anything with an ordering”.

def f[A](foo: Foo[A], bar: Bar, baz: Baz[A])(using o: Ordering[A]): Quux[A]

gets you thinking about Foo and Bar and Baz before coming back to “oh yeah, and we can order the A”.

Being able to keep this locality in more cases is the biggest advantage of the syntax.

Plus, [A : Ordering : Monoid] looks really weird. Are you saying there’s a monoid for your ordering, or that A is both orderable and is a monoid?

3 Likes

This feels quite local to me

def f[A](using Ordering[A])(foo: Foo[A], bar: Bar, baz: Baz[A]): Quux[A]

Indeed it does. I recommend this instead:

[A](using Ordering[A], Monoid[A])

Much clearer what’s going on.


Plus, this is all possible with Scala 3 today!

2 Likes

Right, but putting the using declarations right next to the type like that will in most cases mean far worse type inference, and force you to always specify the types explicitly.

For example, this does not compile, and instead spits back an ambiguous implicits error

  def f[A](using Ordering[A])(foo: A, bar: A): Boolean = summon[Ordering[A]].lt(foo, bar)

  f(1, 2)
2 Likes

OK, that’s a fair point.

Maybe the compiler/type inference could be improved to be able to handle such cases. That would be really awesome. It would improve the UX for people in general, not just for this particular use case of type classes.

But it would have to be the language/compiler developers to say how feasible that is. /cc @smarter

EDIT:
The error is

Ambiguous given instances: both object String in object Ordering and object Boolean in object Ordering match type Ordering[A] of parameter x$1 of method f

Explicit type annotation fixes this, but it’s unfortunate that it’s needed.

f[Int](1, 2)

https://scastie.scala-lang.org/Pxvr06XlRH2hj2OHcQEFow

1 Like

I agree, it would be awesome, but I have no idea how this could be accomplished. The problem is, we have a tradeoff with no uniformly best solution. Typing more arguments before the using clause constrains the type for the implicit search which might be needed to make it pass. Typing more arguments after the using clause means that the implicit search had a chance to constrain or instantiate some type variables, which gives better expected types for these arguments. Better expected types might be needed to typecheck lambdas or to do implicit search in the arguments, for instance. So the best (in fact only) solution is to fix a left-to-right order and stick to it. Or else existing programs will start breaking.

I just tried that and confirmed that it compiles. I would not have believed that would work. I would classify it as a bug, though.

This is working already and seems to make with unnecessary(?)

trait A:
  def foo: Int

given a: A = new:
  def foo = 1

It looks similar but is not the same.

given a: A with
  ...

Is an A with all the refinements implied by its body.

given a: A = new:
  ...

is just an A. This makes a difference in actual code bases.

This is actually better on an encapsulation point of view.

Is there a reason new does not refine all the time ?

I know sometimes inferring a more precise type changes behavior, but I was under the impression that this was only linked to implicit resolution

If new would refine all the time, we’d have to make any members that we add locally to an anonymous class private, or they would show. It’s actually quite tricky to derive a refinement from a local class.

It’s the Σκύλλα και Χάρυβδις of typing.

2 Likes

Note that there is this syntax:

trait A:
  ...
trait B:
  ...
trait C:
  ...
val a = new A with B with C:
  ...

I think the following is deprecated though:

class D extends A with B with C:
  ...

, in favor of:

class D extends A, B, C:
  ...

But AFAIK the new syntax above remains.

Thank you for the comments. Based on these discussions, I went ahead and proposed a SIP. The main differences of the SIP compared to the proposal here are:

  • Default names for context bound witnesses were dropped and deferred to a later SIP. There’s no consensus yet on what is the best technique for supporting such names in the presence of multiple context bounds.
  • The improvements to the deferred givens were incorporated.
  • There is some discussion regarding the alternative of dropping context bounds and restricting the language to just using clauses.
6 Likes

Status update: the SIP for this change is well underway to be accepted for an upcoming minor version of Scala, likely 3.6.0. An initial vote for the design was accepted in a previous SIP meeting, although that is not the entire process.

It is already available in the RCs of 3.5.0 under the following experimental flag:

import language.experimental.modularity

While some of the proposed changes are close to getting consensus for full approval, some are more controversial (see the SIP thread).

We encourage you to experiment with the features using the 3.5.0 release candidates and subsequent releases, and provide feedback.

5 Likes

@bjornregnell has encourage me to share my suggestion about the new syntax for givens. I have to admit, it’s a bit bike-sheddy, but I still hope it can result in a cleaner, more regular and thus user friendly syntax, so I think it’s worth it.

SIP-64: Improve the Syntax of Context Bounds and Givens by odersky · Pull Request #81 · scala/improvement-proposals · GitHub

I’m not sure if this is the right place to post this, or whether it’s perhaps too late. But I was wondering, whether we could have an alternative proposal which looks like this.

The benefit I see it this is that it keeps the syntax very regular and intentionally resembles definition of methods (but with given instead of def). That will make it easy for (not only) beginners to work with.

The most important motivation for me was the intentional similarity between givens and ordinary method definitions (defs).

  // Simple typeclass
  given intOrd: Ord[Int]:
    def compare(x: Int, y: Int) = ...

  // Parameterized typeclass
  given listOrd[A: Ord]: Ord[List[A]]:
    def compare(x: List[A], y: List[A]) = ...

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

  // Simple alias
  given intOrd: Ord[Int] = IntOrd()

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

  // Alias with using clause
  given listOrd[A](using Ord[A]): Ord[List[A]] =
    ListOrd[A]()

  // Concrete class instance
  given context: Context // this would be a change of meaning from abstract given

  // Abstract or deferred given
  given context: Context = deferred

  // By-name given
  given context: => Context = curCtx
7 Likes

I think I also prefer the def-like idea, it seems slightly more intuitive to me, and more similar to the current syntax

I furthermore think it would be a shame to pass by an improvement* just because it’s “late”, especially since this is only a syntax change, which tend to be easy to do (as long as the syntax is experimental of course, speaking from experience)

*This assumes there is some for of consensus on this being an improvement, that remains to be seen, but I am hopeful

3 Likes

Yes I think it’s good if we discuss more here after many of us have tested out the new proposal. See here: SIP-64: Improve the Syntax of Context Bounds and Givens by odersky · Pull Request #81 · scala/improvement-proposals · GitHub

See esp. under “Cleanup of given syntax” where @odersky has explained some rationale for using arrow instead of colon.

I agree that for named givens the def-analogy is working OK, but the normal case is unnamed givens and there it looks more strange, esp. when combined with type parameters and context bounds etc.

There is also precedence of the syntax for polymorphic function types where the type parameter comes first, and then an arrow, like as in:

scala> val f: [A] => A => String = [A] => (a: A) => a.toString
val f: [A] => (x$1: A) => String = Lambda$2165/0x00007f67186855a0@2301e362

I think, as always, that new syntax needs some time to get used to, as we are biased by old syntax. One interesting angle is to imagine how a cleanup should look to be appreciated by a newcomer to Scala…