Skip to content
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

Create Option factory method for T | Null #15891

Open
Facsimiler opened this issue Aug 21, 2022 · 11 comments
Open

Create Option factory method for T | Null #15891

Facsimiler opened this issue Aug 21, 2022 · 11 comments

Comments

@Facsimiler
Copy link

Facsimiler commented Aug 21, 2022

When using -Yexplicit-nulls, it's often desirable to create an Option from an obtained reference that might be null:

For example, the following would avoid any mention of the dreaded null value:

Option(someJavaMethodPossiblyReturningNull()) match

    // Function returned null
    case None => // etc.

    // Value was non-null.
    case Some(x) => // etc.

Except that there is no such factory method, and the Scala compiler complains that it received a T | Null value when trying to create the Option value. Clearly, if the nn method is used to get the type right, then an exception is thrown if the reference is null.

This use of Option is idiomatic for handling possibly-null values, so it seems ironic that it isn't available when using explicit nulls.

Would it be possible to add a factory method to the Option companion, such as the following?

object Option:

    def apply[T](value: T | Null): Option[T] = // ...

Or am I missing something, such as extension method like nn that converts the value to an option?

@julienrf
Copy link
Collaborator

I don’t think it’s a good idea since there is no way to ensure that T cannot be Null itself. Consider the following:

type Threshold = Int | Null
val defaultThreshold: Threshold = null
val maybeThreshold: Option[Threshold] = Option(defaultThreshold)
assert(maybeThreshold.contains(defaultThreshold)) // crash

Here, defaultThreshold is a valid value of type Threshold, however, wrapping it in Option makes the runtime forget about it.

I think having such an operation on Option would be a large source of bugs.

@sjrd
Copy link
Member

sjrd commented Aug 23, 2022

I don't think that's a valid counter-argument. You can get this today with type Threshold = String. It doesn't lead to unsoundness. If your T can be Null, you still have to deal with null afterwards, and the type system will make you do it. But if your T is non-nullable, then the new signature helps you by proving you don't have to deal with null afterwards. This is a win.

In order to show an actual issue with this suggestion, you would have to show unsoundness, i.e., a program that doesn't use any asInstanceOf yet throws a ClassCastException, or a program with a value typed as non-nullable but actually containing null.

@bishabosha
Copy link
Member

bishabosha commented Aug 23, 2022

you can have this one

def opt[T](m: T | Null)(using util.NotGiven[Null <:< T]): Option[T] =
  Option(m).asInstanceOf[Option[T]]
scala> val maybeThreshold = opt(defaultThreshold)
val maybeThreshold: Option[Int] = None

Edit: actually this seems a poor encoding because it seems the compiler does not intelligently infer a new union type here if there is more than one part of the union type that is non-null, e.g. it fails for type Threshold = Int | Null | String

@bishabosha
Copy link
Member

would it be too much to expose StripNull or StripType as a compiletime op?

@odersky
Copy link
Contributor

odersky commented Aug 26, 2022

I think the issue here is that since we have Option.apply working for unchecked nulls, and it is supposed to convert nulls to None, it is ironic that it does not work anymore for checked nulls. Correct?

The problem is that we share the standard library with Scala 2.13. On the other hand, this is not a binary affecting change, so maybe we can change this transparenty, by adding a stub or something.

In any case I think this needs to be solved behind the scenes. Adding more operators for this specific case will cost us in the long run.

@Facsimiler
Copy link
Author

Yes, correct. It seems ugly to still have to deal with the null value explicitly, and that there's no out-of-the-box solution for handling such values better. (To be clear, my use case isn't so much to create nullable values, but to deal with possibly null values when working with Java APIs.)

If changing Option.apply isn't the solution, perhaps an implicit conversion from T | Null to Option[T], or an explicit toOption extension method on T | Null, might be possible?

@sjrd
Copy link
Member

sjrd commented Aug 26, 2022

A lunch discussion suggested that an explicit toOption extension method on T | Null is a likely good candidate.

@dwijnand
Copy link
Member

dwijnand commented Sep 2, 2022

There's one in dotty.tools.package. Can we move that to the patched Predef?

@longliveenduro
Copy link

I would love a solution to this. I really love Scala, but when I used Kotlin using .? felt so good and easy. I do understand that you don't want to introduce a new operator, but is at least the toOption() anytime coming soon?

@carlos-verdes
Copy link

carlos-verdes commented Oct 12, 2022

Why not a simply getOrElse extension method?

Something like this:

extension [T](s: T | Null) def getOrElse(t: T): T= if s != null then s else t

For Strings this also could be helpful (I use for exception messages for example):

extension (s: String | Null) def getOrEmpty: String = if s != null then s else ""

@alexandru
Copy link

alexandru commented Oct 24, 2023

I don’t think it’s a good idea since there is no way to ensure that T cannot be Null itself. Consider the following:

type Threshold = Int | Null
val defaultThreshold: Threshold = null
val maybeThreshold: Option[Threshold] = Option(defaultThreshold)
assert(maybeThreshold.contains(defaultThreshold)) // crash

Here, defaultThreshold is a valid value of type Threshold, however, wrapping it in Option makes the runtime forget about it.

I think having such an operation on Option would be a large source of bugs.

I don't see that as an issue because you could still do:

val maybeThreshold: Option[Threshold] = Some(defaultThreshold)

The behavior of Option.apply is factual, the type system just has to reflect it in the types:

def opt[T](value: T | Null): Option[T] =
  value match {
    case null => None
    case v => Some(v.nn)
  }

Option(null)
// val r: Option[Null] = None

opt(null)
// val r: Option[Nothing] = None

Which is more correct? Currently, the inferred type in Option.apply(null) doesn't make much sense, IMO.

I think untagged union types in Scala 3 are in my top 3 favorite additions, and the number 1 use-case is dealing with Null. It's a pity that it's not getting used in the standard library, yet, especially if it can be added in a backwards-compatible way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants