Proposal To Revise Implicit Parameters

This is not even possible under current Dotty for non trivial code bases. Things like traits no longer taking into account ordering (which is a difference in Dotty) will cause certain codebases under current Scala2 to break, I am not sure this is even a worthy cause unless you can guarantee it in 100% of cases (i.e. the source code can cross compile for both Scala2 and Dotty without any behavior in the program).

I mean look at other major libraries now, some already have different sources for different Scala version as it stands right now.

I don’t want Scala to turn into C++/C# where rather than having a few orthogonal constructs that bring consistency to the language we instead keep on rubber bolting on half baked features to tackle one specific subset of some use case and this proposal is heading down this road.

Our goals are the same, but we are clearly in disagreement what that means. I can just say you should give me a bit more credit. You liked the previous version of implicits that I designed. Trust me, the new one will be even better :smile:

3 Likes

Honestly I am failing to see it because I see it being less orthogonal and consistent then the current version of implicits (assuming we fix the quality of life issues which I detailed earlier), I have already detailed my reasons so there is no point in repeating them.

Note that C++/C# went down this road because of the attitude that I am outlining, which is that rather than fixing current issues they keep on adding new ones. They might have thought that the new feature was better than the existing, but its this intention which is what heads us down this road.

2 Likes

Beginners (and in fact even non-beginners) should rarely need to explicitly pass an implicit parameter so I’m not too worried about that.

Fair point, but using a new syntax and keeping support for the old syntax also means a migration period during which some of the libraries you use might be on the old syntax and some on the new, meaning that you have to be very careful at use-site, and that any library update can potentially break your code silently, e.g. if a library defines:

def foo(implicit x: A): A => B

and you call foo(new A), then the meaning of this call will change if the library switches to:

def foo given (x: A): A => B

And your code might still compile!

Regardless, I also mentioned in my proposal that we could use a different keyword, which would look like this:

def minimum[T](xs: List[T])(given ord: Ord[T]) =
  maximum(xs)(given descending)

minimum(xs)
maximum(xs)(given descending)

And keep implicit with its current semantics. But I’m not convinced that this is strictly better, and I’m even less convinced that it’s worth the amount of controversy it’ll generate and is already generating as witnessed by this thread :).

4 Likes

@odersky Apologies for polluting the wrong topic. But yes, in answer to your question, the new system is vastly better. I used to have a lot of problems where f(x: X)(implied y: Y): A => B was tricky or even impossible to use as a lambda because it would tend to expect a Y rather than an A, but with f(x) given (y): A => B it works as expected. Having the option to either explicitly name the given parameter, or just state the type is really nice – particularly when composing implied instances together you often don’t need a name. I think there are perhaps 3 times I’ve needed to use given at the call site to disambiguate which instance to use, and one of those went away when the compiler’s inference was improved. To be honest, because you can do zero overhead opaque types now, it’s less overhead to simply let the types guide things rather than manually wrangling your instances. But where I have had to manually supply instances, the given syntax at the call site has worked out nicely.

opaque type Reversed[T] = T
object Reversed {
  def apply(t: T): Reversed[T] = t
  implied given Ordering[T] for Ordering[Reversed[T]] = the[Ordering[T]].reverse
}

someList.sortBy(Reversed.apply)

The combination of top-level declarations and functional interface sugar and the implied/given/for syntax is just so much nicer, particularly now that the compiler is substantially improved in what it can infer. There are still some rough edges instantiating function types with given parameters in them - it isn’t always obvious how to do it or even if it can be done.

One gotcha I’ve hit several times when reformatting is that because the for keyword is overloaded, if you put the for on a new line, the parser treats it as a comprehension, even if it is ostensibly part of an implied instance declaration.

But how would you teach implicit parameters? You’d have to teach to a beginner that the compiler will infer the parameter for you. Then you’d have to say the way it is done is that the call is expanded to something like this:

   foo(implicit arg)

I could not bring myself to teach that. It would be just not sensical enough to teach with a straight face. If you change implicit to given, it would be better but still a little bit weird.

    foo(given arg)

Syntactically, what is this given? It has no correspondence to anything else in the language, just like (implicit ...) for parameters is a highly irregular case. By contrast,

    foo given arg

is syntactically an infix operator. So we know how to parse this. It fits with the rest of the language. It’s just not what people are used to now, but that should not be the deciding concern.

4 Likes

Well how I teach it (which also includes for Java developers) that just like having a val in a Scala constructor parameter list, i.e. implicit in a parameter list means that the variables can be passed implicitly. I mean you can do this right now in Scala

class SomeClass(val a: String)

You can argue that the val is redundant, but it you are signalling that you can pass in a reference that is a value since you can also pass in a reference that can be variable, i.e.

class SomeClass(var a: String)

EDIT: New versions of Intellij also have inspections which actually show where the implicits are coming from, which is probably what helped the most in teaching implicits. The golden rule I have learnt when people complain or don’t understand the feature is that their complaint is often not what the actual issue their having trouble understanding. In the context of implicits, its usually not the concept of implicits that are hard to understand (or w/e keyword you want to give it) but rather the fact that they have no idea where the values are coming from (and this is what usually comes up as “magic”)

1 Like

Though I can imagine all the controversy in this topic regarding making such backwards-incompatible changes, I personally quite like the proposal. Ever since the implicits are a mechanism, not an intent clicked I don’ t really see implicit parameters the same as other parameters anymore. Implicit parameters are used for injecting some context, they are really not the same kind of arguments your normally pass to a function.

Which is why I don’t really like the implicits as default arguments proposal. They make them more similar, whereas the current proposal makes them visually more distinct. It might be closer to what people are used to, but I don’t think that highlights very well how they should be used.

For me, the whole proposal falls or stands on how easy it is to migrate existing code bases. Both a beginner and an expert can learn this syntax quickly enough, however, if it is hard to upgrade existing code we will likely stay in limbo for too long.

2 Likes

Existing code bases can be migrated by simple, automated rewrite rules. Use-site rewrites are already implemented in the Dotty compiler. Definition site rewrites in a single project would be equally easy.

The tricky bit is migrating libraries in lock-step with their users. I.e. at some point a library like cats might want to switch to new-style implicits. Can the same library then be used both with new and with old code? The answer is, by and large yes, if user code has been rewritten using the aforementioned automatic rewritings (It is for this purpose that the ability to cross-compile is of paramount importance). But every library has to decide for itself when to assume or require that from its users.

I can say with some degree of confidence that regardless of what happens with current implicits, cats will most likely need to have source specifically for Dotty/Scala3, especially considering that cats-core heavily relies on macros as well (and I am not even getting into typeclasses if they get included in Dotty/Scala3). There are other changes like union types, opaque types, typeprojections (which Cats might want to take advantage of).

I would not use complex fundamental libraries as Cats as a benchmark for migration, from what I can see it will be painful for most major libraries to migrate. The important thing is that Dotty/Scala3 is bincompatible with Scala2 (which appears to be case) so at least the community doesn’t have to wait for all libraries in the ecosystem to transition. More importantly is how hard it is for applications, rather than libraries to migrate and fixing the current implicit quality of life issues would be picked up the compiler.

Good to know. I think we can fix that.

I’m afraid I have to agree with Martin on this one that it’s such a dramatic change that it would require a new language and/or introduce as much confusion as it solves.

Consider

implicit val implicitBar = new Bar("default")
def run(i: Int, b: Bar = implicit): Unit = {
  b(i)
  if (i > 0) run(i-1)
}

def doIt(): Unit = {
  implicit val myBar = new Bar("my")
  run(5)
}

When you have a parameter marked (implicit bar: Bar) it’s pretty obvious that it’s available implicitly inside the def as well as being supplied implicitly. The = implicit makes that vastly less obvious; in fact, it suggests precisely the opposite, which is that it’s filled in from whatever is available in context but doesn’t propagate. In the above code, the natural interpretation is that the recursive call is with defaultBar, and the call from doIt uses myBar instead just for the first iteration.

Now, you could make things actually work that way, but this seems weird and awkward to me. Nowhere else does the availability of a variable depend on what you assign to it.

If you wanted to make it regular in the language, you instead would need a type tag.

def run(i: Int, b: Bar with Implicit): Unit = { 
  b(i)
  if (i > 0) run(i-1)
}

Now things are a bit less weird. b is in scope, and it has the Implicit trait, so it is eligible for consideration for whatever makes Implicit special; in this case automatic filling-in. It’s the same style of sugar as letting apply(i: Int) be called as just (i).

I’m not sure this is a good solution either; as Martin points out, having implicit and regular parameter blocks be the same prevents you from emptying parameter lists and/or perpetuates the current ambiguity about what (a)(b) means.

But I think it’s way less irregular than your suggestion, at least considered in the context of current Scala.

2 Likes

Well you could also argue that default parameters satisfy this condition as well, i.e. the availiblity of the variable depends on what you assign to it. Most of these issues in general btw are due to how implicit’s interact with overloads and default parameters, but we can rewrite your example as

def run(i: Int, implicit b: Bar): Unit = {
  b(i)
  if (i > 0) run(i-1)
}

And its completely consistent with what implicit means and how its used, afaik it will also fix the overload/default parameter issue

uHow about something as follows?

If the biggest problem is we do not like assigning b = implicit, that seems solvable.

Implicit parameter lists already behave differently from normal param lists: in their optionality, their interaction with default values, type inference, and so on. Having implicit params behave slightly differently from non-implicit params (e.g. in terms of eta expansion) may not be pretty, but it’s not worse than the status quo.

In fact, implicits would behave almost exactly as they do right now, except we’re removing the requirement for them to be in a separate param list. This just lets people put the parens where they make sense, rather than forcing an arbitrary currying as the status quo does. That doesn’t seem like a whole new language to me!

As for teaching, you would teach them as default arguments whose default has to be provided implicitly, or explicitly. I can’t think of any other concepts or syntax necessary, and it seems straightforward enough now that they would look like normal default arguments rather than weird curried parameter lists

4 Likes

Great minds think alike :grinning:

Placing the implicit before the variable name also probably works better for variables which both are taken implicitly and have a default value.

def run(i: Int, implicit b: Bar = new Bar) = ???

vs

// not sure what you're thinking of for this case
def run(i: Int, b: Bar = implicit = new Bar) = ???

So would implicit at the beginning of a parameter list, still implicit the whole parameter list?

1 Like

You could make a rule as follows,

  • If you put implicit at the start of a curried parameter list (like currently), all of the parameters are implicit, i.e.
    def run(i: Int)(implicit b: Bar)
    
  • If you put implicit as a keyword in an uncurried parameter list, i.e.
    def run(i: Int, implicit b: Bar)
    
    • If you have only one parameter, it doesn’t make a difference
    • If you have multiple parameters but you have default arguments, then the implicit only applies to the parameters which have the keyword. Just like default arguments, they should only apply to implicit’s at the end of the list. This works the exact same way other keywords/annotations that are prepended to parameter lists work.

Note that this may be solving a specific problem where you are not passing an implicit “as an environment” but rather directly in the parameter list. You can avoid this (even currently) by doing what I mentioned here Proposal To Revise Implicit Parameters

EDIT: Alternatively you can do what we currently do with overloading, which is that if you have implicit at the start of a uncurried parameter list with greater than one argument then it works like it does currently otherwise if you start from right to left and apply the keyword before the parameters then only those arguments are inferred implicitly, i.e.

def run(i: Int, a: Awesome, b: Brandy, implicit c: Context)

Obviously this depends if you want to be source compatible with what Scala2 is currently, otherwise you can be very clean, i.e. if you want an implicit to apply to all parameters in a parameter list then it needs to come before, something like.

def run implicit(i: Int, a: Awesome, b: Brandy, c: Context)

(in this, i, a, b and c are all implicit)

Well, no, it’s one of three problems. Your/mdedetrich’s suggestion solves one.

But we have two left. First, parameter blocks resolve left to right, so

(i: Int, implicit ctx: Context[i.type])
(i: Int)(implicit ctx: Context[i.type])

aren’t the same. (The former doesn’t compile.) Breaking this rule for implicit parameters is not good for regularity. This means that we want a way to refer unambiguously to the latter, i.e., the parameter block can completely go away and yet you can summon it back when you need it.

So a solution of some sort is important here.

Second, as Martin points out, (implicit ctx: Context) gets old after a while. It’s clunky and effortful.

(Specifying what is available to infer is also an issue, but it’s somewhat separate.)

1 Like

Excluding the implicit for now since it’s new proposed syntax, in Dotty both of these things work and behave the same.

2 Likes