I agree that this has improved the regularity of the original example, and provides an additional consistent simplification by just going directly to =
(consistent because you can already do this with context functions)! Although I also find the arrow to not look strange, and find the parallel to implication pleasing, I want to think through the alternative because I think most people have a lot more familiarity with it.
Your latest proposal is basically to use the form of the mapping that is curried over type parameter lists and regular parameter lists.
So, as usual with curried functions, there’s an exact parallel between these two forms:
given [A] => Ord[A] => Ord[List[A]]
given [A](using Ord[A]): Ord[List[A]]
and we’re already required to learn the second one (sans : Ord[List[A]]
) to use extension methods.
extension [A](a: A)(using Ord[A])
So we already have precedent for that; the curried form is just yet more to learn. The arrow form feels plenty natural to me (no real extra learning), I like it, and yet…I hesitate because it seems like it’s probably an increased burden for some people who are not me.
Are there any cases where the method form doesn’t work? Well, it’s the old with
thing again:
given listOrd[A](using Ord[A]): Ord[List[A]]:
def compare(x: List[A], y: List[A]) = ...
We have replaced with
with :
but the double-duty is exactly the same as before.
But hang on! We can’t do this:
val intListOrd: Ord[Int] ?=> new Ord[List[Int]]:
def compare ...
Here we are placing a (context) function definition in a type declaration slot, and the compiler doesn’t like it. It’s the same problem that we already have with
doing double-duty: you are using type ascription but placing not a type but a definition of the operation in that spot. It’s a little less obvious than with with
, but it’s exactly the same issue.
So, in fact, you haven’t actually removed the issue by moving to functional form. It’s a little more obscure, and maybe obscuring it is enough, but the problem is still a problem.
So what would be regular?
given listOrd = [A] => Ord[A] => List[Ord[A]]:
def compare(x: List[A], y: List[A]) = ...
Now we’re defining a specific function instance with known type, a particular instance is created and everything is fine save that you probably don’t want
def p[A: Ord]() = ListOrd()
given listOrd = [A] => Ord[A] => p()
because the type isn’t explicit enough. Why is List[Ord[A]]:
explicit enough where p()
isn’t? Because if you’re explicitly creating an anonymous class, you have to state the type of the superclass right there. There’s no “type inference” to speak of. What you get is exactly what it says on the tin: this contains one List[Ord[A]]
.
But if that allows us to rescue the functional form from insufficient explicitness, it also allows us to rescue the method form:
given listOrd[A](using Ord[A]) = Ord[List[A]]:
def compare(x: List[A], y: List[A]) = ...
So if we embrace regularity, there really isn’t anything that the method form can’t do. It’s only because we’re allowing irregularity to creep into the function form, probably mostly due to lack of familiarity, that it seems like it handles cases that the method form doesn’t.
One new
issue
In givens, the proposal is to not require the new
that is required elsewhere. Why might you want new
? Well,
scala> class C():
| def apply(f: Int => Int) = f(foo)
| def foo: Int = 0
|
// defined class C
scala> C():
| _ + 5
|
val res1: Int = 5
scala> new C():
| override def foo = 2
|
val res2: C = anon$1@77d58f3a
So the new
is doing work here to disambiguate the two cases.
I don’t have a strong opinion on how to resolve this, but it’s worth pointing out, because the proposal is to change the behavior to not require new
. You certainly should be able to use it if you want, though:
given listOrd = [A] => Ord[A] => new Ord[List[A]]:
def compare(x: List[A], y: List[A]) = ...
given listOrd[A](using Ord[A]) = new Ord[List[A]]:
def compare(x: List[a], y: List[A]) = ...