dotty-feature-requests
dotty-feature-requests copied to clipboard
Remove given instance definitions in favor of given aliases
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
}
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 What about having a colon after given (without an underscore) ?
given: Position = enclosingTree.position
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
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...
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.
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 useextends: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] { ... }
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.
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 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).
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.
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)
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
To summarize : my feeling is that the current design is especially relevant in the context of coherent type classes.
I think it's safe to say Scala will never have globally coherent typeclasses as per this discussion.
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.
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 Foodoesn't work ifFoois 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 requiresgiven (Foo { type T = Int }) = new Foo { type T = Int }. I just noticed this doesn't even work (I thought it did?!?), because those givens definedefs 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