Elvis operator for explicit nulls

I apologize I didn’t mean to target anyone in specific here. My point was, I feel the community discussions are often ignored, by the committee and people in charge of making the decision of Scala designs. Community will want one thing and sometimes the discussions are completed dismissed. I hope we can be heard more and our opinions can weigh more so that the discussions wouldn’t be pointless

1 Like

This is simply false. I understand that it can feel that way when a decision is ultimately made that some in the community were opposed to, but it is simply isn’t the case that the feedback is not read or doesn’t register. It is read and considered — by Martin, by myself, by the rest of the committee, and by others who work on Scala at LAMP, Lightbend, the Scala Center, VirtusLab, everywhere. All of us spend a great deal of our time reading and absorbing as much we can of the enormous volume of things being said about Scala online. We can’t directly respond to all of it, or even most of it, but if anyone imagines we aren’t reading it or that it doesn’t register with us — well, that simply isn’t true.

14 Likes

While I have my own complaints about the implementation of the process (e.g. sometimes optional braces), I think in this case, the process is playing out just as it should.

This is the first step: someone posted an idea, and some people gave feedback. We also call this the pre-SIP discussion.

The feedback in the case of Elvis was, I believe, that while Elvis has some merits, it does not have sufficient potential for widespread use to justify inclusion in the standard library.

The conclusion I would draw from this is that the best home for Elvis is some third-party library.

But, if someone truly believes they have a convincing argument that Elvis should be in the standard library, they could move to the next step and write a formal SIP proposal.

3 Likes

What the community wants can be difficult to gauge. It’s easy to get the impression that the community wants is equivalent to what the people you interact with frequently want, without that group being representative of the wider community.

6 Likes

I feel that if a post doesn’t get a laugh emoji from Seth, it is a complete failure.

I realize that sometimes they’re busy cutting releases or running community builds.

2 Likes

To quote an old and related thread:

Perhaps we ought to have an unboxed Option in the stdlib to avoid allocation costs over using null, but I agree that we should NOT encourage the use of null by adding such an operator. However, it might be worth adding such an operator for Option or an unboxed version of Option.


Additionally, I think it’s worth noting that the elvis operator with null is a bit inconsistent as well. When being used with null, it doesn’t distinguish between its RHS being nullable or not—that is, it represents the operations equivalent to both orElse and getOrElse on Option. I think that it is both trivial and valuable to separate those two cases into different symbols (e.g. :? and one of ::? or :??)

4 Likes

The naive elvis at the top says under -Yexplicit-nulls:

-- [E008] Not Found Error: elvis.scala:17:15 -----------------------------------
17 |  println(null ?: null)
   |               ^^^^^^^
   |      value ?: is not a member of Null.
   |      An extension method was tried, but could not be fully constructed:
   |
   |          extension_?:[A](null)
1 error found

Not sure I understood you, but I don’t think I’d want a nulling version I could use accidentally with a shaky finger. It’s like having a gun lock on only one of your footguns.

An unboxed Option sounds valuable, but how can it be done? If None is represented by null, how would we represent Some(null), or is that forbidden? And how can we prevent m(a: A) and m(a: Option[A]) to erase to the same signature?

I am hoping that private and local Options will be optimized away through inlining, although I’m not entirely sure such hope is justified. :grin:

2 Likes

Like this: https://github.com/sjrd/scala-unboxed-option

9 Likes

Ah, thanks, interesting.

So, UOption[A] allows values of type A, including null, and UNone, so USome(null) is distinct from UNone.

If I understand this right, m(aOpt: UOption[A]), erases to m(aOpt: AnyRef). Hm, at least it is distinct from m(a: A).

Looks good! I wonder if it is possible to construct an unboxed Either[A, B] likewise. :smiley:

You can’t likewise construct an unboxed Either, no. You can note that the core methods of UOption are based on type testing for UNone and WrappedNone, and if the value is neither, then we know it’s an A. We never directly type test against A; it’s always implicit. That strategy does not transfer to dealing with more than one generic type, so it’s not possible to do Either[A, B].

Don’t think of Either[A, B] as something similar to A | B. A more equivalent typed would be (A | B, Boolean), with the boolean indicating if it’s left or right. This becomes more evident if you think of the type Either[A, A]. We know there is an A in there, but there is still the information about which side it’s on which we do not know.

3 Likes

I am - like seemingly most - against promoting using null and thus something like the elvis operator.

Is there any reason why we can’t add the unboxed option in the stdlib and deprecate the boxing option? TBD I often don’t care about the allocation, but it seems there is no harm in the more non-boxing version?

2 Likes

I would like options a lot more if there was no potential boxing overhead to them.

The unboxed option is not a drop-in replacement for Option. Because a: A and sa = USome(sa) ultimately are both a at run-time, it becomes impossible to distinguish an A from a UOption[A]. This has several consequences:

  • sa.toString() is always the same as a.toString(), instead of being s"Some($a)"
  • (sa: Any) match { case a: A => } matches, whereas it wouldn’t for an Option
  • (a: Any) == (sa: Any) is true, whereas it would be false for Option (this is mitigated by multiversal equality in Scala 3)

This is why replacing the stdlib’s Option by the unboxed option is not so easy.

7 Likes

You can actually implement unboxed Either, or let’s say half-unboxed Either. It will just only be unboxed in one of its two sides.

Since Either is right-biased nowadays, and since Either is often used for error-handling with the right side being the happy side, it makes sense to make that side the unboxed side.

Simply represent Left(x) as boxed and Right(x) as x unless x is an instance of Left[?] or Right[?], in which case you represent it as a boxed Right(x). This way, you always know if a value is conceptually a Left value (if it’s an instance of Left[?]) or a Right value (if it’s an instance of Right[?] or if it’s not an instance Left[?]).

EDIT: fix minor omission

13 Likes

Ah yes, that works. I had not thought about that!

1 Like

To make sure I’m understanding this, would this be conceptually equivalent to Left[L] | R ?

No, because in your formulation, if you take R = Left[T], you get Left[L] | R =:= Left[L | T] — in other words, you fail to soundly represent Right values of Left-valued payloads.

If we had negation types, we could represent half-unboxed either as:
    Left[L] | Right[R] | (R & not Left[Any] & not Rigth[Any])

Notice that in the formulation above, taking R = Left[T] leads to:
    Left[L] | Right[R] | Nothing =:= Left[L] | Right[R]
which expresses the fact that in this particular case, you need to box the value.

2 Likes

the other way to do it is to have dedicated VM support for it, and store a flag in the object header or something