dotty-feature-requests icon indicating copy to clipboard operation
dotty-feature-requests copied to clipboard

Remove given instance definitions in favor of given aliases

Open julienrf opened this issue 6 years ago • 16 comments

Currently, it is possible to write a given instance definition without writing an = sign:

given intOrd: Ord[Int] {
  def compare(x: Int, y: Int) =
    if (x < y) -1 else if (x > y) +1 else 0
}

This is not consistent with the rest of the language, which requires all term definitions to have a left-hand side and a right-hand side separated by an = sign, or to use extends:

val foo = 42
def bar(x: Int): Unit = println(x * x)
object Baz extends Something

I think the main reason for allowing users to write given instance definitions without = sign is to remove the repetition with the given instance type, which is mandatory:

given intOrd: Ord[Int] = new Ord[Int] { ... }

However, Dotty can infer the name of an anonymously extended class:

given intOrd: Ord[Int] = new { ... } // Dotty infers “new Ord[Int] { ... }”

I think we could remove the special case of omitting the = sign when writing given instance definitions, in favor of a given instance alias that uses the new { ... } syntax.

Here is a comparison of a complete definition:

// Currently
given intOrd: Ord[Int] {
  def compare(x: Int, y: Int) =
    if (x < y) -1 else if (x > y) +1 else 0
}

// Proposal
given intOrd: Ord[Int] = new {
  def compare(x: Int, y: Int) =
    if (x < y) -1 else if (x > y) +1 else 0
}

julienrf avatar Nov 06 '19 08:11 julienrf

I also find the current syntax very confusing, especially the shorthand syntax:

given Ord[Int] { ... }

which is to stand for:

given someName: Ord[Int] { ... }

Indeed, because of the expectation that this sets (you can ignore the 'someName:' part) I am left to wonder whether the example given in the doc:

given Position = enclosingTree.position

means this (consistent with the shorthand above):

given someName: Position = enclosingTree.position

or that (consistent with the rest of the language):

given Position: InferredType = enclosingTree.position

From what I understand, the new syntax was meant to (superficially) "look and feel" like a proper language feature, making type class instances more "first class", except that it's not actually a new feature — it's a syntactic twist to save a few characters. The problem is that it's so flexible and general that I don't think one can can fully understand it without understanding the underlying encoding anyways, which defeats its purpose.

So we have a strange syntax that's alien to experienced users and mind-boggling to beginners, and confusing to both groups.

I really hope the SIP committee see that this situation is untenable.

LPTK avatar Nov 06 '19 09:11 LPTK

@LPTK What about having a colon after given (without an underscore) ?

given: Position = enclosingTree.position

neko-kai avatar Nov 06 '19 20:11 neko-kai

I would prefer just using an underscore, which is already the conventional way of saying "I don't care about the name of this thing."

given _: Position = enclosingTree.position

LPTK avatar Nov 06 '19 20:11 LPTK

That two character difference is annoying, though, I think inserting colon, but omitting the underscore has a better chance of being accepted since it's closer to the current syntax...

neko-kai avatar Nov 06 '19 21:11 neko-kai

I would prefer just using an underscore, which is already the conventional way of saying "I don't care about the name of this thing."

given _: Position = enclosingTree.position

I could not agree more. It is much less of a burden to a person to add another character than to learn, remember and parse visually another set of syntax ("in this case it looks like this, in this case it looks like that"). I would exchange a small additional verbosity to get a smaller cognitive load due to a reduced instruction set, every time.

Or in other words, perhaps sometimes we aim too much for cleverness and conciseness over simplicity and consistency.

dlangdon avatar Jan 08 '20 13:01 dlangdon

This is not consistent with the rest of the language, which requires all term definitions to have a left-hand side and a right-hand side separated by an = sign, or to use extends:

val foo = 42
def bar(x: Int): Unit = println(x * x)
object Baz extends Something

Well you can write

object Baz { ... }

So why not

given Ord[Int] { ... }

rjolly avatar Apr 08 '20 11:04 rjolly

Well you can write

object Baz { ... }

So why not

given Ord[Int] { ... }

Because in the first case, you're introducing a new name Baz, and in the second case you're referring to existing names Ord and Int. It's really a totally different sort of semantics, which is what makes it confusing as the syntax is so similar.

LPTK avatar Apr 08 '20 12:04 LPTK

Well, one could say you are introducing a new name Ord[Int] as it can later be imported using a by-type import import foo.{given Ord[Int]}

rjolly avatar Apr 08 '20 13:04 rjolly

@rjolly sorry, this does not make any sense to me. That would mean the name you're introducing is semantically significant, which is unheard of in any modern mainstream language. And really, it's not a name. The actual name for these definitions does exists and it is different (in your example it will be something like given_Ord_Int).

LPTK avatar Apr 08 '20 15:04 LPTK

Yes, I agree that in given ordInt as Ord[Int] { ... }, the name is definitely ordInt and not Ord[Int]. But my point is that the current design is self-coherent as it uses given as something that is neither a value nor a type (or that is both in a sense). I used to have a problem with anonymous givens especially when it comes to given imports, see https://contributors.scala-lang.org/t/by-type-imports-are-they-necessary/4121 . Now I can see there is a coherence in these design choices. Of course, it departs from the old implicit scheme where implicits are essentially values, and will take a bit to getting used to.

rjolly avatar Apr 08 '20 16:04 rjolly

Yes, I agree that in given ordInt as Ord[Int] { ... }, the name is definitely ordInt and not Ord[Int].

I'm not talking about that case. I'm saying that in given Ord[Int] { ... }, the name (generated by Dotty) is given_Ord_Int, and as far as I know you can import and use this name explicitly. You can try it with the following code:

trait Ord[T]
given Ord[Int] { }
@main def m = println(given_Ord_Int)

LPTK avatar Apr 08 '20 17:04 LPTK

Yes, I'm not disputing that the old implicit system is used under the hood to make things happen. As I understand, the value of the new system is to raise the level of abstraction. I have to say I am not in total disagreement with your argument that

From what I understand, the new syntax was meant to (superficially) "look and feel" like a proper language feature, making type class instances more "first class", except that it's not actually a new feature — it's a syntactic twist to save a few characters

rjolly avatar Apr 08 '20 19:04 rjolly

To summarize : my feeling is that the current design is especially relevant in the context of coherent type classes.

rjolly avatar Apr 09 '20 13:04 rjolly

I think it's safe to say Scala will never have globally coherent typeclasses as per this discussion.

neko-kai avatar Apr 09 '20 14:04 neko-kai

It might still have locally coherent type classes, and the discussion above is still relevant for these. At least it is how I understand it.

rjolly avatar Apr 09 '20 14:04 rjolly

I've also found multiple confusions between given instances and given aliases — some error messages and behavioral only make sense once you understand the desugaring.

  • A given instance given Foo doesn't work if Foo is an alias to a refinement type, because those are forbidden in extends clauses.
  • At the same time, only given instances work well when they define extra type member, such as in given Foo { type T = Bar ...}; switching to an alias requires given (Foo { type T = Int }) = new Foo { type T = Int }. I just noticed this doesn't even work (I thought it did?!?), because those givens define defs and those are not stable:
scala> class Foo{ type T}
// defined class Foo

scala> given (Foo { type T = Int }) = new Foo { type T = Int }
def given_Foo: Foo{T = Int}

scala> 1: given_Foo.T
1 |1: given_Foo.T
  |   ^^^^^^^^^
  |   (given_Foo : => Foo{T = Int}) is not stable

Blaisorblade avatar Apr 23 '20 15:04 Blaisorblade