question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

New implicit parameter & argument syntax

See original GitHub issue

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).

Issue Analytics

  • State:closed
  • Created 7 years ago
  • Reactions:28
  • Comments:71 (41 by maintainers)

github_iconTop GitHub Comments

157reactions
lihaoyicommented, May 22, 2018

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:

  • Parameter lists for which every parameter is implicit can be elided. i.e.

    def f(a: Int)(implicit b: Int, implicit c: Int)
    f(1) // b and c resolved implicitly
    

    This will allow backwards-compatibility[1] with the existing Scala 2.x implicit-parameter-list syntax & semantics, rather than forcing people to call it via f(1)() as would a naive reading of the above description.

  • Implicit parameters cannot be passed positionally:

    def f(a: Int, implicit b: Int, implicit c: Int)
    f(1, 2, 3) // invalid
    f(1, b = 2, c = 3) // OK: implicits passed explicitly
    f(1) // OK: implicits resolved implicitly
    

    This will allow for interspersed implicit-and-non-implicit params:

    def f(a: Int, implicit b: Int, c: Int)
    f(1, 2) //a = 1, b resolved implicitly, c = 2
    f(1, b = 3, 2) //a = 1, b explicitly passed as 3, c = 2
    

    Note that keyword-only/non-positional params are themselves not a new idea, being common in Python as well as languages like Ruby or Javascript where passing arguments as dictionaries is commonplace. This should be the only tweak necessary in order to make possible all of the semantics that @odersky describes above.

Conclusion

To sum up, implicits-as-defaults would:

  • Allow us to get all the semantics of @odersky’s original proposal above

  • Provide 100% semantic backwards compatibility with the status-quo Scala semantics and syntactic backwards compatibility with the status-quo Scala syntax, needing only a trivial[1] syntactic migration.

  • It decouples implicit parameters with currying, separate parameter lists, and other confusing-to-newcomers language features that they have no business being coupled to and tend to make learning about implicits more confusing than is truly necessary

  • While at the same time associating them with default parameters, a language feature that they are very similar to (both in semantics & in use case) and that we can expect newcomers to be already familiar with

  • Be instantly familiar to the people already using Scala! They don’t know what ?() or .explicitly are, but they know you can mark parameters as implicit and you can pass default arguments explicitly. No re-training required

  • Provides a neat, already-understood syntax/semantic for providing a subset of implicit params explicitly.

  • It would make implicit parameters easier to learn, not by adding special syntax & features, but by removing unnecessary coupling & arbitrary restrictions that hide the fact that implicits really aren’t that different from default/named parameters, a feature everyone already knows & loves.

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,

27reactions
mdedetrichcommented, Jun 3, 2016

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)

Read more comments on GitHub >

github_iconTop Results From Across the Web

Contextual Parameters, aka Implicit Parameters | Tour of Scala
A method can have contextual parameters, also called implicit parameters, or more concisely implicits. Parameter lists starting with the keyword using (or ...
Read more >
Implicit arguments — Coq 8.16.1 documentation
Implicit arguments can be declared when a function is declared or afterwards, using the Arguments command. Implicit Argument Binders¶. implicit_binders ...
Read more >
Implicit Parameters in Java - ThoughtCo
The implicit parameter in Java is the object that the method belongs to. It's passed by specifying the reference or variable of the...
Read more >
Implicit Implications (part 1): Implicit Parameters - Rally Health
There is another syntax that desugars into an implicit parameter, called context bounds: // The [A: Ordering] is called context bounds, and it...
Read more >
Implicit Arguments - Lean Manual
Named arguments enable you to specify an argument for a parameter by matching the argument with its name rather than with its position...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found