scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.88k stars 1.06k forks source link

New implicit parameter & argument syntax #1260

Closed odersky closed 5 years ago

odersky commented 8 years ago

Motivation

The current syntax for implicit parameters has several shortcomings.

  1. There can be only one implicit parameter section and it has to come at the end. Therefore normal and implicit parameter types cannot depend on other implicit parameters except by nesting in an inner object with an apply method.
  2. The syntax (implicit x: T, y: U) is a bit strange in that implicit conceptually scopes over x and y but looks like a modifier for just x.
  3. Passing explicit arguments to implicit parameters is written like normal application. This clashes with elision of apply methods. For instance, if you have

     def f(implicit x: C): A => B

    then f(a) would pass the argument a to the implicit parameter and one has to write f.apply(a) to apply f to a regular argument.

    Proposal

    • Introduce a new symbolic delimiter, ?(. This is one token, no spaces allowed between the ? and the (.
    • Write implicit parameter definitions with ?( instead of (implicit. E.g.
    def f(x: Int)?(ctx: Context) = ...

    instead of

    def f(x: Int)(implicit ctx: Context) = ...
    • Explicit arguments to implicit parameters have to be enclosed in ?(...). E.g.
    f(3)?(ctx)
    • There can be several implicit parameter sections and they can be mixed with normal parameter sections. E.g.
    def f ?(ctx: Context)(tree: ctx.Expr) = ...

    Problems

    • ? can be already used as an infix operator. So the meaning of (a)?(b) would change but only if no space is written after the ?, which should be rare.
    • ? can be part of a symbolic operator. In this case the longest match rule applies. So def #?(x: T) defines an operator #? with a regular parameter list. To define # with an implicit parameter list an additional space is needed: def # ?(x: T).
smarter commented 8 years ago

Explicit arguments to implicit parameters have to be enclosed in ?(...)

I like this because it would prevent sneaky and hard to diagnose bugs like https://github.com/lampepfl/dotty/commit/5225f00ab3300bac62a467ecc532c275e02e7f43

But I worry that newcomers will think that ??? and ? are related, even though one is very unsafe and the other completely safe, I don't have a better suggestion except maybe renaming ??? to !!! :).

retronym commented 8 years ago

This would still lack the expressive power to skip an implicit parameter section that is immediately followed by another implicit param section.

The workaround would be the same for as the way we selectively provide implicit params within a single section, by using implicitly.

def foo ?(a: Int) ?(b: Int) = 0
foo ?(1)?(2) // explicitly providing a and b
foo ?(implicitly)?(2) // explicitly providing b
odersky commented 8 years ago

@retronym Good point. I think the workaround is fine.

Blaisorblade commented 8 years ago

Hi! I like that you address this problem :-)

Should you want to bikeshed syntax, I'd consider using characters that are reserved — my proposal happened to use [[...]] (inspired by [...] for type arguments, which also create optional parameter lists) and should avoid the above problems with ?. http://blaisorbladeprog.blogspot.de/2013/01/flexible-implicits-from-agda-for-scala.html

This would still lack the expressive power to skip an implicit parameter section that is immediately followed by another implicit param section.

The workaround would be the same for as the way we selectively provide implicit params within a single section, by using implicitly.

Slight issue with this workaround: you might need many implicitly to skip many arguments, and adding implicit parameters would break more source code.

What about using allowing to use named parameters to skip implicit sections? That's the solution in Agda, and it easily scales to longer lists of arguments. While having 10 implicit arguments might (should) be less common in Scala than Agda, a more SCALAble language doesn't hurt, and I trust shapeless to run into arbitrary limits to scaling.

def foo ?(a: Int) ?(b: Int)?(c: Int)?(d: Int) = 0
foo?(d = 2) //explicitly providing `d`
odersky commented 8 years ago

@Blaisorblade The named argument trick looks neat. I'd like to check how hard it would be to add it.

ctongfei commented 8 years ago

So it would be "xs sorted ?(order)"? A little bit ugly I think.

I prefer using an additional keyword for introducing implicit parameters such as by or under but probably that'll not be backward-compatible.

Declaration: def sorted under (T: Order[T]) = ??? def map[B](f: A => B) under (sc: SparkContext) = ??? Usage: xs sorted under order xs map f under sparkContext

ctongfei commented 8 years ago

This ? syntax also cannot express the following currently expressed by implicit: { implicit r => ... }

propensive commented 8 years ago

As an alternative, how about reusing the period (.) instead of ?? There would be no risk of it meaning anything else.

implicit val str: String = "x = "
def foo(x: Int).(y: String) = y+x
foo(0) // "x = 0"
foo(1).("Value: ") // "Value: 1"

Also, to which implicit block would context bounds be appended? Could we say, an entirely new block appended immediately after the type parameters, or would that be more confusing than a new block at the end?

Perhaps, given this new lightweight syntax, the type parameter block is the wrong place to be defining context bounds. Much as I like the context-bound syntax, assuming an anonymous parameter syntax, there's not that much difference between,

def foo[T: Typeclass](t: T) = ...

and,

def foo[T](t: T).(_: Typeclass[T]) = ...

versus the original,

def foo[T](t: T)(implicit ev: Typeclass[T]) = ...

and I think the argument could quite easily be made that the additional clarity at the definition site (especially given potential confusion about which implicit block contains the context bounds) would be a net benefit.

Generally, I'm very supportive of the original proposal, though. :+1:

mdedetrich commented 8 years ago

Like the proposal, I disagree with the ? syntax though. ? implies optional, or maybe unknown, it just seems kinda random in the context of implicits.

In fact, I don't really see the problem with having stuff like

def f(x: Int) implicit (ctx: Context) = ...

and

f(3) implicit(ctx) 

or maybe

f(3) implicitly(ctx)

Note that I have just replaced ? with implicit/implicitly and just added whitespace to make it look cleaner.

p.s. I am kinda a fan of the implicit keyword because its easier to spot with your eye and its not confused with anything else. If we really want a symbol, than I think something like % would be better, since I don't think its used anywhere else and its also visually easy to spot. Not really a fan of @propensive idea of using a period (.), since its really hard to spot and easy to visually confuse with method invocation.

# could also work, I mean its already used for type projections but in context of implicit its somewhere else (you could also argue that they are somewhat related)

smarter commented 8 years ago

? implies optional, or maybe unknown, it just seems kinda random in the context of implicits.

Well, implicits are sort-of-optional arguments, the ? is also used in Haskell for implicit parameters: https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#implicit-parameters

mdedetrich commented 8 years ago

Well, implicits are sort-of-optional arguments, the ? is also used in Haskell for implicit parameters: https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#implicit-parameters

Most other mainstream languages tend to use ? to refer to an optional value, or as a safe way to deal with null. I can sought of see the connection in logic, however I would prefer to prefix the ? infront of the type, rather than using it inplace of the keyword, i.e. (? a: A, ? b: B) instead of ?(a: A,b: B).

I guess my biggest grip is more how it visually/syntactically looks

DarkDimius commented 8 years ago

I like the proposal, but I'm not sure about the syntax, as ? is also a potential shortcut for OR type with null. I don't like the look of signature:

def f[A, B]?(a: ?A, b: ?B)
sirinath commented 8 years ago

Add an optional :- separator for implicit. E.g. (implicit:- x: T, y: U). :- means applies to all. If it is optional then omitting it will give the old syntax. Similarly you can have (x, y -: U) if you want.

Another option is to make implicitness part of the typesystem when you add effects. E.g. (x: T @ {implicit}, y: U @{implicit}) or (x: T, y: U -: @{implicit}). Assuming @{} how you specify effects and tag types (as called in Nim Language) / type extensions / annotated type (as called in X10 language).

sirinath commented 8 years ago

Also implicits can simply become annotations than a baked in language feature.

nicolasstucki commented 8 years ago

I assume that this change in syntax should also be reflected on lambdas with implicits. @odersky do you have a proposed syntax for those cases? In particular consider a method that has a parameter that is a lambda which receives one implicit parameter. Would that clash with the Or type withnull mentioned by @DarkDimius?

sirinath commented 8 years ago

Another way might be (x: T = _, y: U = _) where _ means implicitly resolved. This way ? can be reserved for or with Null. E.g. (x: T? = _, y: U? = _) and also { x: X = _ => .... }

stuhood commented 8 years ago

How about allowing use of a keyword in the default syntax?

def meth(arg1: String, arg2: StringContext = implicit): String

On Aug 19, 2016 1:51 AM, "Suminda Dharmasena" notifications@github.com wrote:

Another way might be (x: T = , y: U = ) where means implicitly resolved. This way ? can be reserved for or with Null. E.g. (x: T? = , y: U? = _)

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/lampepfl/dotty/issues/1260#issuecomment-240963978, or mute the thread https://github.com/notifications/unsubscribe-auth/AAC2lFAbyaTJyh4d7ErJfAxz9kkOmEGJks5qhW6KgaJpZM4IhPF4 .

Blaisorblade commented 8 years ago

@stuhood Most proposals are about implicit parameter lists—but all parameters in the same list are of the same "implicitness", unlike in your example. Going in that direction seems be more problematic though I admit I lack ATM a specific example of problems with it.

sirinath commented 8 years ago

I think new syntax should allow to pick and choose what is implicit.

OlivierBlanvillain commented 7 years ago

A bit late to the party, here is my proposal for explicitly providing implicit arguments: remove this option from the language. Instead, implicit arguments could be provided explicitly using the following:

(Assuming implicit I => O is the syntax for an implicit function of arity one)

implicit class ExplicitlySyntax[I, O](f: implicit I => O) {
  def explicitly(i: I): O = {
    implicit val ii: I = a
    f.apply
  }
}

Example of usage:

def f(i: Int)(implicit j: Int)(k: Int)(implicit l: Int): Int = i + j + k + l

f(1)(3) // i = 1, k = 3, j & l implicitly resolved

f(1).explicitly(2)(3).explicitly(4) // i = 1, j = 2, k = 3, l = 4

I think this would play nicely with removing parameter blocks with multiple implicits from the language, which would also fix point 2 of the original motivations. (otherwise explicitly would have to be defined for several arities...)

Blaisorblade commented 7 years ago

@OlivierBlanvillain

I think this would play nicely with removing parameter blocks with multiple implicits from the language,

If you do that, how would you migrate existing code to the proposed language?

The original proposal is also confusing in that regard, but since code migration is an important concern, I assumed the old syntax would still be supported with the current semantics... I don't immediately see how in your proposal. Unless you do all those changes on the new and improved implicit syntax.

sirinath commented 7 years ago

Code migration is an issue but shouldn't you make dotty as an opportunity to improve and and fix problems in a breaking way. Let as long as Scalafix can fix something breaking will not be an issue.

OlivierBlanvillain commented 7 years ago

@Blaisorblade Turning f(implicit a: Int, b: Int) into f(implicit a: Int)(implicit b: Int) should be a straightforward syntaxtic rewrite, and multiple (trailing) implicit parameter lists looks like a simple change in scalac (see https://github.com/scala/scala/pull/5108). Expliciting implicit parameters needs semantic understanding, but shouldn't be too hard either:

f(Nil)(ctx)

becomes

{
  implicit val $c: Context = ctx
  f(Nil)
}

Which hopefully is equivalent and cross compile with both scalac/my proposal.

sirinath commented 7 years ago

I still feel better to represent f(implicit a: Int, b: Int) as f(a: Int = ?, b: Int = ?) or f(a: Int = _, b: Int = _) or f(a: Int = implicit, b: Int = implicit)

odersky commented 7 years ago

Turning f(implicit a: Int, b: Int) into f(implicit a: Int)(implicit b: Int) should be a straightforward syntaxtic rewrite

I thought we'd keep the syntax f(implicit a: Int, b: Int), so no rewrite of function definitions would be necessary.

The migration story for .explicitly is more difficult. We first have to teach the compiler to accept it, because right now it would do the wrong thing. I.e.

 f(a).explicitly(b)

would enforce that an implicit argument is passed to f(a) instead of preventing it. I would hope that we can do this under some "future" mode in scalac. dotty would still accept arguments passed to implicit parameters, but only under -language:Scala2. The rewrite tool would insert .explicitly whereever this is needed.

So, if we can get scalac to accept .explicitly we have a migration path.

stuhood commented 7 years ago

@stuhood Most proposals are about implicit parameter lists—but all parameters in the same list are of the same "implicitness", unlike in your example. Going in that direction seems be more problematic though I admit I lack ATM a specific example of problems with it.

@Blaisorblade : My feeling is that aligning implicit arguments with default arguments as @sirinath suggested (although not directly relevant to the discussion of multiple parameter lists) would be a cohesive change. In particular, the semantics of default arguments (needing to be defined at the end of a parameter list) match. The ability to pass implicit arguments by name would also be cohesive.

Additionally, in a huge number of cases, defining an implicit would not require a second parameter list. You'd only define a second parameter list if it gave the callsite syntax you desired. I suspect that a vast majority of currently multi-arg-list functions would not need to be any longer.

sirinath commented 7 years ago

Dotty should be take as an opportunity to make breaking changes to improve.

propensive commented 7 years ago

@stuhood The thing that I think would be difficult to reconcile (though maybe not impossible) is the calculations of LUBs for unconstrained types mentioned in more than one parameter in the same parameter block, when implicit resolution is involved too. In Scala 2.x, types mentioned in implicit parameter blocks get fixed parameter-by-parameter, left-to-right. It's often very useful to walk this delicate path to get desirable type inference, though that's not to say alternative ways of achieving the same thing wouldn't be possible too...

sirinath commented 7 years ago

I am not sure about the internals or if relates to this specific case. by Guy Steele mentions how it was done differently in this in comparison to Scala: https://www.youtube.com/watch?v=EZD3Scuv02g

Aside from this I am really hoping that Dotty borrows the operator overloading described in the above.

propensive commented 7 years ago

Hi @sirinath - sorry I'm not able to watch the Guy Steele video now, so hopefully I'm not answering the wrong question, here...

It's very much a subtlety of the language, but it's not "internals", as it's a useful feature to users of the language.

To look at this differently, separate parameter blocks offer two distinct things (aside from alternative syntax at the call site):

I would want to make sure that any new implementation offers at least the same power.

smarter commented 7 years ago

the parameters in the same explicit parameter block are typechecked together.

Note that this is already pretty different in dotty, we solve constraints "as late as possible but not too late", which means in particular that we can delay solving constraints until several parameter blocks have been typechecked. See https://www.youtube.com/watch?v=YIQjfCKDR5A

propensive commented 7 years ago

Yes, I remember we talked about this about a year ago... ;) I really need to experiment with it to get a feel for it. I may need to learn to love the new inferencer...

sirinath commented 7 years ago

I am not an expert in the implementation side but please try to watch the video also. It might generate more ideas on how to improve the state of the art.

nafg commented 7 years ago

Passing explicit arguments to implicit parameters is written like normal application. This clashes with elision of apply methods.

First let me say that while this is a problem, I feel like some of the solutions are worse than the problem. The whole idea of implicit parameters is that they are first and foremost ordinary parameters, except that they can be "inferred" so to speak -- similar in some ways to type inference and argument defaults. I feel like making it harder to supply implicit parameters explicitly breaks that elegance.

How about a very simple solution -- allow writing empty parentheses for implicit lists. For instance:

def m(a: A)(implicit b: B, c: C)(d: D)(implicit e: E)

m(new A)()(new D)    // supply b, c, and e from the implicit scope

This would make implicits consistent with default arguments. In fact it's almost like (e: E = implicitly[E]) except that the implicit is resolved at the call site. (Maybe if implicitly was an inlined macro this could be made actually true?)

The syntax (implicit x: T, y: U) is a bit strange in that implicit conceptually scopes over x and y but looks like a modifier for just x.

That is annoying but really doesn't justify breaking code, making valid identifiers magically "sometimes-keyword"s, etc.

stuhood commented 7 years ago

@odersky : Would you mind responding to @sirinath's proposed syntax from https://github.com/lampepfl/dotty/issues/1260#issuecomment-240963978? It really feels like it would be a huge unification, would remove the need for multiple implicit parameter lists entirely, and removes all concerns around eta expansion and additional callsite syntax.

joan38 commented 7 years ago

I'd like also to see arguments from the main people on @sirinath and @stuhood proposal: https://github.com/lampepfl/dotty/issues/1260#issuecomment-262139822 I didn't see anyone showing cons to these proposals.

nicolasstucki commented 7 years ago

A simple cons is that you would not be able to support implicit arguments with default values.

soronpo commented 6 years ago

A simple cons is that you would not be able to support implicit arguments with default values.

Well, it's still possible to allow f(a: Int = implicit myDefaultValue, b: Int = implicit), so a is an implicit argument with th default value myDefaultValue, and b is an implicit argument without a default value.

soronpo commented 6 years ago

I should mention, though, that explicitly annotating every implicit argument with the word implicit is very verbose, which is a con for me. Also a block that has mixed implicit and explicit arguments will be difficult to use. It is better to come up with a way to annotate the whole block. I think @mdedetrich's https://github.com/lampepfl/dotty/issues/1260#issuecomment-223608870 is best syntax proposal yet.

Blaisorblade commented 6 years ago

From Gitter May 11, 2018 4:35 AM:

@Glavo: If I want to have a function with both erased and ordinary implicit parameters, what should I do?

@Blaisorblade: @Glavo wow, seems we'd need lampepfl/dotty#1260 for that

That's because also erased scopes over whole blocks, so you'd need to write

def f(implicit x1: T1)(implicit erased x2: T2) = ...

which doesn't work because that's two implicit argument lists. And I don't see a way to handle this case without solving the general issue. @nicolasstucki thoughts?

nicolasstucki commented 6 years ago

Currently there is no way to support this. The plan is to wait for support of multiple implicit parameters lists in the future.

lihaoyi commented 6 years ago

I think @sirinath's and @stuhood's ideas on unifying default params & implicits are worth considering. As I understand it, the core of the idea is this:

Implicits as Defaults

A function defined as:

def f(a: Int, implicit b: Int, implicit c: Int)

Would be called via:

f(1) // b and c provided implicitly, if in scope
f(1, b = 2) // b provided explicitly, c provided implicitly
f(1, b = 2, c = 3) // b and c provided explicitly

As you can see, this provides a neat, already-familiar syntax for anyone who wants to provide a subset of implicit params while letting the others get inferred: something that is impossible/awkward under status quo Scala. Also note the symmetry with how Scala (and every other programming language under the sun) already treats default params:

def f(a: Int, b: Int = 2, c: Int = 3)
f(1) // b and c provided by defaults
f(1, b = 2) // b provided explicitly, c provided by default
f(1, b = 2, c = 3) // b and c provided explicitly

Effectively, implicit parameters become a special case of default params: rather than the default being hardcoded, the default is implicitly resolved at the call-site, but otherwise the call-site syntax & semantics are identical.

Implicit params with defaults would also compose nicely:

def f(a: Int, implicit b: Int = -1, implicit c: Int = -1)

Where you would be able to call f(1) without an implicit Int in scope, and b and c would be assigned to default values. With an implicit Int, that value would be used for b and c. Or you could pass b and c explicitly. This is in fact exactly the current semantics in status-quo Scala!

Implicits-as-defaults unifies two currently similar-but-awkwardly-different ways of eliding, or manually providing, values for function params: implicits & default params. It does so in a way that makes implicit params more familiar to anyone coming from any other language that supports default params: Python, Javascript, C#, C++, etc..

Implicits-as-params also decouples "implicit params" with "multiple parameter lists" entirely. Nevertheless, you can still have implicit params in a separate parameter list, as is the status quo requirement:

def f(a: Int)(implicit b: Int, implicit c: Int)

But you can have it as part of an existing parameter list (as shown above) and still have it be resolve implicitly. Implicits-as-defaults neatly generalizes the status quo implicit-param-list-must-always-come-last requirement: current code using implicits-always-come-last will continue to work unchanged[1].

Use Case

Consider a subprocess function, that takes in a list of command-line args, a working directory, and spawns a subprocess:

def subprocess(args: Seq[String], cwd: String) = ???

subprocess(Seq("ls"), "/home/ubuntu")

A library author may want to make the cwd have a default value: they may think "most" commands the user will want to run in the same directory they started the process in:

def subprocess(args: Seq[String], cwd: String = new java.io.File("").toAbsoluteFile().toString) = ???

subprocess(Seq("ls"))) // default cwd 
subprocess(Seq("ls"), "/home/ubuntu") // explicitly pass in cwd

However, at a later date, they may decide that it's better if the "default" is configurable, but yet still want the user to be able to override it on a case-by-case basis. In the status quo, and under the top-level proposal above, that will require re-writing all call-sites to:

subprocess(Seq("ls"))) // implicit cwd 
subprocess(Seq("ls"))("/home/ubuntu") // passing in an implicit explicitly, status quo
subprocess(Seq("ls")).explicitly("/home/ubuntu") // passing in an implicit explcitly, top-level proposal

However, when you think about it, in both cases both the library author and user want the same thing: a default value, that the user can override. Does the fact that the default value is configurable really make such a big difference that the user should go and re-write all their callsites mechanically into a different syntax?

Using implicits-as-defaults, this would look like:

def subprocess(args: Seq[String], cwd: String) = ???

subprocess(Seq("ls"), "/home/ubuntu")
subprocess(Seq("ls"), cwd = "/home/ubuntu")
def subprocess(args: Seq[String], cwd: String = new java.io.File("").toAbsoluteFile().toString) = ???

subprocess(Seq("ls"))) // default cwd 
subprocess(Seq("ls"), cwd = "/home/ubuntu") // explicitly pass in cwd
def subprocess(args: Seq[String], implicit cwd: String) = ???

subprocess(Seq("ls"))) // implicit cwd 
subprocess(Seq("ls"), cwd = "/home/ubuntu") // passing in an implicit explicitly

With implicits-as-defaults, the identical syntax in these three code samples properly reflects the identical semantics that a user wants to convey: that they sometimes want the value to be inferred, and sometimes they want to provide it explicitly. Whether the inferred value is hardcoded, computed in the default value getter, picked up from the receiver of the subprocess method or it is resolved from the enclosing scope implicitly, isn't really that important w.r.t. the intent of the code.

(Note that this is a real issue I've hit several times; ammonite-ops code is full of ugly curried function calls like %%('git, "rev-parse", "HEAD")(pwd) that have no real reason to be curried, except to make implicit-passing possible)

Compatibility

Implicits-as-default-params can be specced out to be compatible with both the status quo Scala syntax/semantics and* the @odersky's proposal's call-site semantics, with two minor tweaks:

Conclusion

To sum up, implicits-as-defaults would:

I'm not going to pretend that it won't be a lot of work tweaking the spec/parser/typechecker/inferencer/ASTs/etc. to make this implicits-as-defaults work, but as far as I can tell this proposal gives us the best of both worlds, with zero migration-costs, and some huge additional benefits in learnability of implicit parameters for newcomers to the language. So if we're going put in the effort to do anything, we might as well do this.

[1] You would need a trivial syntactic transform to prepend the implicit keyword to every argument in an implicit parameter list, but that's straightforward. Even a regex would probably do. Note that we cannot preserve the existing "every parameter after the implicit keyword is itself implicit" property without making it impossible to interleave implicit/non-implicit params.

Or we could throw new syntax at it to make the new implicit-per-argument semantic opt-in, making this proposal truly zero-migration, and leaving the existing def foo(implicit x: Int) syntax to remain with it's current meaning of "all params in list are implicit". def foo(x: Int = implicit) or def foo(x: Int = implicit 123) as described above, or def foo(x: Int = implicitly) all seem plausible,

sirinath commented 6 years ago

@lihaoyi Great proposal! Well thought though.

Perhaps something to consider add to the above is when passing implicit parameters positionally in some cases with some rules:

Basically you can add 3 more rules

I would believe most of the uses will confirm to the above rules.

We cannot allow positional arguments if all implicits are not to the end of the parameter list and only part of the implicit parameters are provided in which case we do not know what parameters are inferred.

buzden commented 6 years ago

@lihaoyi, I can't catch how do you suggest to pass explicitly a parameter to a function like

def f(x: Int) = { (implicit ctx: Context) => ??? }

Do you mean that this name for the context variable should be expressed in the type of returned value to make me able to call it like f(5)(ctx = whatever)?

LPTK commented 6 years ago

How hard would it be to have implicit resolution in default arguments be delayed to call sites instead of definition site?

def foo(x: Int = implicitly) = x

The above currently looks for an Int implicit when type-checking foo, but it would be more useful if it delayed the implicit resolution to call sites. This would not be fully backward-compatible, but I doubt many people currently use implicit resolution in default arguments, though I could be wrong.

This way, what @lihaoyi suggests could be supported with only a minor semantic change.


EDIT to add: there is also an easy migration story to make sure old code retain the same meaning, which is to extract all default arguments to definitions (that's is the way they are compiled anyways):

// the above, when considered as legacy code, would be migrated to:
def foo$default$0: Int = implicitly
def foo(x: Int = foo$default$0) = x
acjay commented 6 years ago

@LPTK

I doubt many people currently use implicit resolution in default arguments

FWIW, I've done this before, to achieve "configurable defaults". Pulling this example out of nowhere, but if you had a function that was:

def mapAsync[B](f: A => B)(implicit parallelism: Int = 2)

You could always call it with one parameter list, regardless of whether you provide the implicit or not. Not sure if this is the same situation you were talking about.

Milyardo commented 6 years ago

I'm personally not a fan of default implicits, so a proposal that use an assignment like @sirinath suggested with def foo(n: Int = ?) appeals to me since it prevents the same parameter from being simultaneously implicit and having a default.

LPTK commented 6 years ago

@acjay I think your example would be unaffected by my proposal. To be clear, I don't advocate for getting rid of the status quo way of doing things (with a keyword and an additional parameter list).

You can still encode your use case as:

def mapAsync[B](f: A => B)(parallelism: Int = implicitlyOr(2)) = ...

where we have:

def implicitlyOr[A](default: A)(implicit a: A = default) = a

or if we don't want to use the legacy way of declaring implicits, we can also define the above as:

def implicitlyOr[A](default: A)(a: Defaulted[A] = implicitly) = a match {
  case Default => default
  case ImplicitValue(v) => v
}
trait Defaulted[+A]
case object Default extends Defaulted[Nothing]
case class ImplicitValue[A](value: A) extends Defaulted[A]
object Defaulted extends LowPriorityDefaulted {
  implicit def apply[A](a: A = implicitly) = ImplicitValue(a) }
class LowPriorityDefaulted { implicit def default = Default }
Ichoran commented 6 years ago

Let's allow any parameter block to contain zero or more implicits, and use ;; as a separator (which lexically cannot be there now, and the double symbol makes it really easy to parse visually). Regular parameters go before; implicit parameters go after. To supply those parameters, you must use ;; at the call-site too.

Before:

xs.sorted(3)  // I wanted element at index 3 but I got type mismatch?!
xs.sorted(myOrder)(3)  // Okay

During transition:

xs.sorted(3)  // Warning: bare passing of implicit parameters is deprecated.  Try ( ;; 3).
  // (Then error about scala.math.Ordering)
xs.sorted(myOrder)(3)  // Also warning
xs.sorted( ;; myOrder)(3)  // Works
xs.sorted(implicit MyOrder)(3)  // Fine too, more explicit

After transition:

xs.sorted(3)  // Works!
xs.sorted( ;; myOrder)(3)  // Also works!
xs.sorted(implicit myOrder)(3)  // Works too!
propensive commented 6 years ago

Thanks for the proposal, @lihaoyi! I would love to hear more about how typechecking could work, particularly with respect to dependently-typed implicits. The ability to interleave implicit and explicit parameters would be very nice, but I think it introduces quite a bit more complexity than the proposal suggests (or at least, details which need to be ironed out).