The reverse might be nice too, e.g. for passing an Option to a native Java method. Though gating this one on into is a bit less obvious, since using it for native Java methods means you can’t actually use into. Maybe it could be automatically applied to non-Scala methods, and require into when you have a Scala method?
given[A] => Conversion[Option[A], A | Null] = _.orNull
If a (new style) given Conversion is used implicitly then either the target type must be wrapped with Conversion.into or the implicitConversions language import must be present.
If this condition is violated, we get a feature warning in Scala 3.9, a regular warning in Scala 3.10, and an error in Scala 3.11.
If this PR is merged as is, we could envision a A -> Option[A] conversion for Scala 3.11.
I mean, you could have asked ^^’
Anyways I did (see below)
Here is my best attempt at somewhat realistic ways this can pop-up in the wild, but it’s most likely possible to do much worse (more innocuous-looking and more realistic):
import scala.language.implicitConversions
given [A]: Conversion[A, Some[A]] = Some(_)
/**
* Resize a list xs to be of size at most `newSize`
* If fillValue is passed an argument, it will be used to fill the missing indices
* guaranteeing the list is of size `newSize`
* Otherwise, the list will not have elements added
*/
def resize[T](xs: List[T], newSize: Int, fillValue: Option[T] = None): List[T] =
if xs.size >= newSize then
xs.take(newSize)
else
fillValue match
case Some(v) => xs ++ List.fill(newSize - xs.size)(v)
case None => xs
/**
* Create a list of size `size` filled with value `value`
*/
def fill[T](size: Int, value: T): List[T] = resize(List(), size, value)
def toTSV[T](ls: List[T]): String = "i\tv\n" + ls.zipWithIndex.map((v, i) => s"$i\t$v").mkString("\n")
val x = <input>
val requiresCorrection = true
val filled = fill(2, x)
val corrected = if requiresCorrection then resize(filled, 4, x) else filled
toTSV(corrected)
(I wasn’t able to get into imported properly, but note that implicit conversions only happen on fillValue anyways)
Running with any x that is not : Option[T] returns what one would expect (example x = 1):
i v
0 1
1 1
2 1
3 1
Running with x as a Some[T] yields some surprises (example x = Some(1)):
i v
0 Some(1)
1 Some(1)
2 1
3 1
And finally, running it with x = None outputs a different number of elements!:
i v
0 None
1 None
This example is of course still not realistic, but I hope your imagination can fill-in the rest
In my eyes, that is still a pretty big weakening of the guarantees
Currently it either works or doesn’t compile at the callsite, with the implicit conversion, it works by default, but downstream code might catch the typing issue.
When program get complex and types get inferred left and right, it’s not impossible that the information gets lost along the way.
This is especially true across library boundaries, since then the people using and defining the method can be quite far apart.
And even if it does break somewhere, you still have to debug where does the wrong type was inserted
Both haha
The first thing that looks confusing to me is this:
Namely: Identical looking call produce slightly different answers
(Changing x to y, or <id> to Some(<id>), can change the semantics)
And then there’s:
The forwarder looks like it just passes its argument without changing them, but it silently does, as a result foo(a) and forwarder(a) don’t yield the same result:
And it’s of course possible to reason through all that, but even with such a “simple” example, it can still lead to confusion:
(Sorry to point to you @daniel, I don’t wish to single you out, but instead imply that there would be many more like you who would get surprised.)
And note that this is a very synthetic example (as you have pointed out).
In reality there could be many lines between the parameter, the call, and the return, making it a lot harder to notice that this is what is going on.
My point is that the conversion is proposed as a tool to fix a specific problem: Using Option for optional parameters is not ergonomic
And we should therefore compare it with other solutions that fix the same problem.
I don’t necessarily think that one specific use-case is not enough to warrant changing things, but given how confusing (even with into) implicit conversions can be, I place the bar pretty high for them.
What I meant was:
There are seemingly very good reasons to do it either way, but we can’t do both (or go back once we’ve chosen one), so we should really make sure we’re doing the right thing
And furthermore, to me this lack of a good option is an indicator that we are maybe going in the wrong direction
I think the worry is that this runtime guarantee does not translate to a compile-time guarantee in some cases (depending on what the type checker does)
Assuming a into use-case, I was however not able to produce a credible reproducer of that, maybe someone smarter will ?
(Maybe using opaque types or more complex bounds)
Totally agree !
(I for example wish that there had been more work into trying to make named tuples work at the stdlib level)
But with Conversions in the stdlib, even one line of code can have a huge blast radius, so we should put a level of scrutiny almost as big as if it was a language change.
(And you probably notice that a lot of my arguments above are true for stdlib Conversions generally)
And this one in particular would be present very broadly, as passing optional values (which don’t have a good default) is a regular occurrence.
So, what I like about Sporarum’s proposal is that it’s unambiguous that the wrapping is always applied. However, it adds a lot of stuff at a language level for a quite minor inconvenience.
What I don’t like about mberndt’s original proposal is just that I don’t like conversions that much, because the gained convenience of not writing a constructor is always lost when a complex expression involving conversions does not typecheck.
However, I realized that is actually not an argument against the proposal of adding the conversion to the standard library. People that want to use into Option can do so, with no changes for anyone else.
I did not read all parts of the thread super carefully, but I have not seen any concrete concerns about the option conversion existing?
And I don’t mean all the problems because some library decides to use it – I suffered through all the spray/akka-http magnet pattern errors, which clearly are all possible even without this proposal, so I don’t think this changes anything in that regard.
My gut feeling says that adding the bounds to A would just make everything more confusing :–). Like, it seems to prevent using the conversion in the cases where it would not apply anyways? But then nothing was gained?
Also, its very unclear what [A <: !(Option[?] | Null)]is intended to mean A is not a subtype of Option and not a subtype of Null? Why not null? Is it not supposed to be nullable as given by the A | Null later?
I guess it seems weird to automatically convert Option[T] or Option[T] | Null to Option[Option[T]]. If you’re already dealing with options, it’s probably better to be explicit. I think it also makes the case @Sporarum showed above a bit less weird.
The conversion is for A | Null, so it lets you talk about the non-null type. E.g., converting Null to Option[Null], or converting to Option[T | Null], especially if using Option[_] is misleading, since the value in the option can’t actually be null.
maybe instead of marking Option as into , which may cause unpredictable behavior, we could introduce an alias for into[Option[T]] and enable the conversion at the call site?
import scala.Conversion.into
given [T] => Conversion[T, Option[T]] = Some(_)
def test(): Unit =
meth(42)
meth()
def meth(e: into[Option[Int]] = None) =
println(e)
type some_good_name[+T] = into[Option[T]]
def meth2(e: some_good_name[Int] = None /** maybe some alias for empty some_good_name also? */) =
println(e)
def test2(): Unit =
meth2(42)
meth2()
I don’t think anybody suggested marking Option as into. The proposal is merely to add given [A] => Conversion[A | Null, Option[A]] = Option(_) to the Option companion object, and into will have to be used in order to actually make use of it.
I don’t think that any of the shown cases converts Option[T]to Option[Option[T]]directly.
I guess you are arguing that this is an example to track negative types more generally. But to me it seems this just adds a ton of complexity to reduce genericity … which seems like a loose loose situation.
Having looked through your example, I’m not convinced that it really demonstrates much of a problem with this conversion. Essentially the issue is that the compiler will happily infer Any as a type parameter for resize, and toTSV doesn’t place any requirements on its type parameter because all it does is call toString on it (arguably that should be def toTSV(ls: List[Any]): String). There’s a myriad of ways this sort of thing can happen without any implicit conversions being involved, much less this particular one.
Well, I’ve made the case before that I think there’s a clear winner here: the one that doesn’t put null land mines into your Some objects which, after all, are supposed to contain a proper value.
I also maintain that either choice would be better than nothing, because at the end of the day, null is just not that common in Scala, and forgoing a feature because we can’t decide how what to do with null doesn’t seem like a good idea.
It seems to me that you have largely overlooked the fact that into will be required soon in order for implicit conversions to kick in, also looking at your code examples. Yes, optional parameters occur frequently. But we’re talking about optional parameters of an unconstrained generic type that are also marked into and where subsequent use of the resulting values doesn’t cause a type error when a mistake is made. Huge blast radius? Honestly I’m not quite seeing it.
I would expect IterableOnce to be present in cases where we serialize to something else, the kind of cases where it’s not at all unlikely we don’t have a strong expected type on the other end
The stdlib marks its arguments (or if it doesn’t, i’m pretty sure it’s at least planning to) into[IterableOnce[A]] because it used to depend on old-style implicit conversions. It’s a real issue if any type can be converted into an iterable.
Generally I don’t think implicit conversions are the best way to do things. I’d more suggest a
extension[T](self: T | Null)
def asOption: Option[T] = Option(self)
For more explicit wrapping into Option. Or you could just use the Option constructor itself, it already makes null None.
Conversion to IterableOnce is actually a complete non-issue because the proposal is to put the Conversion in the Option companion object. When the compiler is looking for a conversion to IterableOnce, it’s not going to look in the Option companion object, so the conversion wouldn’t be found.
The combination of putting the conversion in Option’s companion object plus into seems safe enough to me.
Then just be careful when adding into to standard library methods/functions. I think just making sure there’s an explicit return type might be enough to prevent all of the potentially confusing examples?