SLC: `Conversion[A, Option[A]]`

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.

1 Like