Something still not right with givens

There is something still not right with givens after the Given without as rework, which I think is important to address prior to RC1 as it is interfering in the discussion on optional braces.

What triggered the rework was the relevant remark that as makes better sense as Type as value rather than value as Type as was the case in givens. It is a good remark.

However, what comes after as in givens in some cases is not a type (nor a value) but a constructor. Formerly it was the implicit class case. Now it called structural instance:

given intOrd: Ord[Int] with
  ...

The other forms of givens are given alias:

given global: ExecutionContext = ForkJoinPool()

and (introduced by the PR above) abstract given:

given global: ExecutionContext

In these two cases colon (:) makes perfect sense in replacement of as as it is followed by a type. But in the structural instance case we have a constructor. What happens if we have no structural part to define:

given intOrd: MyConcreteOrd

, is that in order to disambiguate w.r.t. an abstract given we must append with + an empty body:

given intOrd: MyConcreteOrd with {}

There are several problems with this.

  1. it is not pretty
  2. it is verbose
  3. it forces us to use with whereas colon (:) would be more appropriate to start an indented body.
  4. it does not work without braces:
given intOrd: MyConcreteOrd with
  <nothing>

or

given intOrd: MyConcreteOrd:
  <nothing>

-> don’t work.
5) class declarations don’t have this constraint:

class intOrd extends MyConcreteOrd

So the question is : how do we disambiguate from abstract given without with? One answer would be to bring back as in the structural instance case (only). One other possibility would be extends:

given intOrd extends Ord[Int]:
  ...
given intOrd extends MyConcreteOrd

Anonymous would be unchanged:

given Ord[Int]:
  ...

Given alias could be replaced by this more compact form if desired:

given ForkJoinPool

Reactions?

6 Likes

I don’t have a solution ready, maybe change how abstract givens are done… But I agree that given intOrd: MyConcreteOrd with {} is terrible.

3 Likes

Using extends in given implementations was proposed several times, including by myself, instead of going with weird syntactic idiosyncrasies like as or with.

It was rejected because it allegedly exposes the mechanism rather than the intention…

I’m sorry, but with there absolutely does not express any clear intention at all, and it just makes everything really murky and confusing.

On the other hand, the duality between : for declaring without implementing and extends for implementing makes perfect sense to anyone remotely familiar with Scala.

abstract class Foo:
  def foo: Int

abstract class C:
  given G: Foo[Int]
    
class D extends C:
  given G extends Foo[Int]:
    def foo = 42
8 Likes

Note: I’m saying this because with does not seem likely to be selected as the way of opening template bodies (due to apparent lack of popularity); but if with was selected for this general-purpose role, then I’d be okay with using it for given too:

abstract class Foo with
  def foo: Int

abstract class C with
  given G: Foo[Int]
    
class D extends C with
  given G: Foo[Int] with
    def foo = 42
2 Likes

Agreed, but in this case I would even write it:

class D extends C with
  given G extends Foo[Int] with
    def foo = 42

Regarding exposing the mechanism vs intention, I would say that substituting : for as is already halfway to it, and we should bite the bullet.

Edit: maybe only a given alias should be allowed to implement an abstract given. The syntax above should be allowed only if there is no abstract declaration to implement. If there is, better use this form:

class D extends C with
  given G: Foo[Int] = new Foo[Int] with
    def foo = 42

@rjolly also;

What would that look like for anonymous givens?

Nothing would change. Just substitute extends for :

trait Ord[T]

abstract class Foo[T : Ord]:
 def foo: T

given [T : Ord] extends Foo[T] with
 def foo: T = ???

Edit : Maybe the best would be to just drop abstract givens. That way, definitions with no added structural part could not be confused with an abstract given.

class ConcreteFoo[T : Ord]:
 def foo: T = ???

given [T : Ord]: ConcreteFoo[T] // concrete instance without with

Formerly I also proposed constructor inference:

given [T : Ord]: Foo[T] = new
 def foo: T = ???

I am considering making a poll to compare the different options.

Edit2 : Please find the poll below. To participate, please like the post corresponding to the option you think is best.

2 Likes
  1. status quo
given [T : Ord]: ConcreteFoo[T] with {}
2 Likes
  1. bring back as for structural instances only; given alias stays with colon
given [T : Ord] as ConcreteFoo[T]
  1. bring back as entierly; rollback to the pre-“given without as” PR situation
  1. use extends
given [T : Ord] extends ConcreteFoo[T]
4 Likes
  1. use colon but drop abstract given; with becomes optional
given [T : Ord]: ConcreteFoo[T]
  1. constructor inference (i.e. drop structural instances)
given [T : Ord]: ConcreteFoo[T] = new
1 Like

Maybe an addition to option 4 (given [T : Ord] extends ConcreteFoo[T]) you can only extend ConcreteFoo if it is not final, and if it is you should rely on an alias. To be consistent with how extends and final currently work:

given [T: Ord]: ConcreteFoo[T] = new ConcreteFoo[T]

edit Not sure what the status quo does when you have a final ConcreteFoo[T] and give a refinement. Is there a spec for this situation?

final class ConcreteFoo[T]
given [T : Ord]: ConcreteFoo[T] with {
   // .. refinements such as
  def bar: Bar
}

Sorry as it is not clear, but option 4 says nothing about given alias. It is unchanged (and equal to what you wrote).

If ConcreteFoo[T] is final, then you just can’t define a structural instance. You have to use a given alias.

Edit:
Remark1: options 2 and 4 imply that anonymous abstract given are not possible. These options are not incompatible with option 5 (drop abstract given entirely)
Remark2: removing with has the amusing consequence that given becomes yet another keyword that can start an indented block:

given Object:
  ...

is:

given
  ...
1 Like

At this point I’d prefer if implicit remained as the keyword of choice. The given/using syntax replaces one keyword and its peculiarities with two new keywords that are arguably just as confusing if not more.

The original two main sources of confusion—1. implicit conversions and 2. use-site parameter supplying—are solved.
1 is mitigated by the use of the Conversion type constructor and 2 could be mitigated by enforcing a modifier implicit before an argument, much like given can be used to denote that an argument is in a given position.

definition site:

implicit def global: ExecutionContext // abstract
implicit val intOrd: Ord[Int] = new { … } // implemented trait
implicit lazy val global: ExecutionContext = ForkJoinPool() // "alias", lazily created
implicit def _[T](implicit ord: Ord[T]): Foo[T] = … // anonymous "dynamic"

use site:
// with def foo(x: Int)(implicit y: Int): Int = x + y
foo(1) // implicit parameterisation
foo(1)(2) // syntax error
foo(1)(implicit 2) // ok
foo(implicit 2)(1) // ok

Maybe I’m being overly conservative here, but I think the above is a lot simpler. I don’t need to pair given with using or remember/explain why the call-site uses one keyword rathert than the other. On the definition site I don’t need to remember which incantation of given results in a def or a val, and I don’t have to tack on an extra with for no apparent reason…

I think the given version is more akin to:

implicit object intOrd extends Ord[Int] { … }
implicit class _[T](implicit ord: Ord[T]) extends Foo[T] { … }

IMO the added value of given is manifold:

  1. It hides the val/def and object/class distinction
  2. It allows anonymous syntax without _
  3. It limits “parasitic” modifiers like your lazy above for separation of concern
  4. It used to forbid abstract which was a good thing IMO
  5. It requires type ascription (that is, no type inference is allowed, to avoid loops IIUC)

Other than that, I agree that the the correspondence with implicits is pretty clear and there is no point obfuscating it just for the sake of it.

1 Like

It turns out anonymous abstract given are already impossible:

trait Ord[T]

abstract class Foo[T]:
  given Ord[T]
  def foo: T
^^^
anonymous given cannot be abstract

What is the point of abstract given ? I thought it could be used as an alternative to constructor parameters, but I can’t come up with a solution.

The constructor parameter based design:

class Foo[T : Ord]:
 def foo: T = ???

given [T : Ord]: Foo[T] = new Foo[T]

The abstract member based design:

abstract class Foo[T]:
  given ord: Ord[T]
  def foo: T = ???

given [T : Ord]: Foo[T] with
  given ord: Ord[T] = summon[Ord[T]]
                                    ^
ambiguous implicit arguments: both lazy value ord in class given_Foo_T and value evidence$1 in class given_Foo_T match type Ord[T] of parameter x of method summon in object Predef

It does not work. It is too bad, because it goes in the way of better alternatives in the concrete case:

given [T : Ord]: Foo[T] = new Foo[T]
// would be better written as:
given [T : Ord]: Foo[T]
given [T : Ord] as Foo[T]
given [T : Ord] extends Foo[T]
// currently we're forced to write:
given [T : Ord]: Foo[T] with {}

Edit : to summarize, I have eventually no problem with with even in the presence of colon as indented body delimiter in the non-given case, as long as I can omit it (with) when there are no further structural additions. Does that sound possible? Can we drop abstract givens?

I dislike the idea of anonymous abstract definitions of any kind. When you are overriding something it really should be very explicit what it is, and that’s just not the case if there isn’t a name on it.

I presume named abstract definitions are ok? I get errors when I try it, but maybe I’m just confused about the syntax.

On the other hand, you can define an anonymous given alias referencing an abstract definition:

  def ord: Ord[T]
  given Ord[T] = ord
3 Likes

Here is a request for removal of abstract given https://github.com/lampepfl/dotty/issues/10954 . I think it would be better addressed before RC1 and the whole planet starts using it. This associated feature request can wait a little bit more https://github.com/lampepfl/dotty-feature-requests/issues/156