Pre-SIP : Reintroduce view bounds for non-implicit conversions

Currently, view bounds are causing a deprecation error:

class A

def f[T <% A](x: T) = { val a: A = x; ??? }
        ^
        view bounds `<%' are deprecated, use a context bound `:' instead

However, this is what we get and it’s misleading:

def f[T : A](x: T) = ??? // that is:
def f[T](x: T)(using A[T]) = ???

What we want instead is:

import scala.language.implicitConversions
def f[T](x: T)(using Conversion[T, A]) = { val a: A = x; ??? }

Or, alternatively:

def f[T](x: T)(using T => A) = { val a = summon[T => A](x); ??? }

, which brings back the boilerplate the view bound was supposed to remove (maybe the reason it was deprecated?) On the other hand, this syntax is going to be more and more used as we want to avoid implicit conversion. Indeed, the recommended way to express this:

def f(a: A) = ???
given Conversion[String, A] = ???
import scala.language.implicitConversions
f("hello")

, is to apply the conversion explicitly and push it in the library code with a using clause:

def f[T](x: T)(using c: T => A) = { val a = c(x); ??? }

We may call this pattern anticipated implicit conversion or anticipated conversion for short. My proposal is to introduce an extension method as to simplify the conversion application:

extension [T](x: T)
  def as[A](using c: T => A) = c(x)

def f[T](x: T)(using T => A) = { val a = x.as[A]; ??? }

Or, in view bound syntax, which gets back all its relevance:

def f[T <% A](x: T) = { val a = x.as[A]; ??? }

I don’t understand how your final result

def f[T <% A](x: T) = { val a = x.as[A]; ??? }

is supposed to be better than the mentioned status quo

def f[T](x: T)(using c: T => A) = { val a = c(x); ??? }
5 Likes

Well, it’s one parameter section less, and it doesn’t introduce a fresh variable. And it looks like a normal bound T <: A, which makes its meaning clearer - that is, we can expect to somehow being able to “upcast” x to A.

By that argument, context bounds too are useless.

Some historical context :

https://groups.google.com/d/topic/scala-internals/hNow9MvAi6Q
https://groups.google.com/d/topic/scala-sips/wP6dL8nIAQs

Quoting Martin’s original argument:

It seems perverse that we would have syntactic sugar for implicit conversions that we propose to put under a feature flag because they are so easily misused. So I propose to deprecate view bounds.

And indeed they were misused, mostly to model what is now covered by extension methods:

A method with a view bound:

def foo[T <% Ordered[T]](x: T, y: T) = x < y

To get rid of the view bound, define a type alias

type OrderedView[T] = T => Ordered[T]

and rewrite foo to:

def foo[T: OrderedView](x: T, y: T) = x < y

Nowadays we have the Ordering typeclass with its extension methods. However, there remains @lihaoyi’s “implicit constructor” use case which is not subsumed by type classes (or context bounds). Together with Martin’s recommended way of using conversions (that is, not implicit), I think we still have a relevant use for the syntax. We just have to make sure conversion is not applied implicitly without the feature flag (or at all). I will need assistance for this, here is my pull request Reintroduce view bounds for non-implicit conversions by rjolly · Pull Request #11559 · lampepfl/dotty · GitHub. Any help appreciated.

I’m not sure I understand the use-case properly, but to be frank I’d rather see context bounds disappear as well. The convenience of them is diminished by the clarity and versatility of using.

2 Likes

It turns out conversions are not applied implicitly - probably due to the syntax being desugared not to using Conversion[T, A] but T => A which is perfectly fine. I’ve added a neg test to be sure. The PR is ready for review.

Edit : maybe we should resume formal SIP for such change. Is this page still relevant?

Edit2 : I had to use convertTo instead of as, which collides with several constructs (in compiler tests, cats stm, scalaz).

I have to observe that this isn’t anything new. I mean, the community has been saying “don’t use view bounds, because they are going away” since (Googles) at least 2013

1 Like

I’d love to see context bounds removed too. The beginner confusion is not worth being slightly less verbose. However I also wish Scala had the ‘break’ keyword, so maybe don’t listen to me.

2 Likes

I admit the improvement is not huge, but think that it might be all we are left with once implicit conversions are phased out.

Definitely not the only thing left, since, as mentioned above, we can do

def f[T](x: T)(using c: T => A) = { val a = c(x); ??? }

and we can also do

def f[T](x: T)(using c: Conversion[T, A]) = { val a = c(x); ??? }

Both would work even if implicit conversions were completely phased out and Conversion was not special anymore.

Again, reintroducing <% only brings more incentive to use more implicit conversions, without adding any expressiveness. IMO this is not going to fly.

5 Likes

Using more implicit conversions would mean stay with this scheme:

def f(a: A) = ???
f("hello")

On the contrary, my proposal is to nuge people towards the recommended construct.

My problem with your syntax is mainly the fresh variable c. Either we use it systematically by convention, and c is forever unavailable for other uses in the method body. Or we are left with bothering with an original name at all times.

So you’re trading the need for a fresh local variable name for an additional language construct? That’s way below the bar for having a construct in the language, in my book.

2 Likes

Why, it would enforce a conventional name, like convertTo (unfortunately, as seems already in use) and alleviate the burden of coming up with one each time. We already do this with summon, to allow anonymous givens. In a sense, I just propose to add a shorthand for summon[T => A].

I think one problem is that this syntactic question is intertwined with the relevance of implicit conversions as such (in the “implicit contructor” sense), so let me elaborate on why I think these are still important. As you may have noticed, I am trying to use Scala to do math. In math, objects tend to be in a subset relationship, like Integers is a subset of Rationals and so on. What we are looking for is a substitution principle, whereby each time a rational is needed (which you can specify with a bound, T <: Rational), you can substitute an integer. OOP is calamitous in that respect, as it works the other way around : what you typically have is that Rational extends Integer, which means you can substitute a rational for an integer, which does not make mathematical sense.

And you can’t use Integer extends Rational, because that would mean to know every possible superset in advance, which is clearly impossible.

Enter implicit conversions (or views):

given int2rational: (Integer => Rational) = ???

, and you have your substitution principle. This time your requirement is expressed with a view bound T <% Rational rather than a normal, subtype bound. But otherwise it works about the same.

So, that’s why I think the syntax is important, irrespective to conversions being applied implicitly or not (and the prospect is going to be “not” as far as I can tell). It would make the pattern a first-class citizen, on a par with type classes and their special context bound syntax (:) which everybody agrees is important and an improvement w.r.t. the using syntax in terms of level of abstraction.

At the risk of adding an extra abstraction, you can make this relationship explicit (and still fairly low boilerplate)

object definitions {  
  trait Subset[A, B] {
    def witness(a: A): B
  }

  object Subset {
    // This is supposed to work (see `sumBy` example here: https://dotty.epfl.ch/docs/reference/contextual/extension-methods.html#generic-extensions)
    //extension [A](a: A) {
    //  def is[B](using S: Subset[A, B]): B = S.witness(a)
    //}
    implicit class Ops[A](a: A) extends AnyVal {
      def is[B](using S: Subset[A, B]): B = S.witness(a)
    }
  }
}

object example {
  import definitions.Subset, Subset.Ops
  
  case class Rational(numerator: Int, denominator: Int)

  given Subset[Int, Rational] with {
    def witness(i: Int): Rational = Rational(i, 1)
  }
  
  def foo[A](a: A)(using A Subset Rational): Rational = 
    a.is[Rational]
  
  // For the ones you use a lot
  type IsRational[A] = Subset[A, Rational]
  def bar[A: IsRational](a: A): Rational = 
    a.is[Rational]
  
  @main def test() = {
    println(foo(1))
    println(bar(2))
  }
}

Scastie

What I think this alternative hints at is that this is a more general case of running into the limitation that context bounds can only have a single type parameter. If context bounds could be written using an anonymous type parameter, it’d be much more usable:

def foo[A: Subset[_, Rational]](a: A): Rational = a.is[Rational]

Leveraging infix types as in the example above, it’d be even nicer:

def foo[A: _ Subset Rational](a: A): Rational = a.is[Rational]

I thinks it’s more or less isomorphic to what I propose, minus the <% notation:

extension [T](x: T)
  def convertTo[A](using c: T => A) = c(x)

def foo[A](a: A)(using A => Rational): Rational = 
    a.convertTo[Rational]

type IsRational[A] = A => Rational
def bar[A: IsRational](a: A): Rational = 
  a.convertTo[Rational]
1 Like

Scala has been accused of having an excessive number of weird symbolic operators. I don’t think that’s true (at least not for the language proper), but nevertheless we should strive to remove them where we can. So <% is gone, and that’s a good thing!

7 Likes

@rjolly : Yep, the encoding I used wasn’t anything particularly unique, my only insight was that if context bounds could handle an anonymous placeholder, the difference between <% and the alternatives shrinks considerably.

@odersky : is an anonymous placeholder in context bounds a feasible thing to explore at some later date?

Ok, I get it. Indeed these two are equivalent:

def foo[T : _ => A](x: T) = { val a = x.as[T] ; ??? }
def foo[T <% A](x: T) = { val a = x.as[T] ; ??? }

Edit : problem solved:

type <%%[X] = [T] =>> T => X
def foo[T: <%%[A]](x: T) = { val a = x.as[A] ; ??? }

Using a type lambda is already possible:

def foo[A: [x] =>> Conversion[x, Int]](a: A): Int = a

And I thought the plan was for Conversion[_, A] to mean [x] =>> Conversion[x, A] somewhere in the future. That’s why wildcard types are now written with ? instead of _.

1 Like

Clever! That’ll definitely work for now.

I might suggest this alternative:

type %%>[O] = [I] =>> I => O

That way the arrows follow the transformation (T points to A, suggesting that T becomes A):

def foo[T: %%>[A]](x: T) = { val a = x.as[A] ; ??? }

Yes, I’m leaning towards something more explicit, like:

type Conversion[A] = [X] =>> X => A
def foo[T: Conversion[A]](x: T) = { val a = x.as[A] ; ??? }

Anyway, there’s no need to embed this in the language anymore. Everyone can choose what they see fit.

2 Likes