-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Trial: Use "default" for given instances #7941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This is an attempt to validate the current given design against my very first proposal from more than a year ago: witnesses. I have duplicated the docs, replacing syntax and terminology,
Hmm. Haven't thought about it deeply yet, but my gut reaction is positive -- teaching this as "the default value for this type" seems reasonably natural and intuitive, and I think folks are likely to pick up the idea quickly. I like it... |
Does it mean |
I don't like
An implicit definition with non-implicit parameters doesn't make any sense anymore, so just make them implicit by default. |
Some examples where
I think all of these sound pretty good to me: they provide an english meaning, the meaning is reasonably specific, and the meaning is correct. More so than a keyword like These three use cases generally encompass all the ways I use |
I’m not an English native but something that sounds weird to me is that “default” means (to me) that it co-exists with other non-default values. But, in the case of implicits, this raises an ambiguity. So, we don’t really want “a default ExecutionContext”, or “a default Functor[List]”, but the only ExecutionContext and Functor[List]. |
@julienrf The best part of |
But that isn't really true in Scala. You can have alternate definitions; moreover, you can supply a different one at the call site.
|
I argue that in most of the cases this is an abuse of implicits. I’ve seen several libraries using this: Akka HTTP with |
I strongly disagree with that. There are very good use cases for implicit parameters with several valid values: a famous example is the |
@sjrd I think we have two basic cases for an implicit value: 1) when it expresses some context which we want to carry silently through the code, 2) when it represents some required but generic capability we depend on, a.k.a typeclasses. Both have the property that we can expect a unique value per type in each scope, but otherwise, the value might change between scopes and types. |
I prefer default over witness. As @lihaoyi said, it reads well. It is also easy to understand that if you do not provide an implicit arg, then the value marked as default will be used. |
@sjrd from time to time i have to deal with some apps that use a implicit value to pass toggle settings - i guess this would fall into the same category of "very good usecases"? i suppose that worked well and made for some quite clean code in the beginning. in my experience, the fun starts a few years later, when e.g. tests have sprouted a dozen different implicit instances of toggle settings and you never know which of the possible instances is in scope at any given place. i hope i am wrong, but my gut feeling says Context will be the scala compiler's next cake pattern... |
Well, I've been more than happy with my |
I tried to push the idea further along the lines of #1260, by making implicits more like default parameters. In short, the idea was to label an implicit parameter of type The issue is partial application, precisely what I already brought up in #1260. If you have a function with two parameters, the second one implicit, like this: def f(x: A = a, y: B?) = ... then you need to write def f(x: A? = a, y: B?) = ... then you must not write I tried to work around that fact for about a day where I tried different ways to get around the problem but eventually gave up. I am now convinced there is no principled and practical way that could solve this. You can be principled by saying that default parameters always behave like current defaults, i.e. you must write So, it would have to remain Here's my evaluation of this design:
|
I like the idea of using defaults instead of implicits/given very much and I think this proposal is my favorite so far. I agree with @lihaoyi's comments above.
My user expectation is that if I define any default parameter in the parameter list of a method That being said, could we not address this problem by forbidding mixing default parameters with defaults, as in If forbidding that definition is too excessive, there are two ways we can relax the previous rule:
Even for typeclasses that only have one implementation for a given type, I think it makes sense to talk about that implementation as the "default". Users are always in control and the language still allows them to copy-paste that "default" implementation and declare a new instance of it because the compiler cannot guarantee the coherence of any typeclass instance anyway. |
I agree that this can be confusing, but I disagree that it's a new issue: If you throw a bunch of default and implicit-laden functions at someone in Scala 2.13, ask them to guess the results of eta expansion with @ def foo(implicit i: Int) = i
defined function foo
@ implicit val x: Int = 999
x: Int = 999
@ foo _ // why doesn't this work?
cmd2.sc:1: type mismatch;
found : Int
required: ? => ?
val res2 = foo _
^
Compilation Failed
@ (j: Int) => foo(j) // manual eta expansion works tho
res3: Int => Int = ammonite.$sess.cmd3$$$Lambda$1403/0x0000000800820040@604b1e1d
@ res3(1)
res4: Int = 1 Consider also the status quo mixing implicits with defaults, where adding a @ def foo(implicit i: Int = 0) = i
defined function foo
@ foo
res6: Int = 999
@ foo()
res7: Int = 0 Yes, mixing implicits and defaults and eta expansion is confusing, but that's already the status quo and I don't see this proposal making it any worse. It's an edge case that already exists. Furthermore, while this is somewhat inelegant, I can't remember the last time I bumped into something like this causing me grief. If this edge case is something that has a negligible effect on people's quality of life, it's questionable how much value fixing this specific edge case provides (when traded off against other things that we could improve) We also can tweak the semantics to make it more regular if we wish: this is a re-design after all. What if we specced it such that that There's a pretty wide design space here, and I'm reasonable sure we can find a place that has a balance of elegant and compatibility.
I think this is a non-issue for Scala: this isn't Haskell, and we do not have global coherence. Tons of typeclasses have multiple implementations: Despite having multiple possible implementations, each one only can have one implicit definition in scope. And speaking of "the default serializer for |
I'm already confused. |
@Jasper-M Default parameters are method params that have a default value specified in the method definition: |
It's true that the problem existed already in principle, but
I believe that would cause exactly the sort of gotchas we want to avoid: A subtle change has dramatic consequences. If these parameters behave in fundamentally different ways we need different syntax for them.
This looks promising at first, but it does not work out either. Compare: def f(x: A, y: B, z: C): D; f: (A, B, C) => D
def f(x: A, y: B, z: C = c): D; f: (A, B) => D
def f(x: A, y: B = b, z: C = c): D; f: (A) => D
def f(a: A = a, y: B = b, c: C = c): D; f: () => D The logical end of the sequence is
We have a problem even if no eta expansion is intended or allowed. For all-implicit functions, you must write
I tried really hard to come up with an acceptable design for a while. I thought this would be a promising solution. But I now see there is a fundamental discontinuity here that no design can eliminate. We can paper over it, or make the difference clear by the syntax. Unifying default parameters and implicits has other problems as well: Implicit parameters are themselves implicit, but it would be a stretch to automatically make all default parameters with use-site defaults implicit themselves. And if we do, I fear we promote the kind of tangled implicit parameters that people complain about and that let them propose a restriction to typeclasses as the only safe form of implicits. I do not subscribe to that view and use context parameters a lot in my code. But I also think hard where I use them and make sure the usage is extremely uniform. Default parameters are in my view too tempting and at the same time too undisciplined to promote restraint. We could offer use-site defaults as an addition to what we have. Something like def f(x: T = given) ... The |
So we would end up with two slightly different ways of writing the implicit use-site and two different ways of calling it? def f1(x: T = given)
f1()
def f2(given x: T)
f2 |
I decided to pursue #8017 instead. |
This is another experiment to validate the current given design, now against the idea of
using
default
for given instances, which was originally proposed by @olhotak#5458 (comment).
At the time, arguments against
default
were:given
is much more an adjective thandefault
so that point is moot).Int
andString
which would be a bad idea. That point still stands.Maybe we can align implicits and default parameters more. @lihaoyi originally suggested this
#1260 (comment). That raises a host of "interesting" issues with eta expansion and others, so it won't be straightforward. I believe before we tackle this it would be good to already decide whether
default
for implicit instances is a promising path ahead.I have duplicated the reference docs, replacing syntax and terminology.
To get started with browsing: https://github.com/dotty-staging/dotty/blob/try-default/docs/docs/reference/contextual-defaults/motivation.md