Skip to content

Abstract given seems useless #10954

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
rjolly opened this issue Dec 30, 2020 · 25 comments
Closed

Abstract given seems useless #10954

rjolly opened this issue Dec 30, 2020 · 25 comments
Assignees

Comments

@rjolly
Copy link
Contributor

rjolly commented Dec 30, 2020

Abstract given were introduced in #10538 . But these seem useless, as one can always define an abstract field and use a given alias:

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

Would it be possible to drop this feature, as it forces us to add an uncanny with to givens even when no structural part is needed lampepfl/dotty-feature-requests#156

@odersky
Copy link
Contributor

odersky commented Dec 30, 2020

I let @nicolasstucki and @julienrf respond. They have been arguing strongly for abstract givens.

@julienrf
Copy link
Contributor

julienrf commented Dec 31, 2020

I find abstract givens useful, indeed. Abstract definitions are a common means of abstraction (abstract methods and abstract type members). We can observe that in Scala 2 abstract implicit definitions are used (e.g. in cats, endpoints4s, akka, and maybe at other places that I’m not aware of).

From a more fundamental perspective, there is a correspondence between parameters and abstract members. Both are means of abstractions, and you can refactor from one form to the other:

trait WithMembers {
  type A
  val foo: A
}
class WithParameters[A](val foo: A)

However, if we remove abstract givens, this will make it impossible to refactor this:

class Foo[A](using SemiGroup[A])

into that:

trait Foo {
  type A
  given SemiGroup[A]
}

You mentioned a workaround, which in my opinion is not only verbose, but very low-level and “mechanism-oriented” (as opposed to “intent-oriented”). I like how abstract givens removed half of the lines of this part of the compiler.

@julienrf
Copy link
Contributor

That being said, if abstract givens are the only reason why we need the with connector, I agree that it’s an important drawback. Is it true that we could remove with if we didn’t support abstract givens?

@bmeesters
Copy link

I agree with Julien. Also, even if I am not very fond of the empty with {}, leaving it out is IMO pretty confusing:

given [T : Ord]: ConcreteFoo[T]

looks too much like an abstract def in my opinion, so reusing it for a slightly shorter syntax is not worth it IMO. Maybe we can find another solution to get rid of the with {}?

@rjolly
Copy link
Contributor Author

rjolly commented Dec 31, 2020

Is it true that we could remove with if we didn’t support abstract givens?

We could define a structural instance like so without risk of confusion:

given [T : Ord]: Foo[T]:
  ...

I like how abstract givens removed half of the lines of this part of the compiler

Only you (or Nicolas) can tell if there is a better way to express this. If not, then perhaps we need it after all. But in that case, maybe we could bring back a keyword such as as, or better extends, to distinguish empty structural part from abstract given:

given [T : Ord] extends ConcreteFoo[T]

Edit : in this case, the problem is that given alias (and abstract given) on the one hand, and structural instances on the other hand, now are syntactically separate domains, and it would be difficult to implement an abstract given with a structural instance.

@rjolly
Copy link
Contributor Author

rjolly commented Dec 31, 2020

Maybe we can find another solution to get rid of the with {}

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

@rjolly
Copy link
Contributor Author

rjolly commented Jan 1, 2021

After reflection, I still think the workaround is better, for separation of concerns. Abstraction is a thing, and context abstraction is something else. Having both conflated in the same construct is problematic. Especially since givens are meant to disentangle implicits from other concerns (among which is abstract).

Happy new year.

@odersky
Copy link
Contributor

odersky commented Jan 1, 2021

I believe one aspect is that the syntax

given table: Table

for a concrete given is objectionable since it looks like an abstract given. That was a common complaint when : was still as.
It's true that

given table: Table with {}

looks cryptic, but at least it's not misleading. A better way to write this is with an alias, even if it is slightly longer:

given table: Table = Table()

So no matter what we do, we probably should either outlaw

given table: Table

outright, or make it an abstract given. I was fielding the same "separation of concerns" argument as @rjolly to defend why there should be no abstract givens. On the other hand, since it is useful, easy to spec and implement, and the natural syntax is already available, why not?

@rjolly
Copy link
Contributor Author

rjolly commented Jan 1, 2021

Then perhaps we should disable abstract and re-introduce extends (for non-alias):

given table extends Table

For me, the main problem is that this mandates braces, right in the middle of a whole braceless effort:

given table: Table with {}

Edit: and it look abstract too, since there is no equal sign.

@odersky
Copy link
Contributor

odersky commented Jan 1, 2021

And for anonymous? given extends is pretty yucky!

The root issue is that for givens x: is an optional label. Everything has to work with and without that label, and every discussion should include that aspect from the start.

@odersky
Copy link
Contributor

odersky commented Jan 1, 2021

I have the impression that the whole discussion here and the contributors thread is essentially trying to walk back to what we had before with as but with different keywords. I firmly believe that as was already the best choice for a keyword, but that ultimately : is better than any keyword even if it does lead to the irregularity with with.

@LPTK
Copy link
Contributor

LPTK commented Jan 1, 2021

@odersky

given extends is pretty yucky!

For clarity, here is what it ends up looking like:

given extends Ord[Int]:
   def compare(x: Int, y: Int) = ...

given [T](using Ord[T]) extends Ord[List[T]]:
  def compare(xs: List[T], ys: List[T]): Int = ...

I think the reason given extends feels strange at first is that we normally read "given" not as a noun, but as a preposition. In English, we often see the forms "given <some context>, then <some conclusion>" or "given that <some fact>".

However, IIRC you have argued that Scala declarations normally start with nouns (class, object, trait, def, package, etc.), and that "given" should therefore be understood in this capacity – this was actually a hard criterion used in the choice of an appropriate keyword. In a given declaration, we are defining a given (instance).

Under that interpretation, given extends T is not jarring anymore, as it's understood as "this given instance extends T", just like one reads object ... extends T as "this object instance extends T". In fact, this forms pushes the reader to treat the word "given" in its intended grammatical role, making it less of an exception in the language.

// the following
given Obj extends Ord[Int] { ... }

// is conceptually similar to the following
object Obj extends Ord[Int] { ... }

// and if we had anonymous objects, we'd write
object extends Ord[Int] { ... }

// just like we would now write
given extends Ord[Int] { ... }

So I think one will actually get used to this form quickly.

Since it happens to solve many problems at once and has been proposed over and over by the community, why not give it a serious try?

@odersky
Copy link
Contributor

odersky commented Jan 1, 2021

@LPTK I don't understand how this not a clear step backwards from what we had with as?

@rjolly
Copy link
Contributor Author

rjolly commented Jan 1, 2021

For clarity, here is what it ends up looking like:

I think extends should be omitted in your first example, just like colon would be:

given Ord[Int]:
   def compare(x: Int, y: Int) = ...

I agree this looks a bit weird, but colon is no better (and as was eventually worse):

given [T : Ord] extends Ord[List[T]]:
  def compare(xs: List[T], ys: List[T]): Int = ...

I have the impression that the whole discussion here and the contributors thread is essentially trying to walk back to what we had before with as but with different keywords

I think colon is okay for given alias (as it's a type ascription) and eventually abstract given for that matter. Structural instance OTOH would be better written with extends. That way the two concepts are clearly separated. Think you will have to teach this.

@odersky
Copy link
Contributor

odersky commented Jan 1, 2021

We can rationalize extends all we want but

given extends C:
  def f ...

is revolting and a lot worse than

given C with
  def f...

I have seen no argument why with is not the best available choice. The argument that

given C with {}

is ugly and requires braces is not very strong because

  • that's a fairly rare case outside tests where we often construct dummies like this.
  • there's a better alternative in given C = C().

So we might just never teach with {} and always promote the alias.

@rjolly
Copy link
Contributor Author

rjolly commented Jan 1, 2021

I might be misunderstanding something, but

given extends C:
  def f ...

is not what I'm considering, which is:

given C:
  def f ...

Edit: I realize that abstract accommodates nicely:

given c: C // abstract
given C // no confusion since abstract cannot be anonymous
given c extends C // named instance

@odersky
Copy link
Contributor

odersky commented Jan 1, 2021

But then you get the conflict between

given C:
  ...

and

given c: C

The : means two different things but the syntax is too close for comfort, IMO.

@odersky
Copy link
Contributor

odersky commented Jan 1, 2021

I still think what we have is more regular: A given instance consists of

  • given
  • an optional signature sig: consisting of some of the following parts:
    • a name
    • type parameters
    • using clauses
  • a type
  • one of the following
    • = and an alias
    • with and a refinement (in that case the type part is generalized to constructor application)
    • nothing at all (for an abstract given)

@rjolly
Copy link
Contributor Author

rjolly commented Jan 1, 2021

Well, : would evolve to mean 2 things instead of 3 (type ascription, extends, start indented body). I think we just have to substitute extends for : in the syntax for structural instance, and eliminate with.

@odersky
Copy link
Contributor

odersky commented Jan 1, 2021

The point of the current rule is that you can always write your given signature, and once that's done, decide on a continuation with a type alias or refinement. Whereas the extends syntax conflates the two things and makes me decide from the start. So, I don't think this will fly.

@odersky odersky closed this as completed Jan 1, 2021
@som-snytt
Copy link
Contributor

Happy New Year. I'm getting an early start on my resolution to catch up with the latest research into given syntax.

I think "closed" for an issue means "open for bike shedding"?

My experience just now with the docs is that "given instance" is introduced first as somehow "most general."

I suggest that "abstract given" is simplest. (I briefly understood the complaint about conflating structural abstraction with context abstraction, then my vision blurred and I no longer felt the difference.) It's especially simple because summon[A]. Like Keats on beauty and truth, that is all ye need to know.

Then I can understand that I can define a given alias, which has familiar syntax.

Defining a given instance is a bit wonky and I'd prefer to learn about that last. (As suggested, I will probably not wonder yet about "empty with" syntax.)

For the example, "implement Ord", introducing with is not unnatural (although I sympathize with everyone's annoyance; I was kind of a fan of as, you could even call me an as-man). It is a quirk on the order of early definition syntax, or maybe it's not as regular as that.

Just pedagogically, it would be nice to read the use case (or using case) for context parameters first, for which abstract givens more or less suffice; after that, I am motivated to learn how to define aliases and instances.

As a footnote, I miss "optional extends", object X extends {}. So obviously I'd cheer its return as given extends {}.

@rjolly
Copy link
Contributor Author

rjolly commented Jan 2, 2021

Thanks Andrew for your insight. Martin, one last argument : it breaks the rule that (keyword, operator) starts an indented block and : starts a template body. Of course, there is the exception of extension which is neither (and IIRC it starts with nothing, neither with nor :).

Edit: Contradicting myself, there's also refinement type:

C & {
  template body
}

@rjolly
Copy link
Contributor Author

rjolly commented Jan 2, 2021

object X extends {}

In reality, the optional part is extends Object, in object X extends Object {}:

object X {}

In given-land, that would not be possible/allowed:

given x extends Object {}
given x extends Object // structural part omitted
given Object {} // anonymous
given Object
given x {} // extends Object omitted -> collides with syntax above
given x

So, what we have is that given behaves exactly like object (or class) but this time with the x extends part optional.

@rjolly
Copy link
Contributor Author

rjolly commented Jan 3, 2021

there is a correspondence between parameters and abstract members. Both are means of abstractions, and you can refactor from one form to the other:

trait WithMembers {
  type A
  val foo: A
}
class WithParameters[A](val foo: A)

This is precisely what the current design does not allow:

given WithMembers with {
  type A = Int
  val foo = 1
}
given WithParameters[Int](1) // impossible, one has to append with {}

@rjolly
Copy link
Contributor Author

rjolly commented Jan 3, 2021

Whereas the extends syntax conflates the two things and makes me decide from the start

I think I see what you mean. We would have to first parse:

given C = ... // given alias

, and then backtrack to:

given C // given instance

, in case there's no equal sign. Is that impossible ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants