How to deliberately ignore the result of a method?

If I have the following (useless) code in Scala 3.3.1:

def f(): Int = 42
def b(): Unit = f()

compiled with -Wvalue-discard I get the correct warning: discarded non-Unit value of type Int on the second line. However, if I try:

def f(): Int = 42
def b(): Unit = f():Unit

this warning is not silenced. I could not find any documentation that this must be so (for Scala 3), but should this not be the expected behaviour? For Scala 2 it is.

I typically would use def b(): Unit = {val _ = f() } for that

2 Likes

It’s a clever solution, however is not an answer to the question (and also too verbose imho). I am curious why issue#7563 did not make it to Scala 3 yet @som-snytt? Projects migrating from Scala 2 to 3 may see a lot of warnings all of a sudden.

An other way of handling this might be something like:

extension (x: Any) 
  inline def toUnit: Unit = ()

def f(): Int = 42
def b(): Unit = f().toUnit

but i don’t know if this generates any unnecessary code.

The Scala 2 idiom e: Unit was intended as a handy and expressive way to reduce noise. (And that may have been before nowarn was invented.)

Since Scala 3 doesn’t plan to support the ad-hoc mechanism, I plan to delete the advice from messaging. In addition, there are other heuristics around warnings that I plan to put behind a flag.

These gestures at accommodation are mostly a response to the intuition that a warning is too noisy, the people will revolt.

I see the linked PR for Scala 2 includes objections that : Unit is just obscurantism, and value discard is a broken language to begin with.

Note that the value discard warning is redundant with Wnonunit-statement.

Perhaps the pattern match idiom val _ = e is deemed “sufficiently verbose” to support a dubious purpose.

Instead of discarding values, improve API to express that methods are side-effecting (by returning Unit or this.type).

2 Likes

For the record, I prefer to do it like this:

def b(): Unit = { f(); () }
1 Like

That idiom avoids value discard (which is a standard conversion in the language to adapt the value) but will warn under -Wnonunit-statement (which everyone uses because it’s never a good idea to drop a value on the floor).

Worth adding that I think that when someone said “value discard is evil”, they meant that value discard is only OK if -Wnonunit-statement is not triggered.

I think the missing adaptation is “this means this”, so that methods returning this.type should not have to spell out this:

def f: this.type = status = OK  // means { status = OK; this }

Maybe I’ll indulge my predilection for rejection and submit a SIP. (It’s not a coincidence that a SIP is a “submission”.)

2 Likes

But implicitly inserting this is still some “strange magic” that needs to be explained. I think this is much less surpising, although more verbose:

def f: this.type =
  status = OK
  this

Or if you want to keep your vertical space waste to a minimum:

def f: this.type = { status = OK; this }

this is not so bad :wink:

1 Like

(note: for those who read this post and wonder what returning this has to do with returning (): Unit, then here is the story: If your api provides methods for updating state your api user might want to chain such updates using dot notation, and that is possible if you get a reference back (instead of returning ()) on which you can continue the chaining:

  turtle.penDown().left(45).forward(100).penUp().right(90).forward(100)

)

I’ll take this (so to speak) as my pre-sip pre-rejection.

I think anyone who knows to write this.type would not be surprised by “this insertion”, especially after years of coping with “unit insertion” aka “value discard”.

I don’t mean to argue for it here, but only to note that some features only “enter the consciousness” at a certain “Odersky level”.

We may now understand “Odersky level” not as a level of skill or gameplay but as a level of consciousness.

I’m not pre-rejecting you SIP, just giving my gut reflection hint. Comprehension and consciousness is suported by a delicate tradeoff between conciseness and explicitness. I guess the actual case of enum OderskyLevel does not change that.

After all, there is some compiler magic if it is typed Unit, as then () is inserted, so your SIP could start with the angle that inserting this if typed this.type is an increase in regularity… I’m not sure everybody agrees but it would be interesting to test that assumption balloon with a pre-SIP.

I would expect a type mismatch error here, expected type Unit but got type Int.

That is the “value discard” conversion mentioned. x becomes { x; () }

Edit: I forgot to say that I have come to accept that just because there is such a feature doesn’t mean that there ought to be such a feature.

2 Likes
val _ = f()
        f() = _ lav
        f(): Unit
                 ^^  too verbose?

I dunno, 8 characters instead of 6 where it’s completely clear that you really did mean a discard instead of an explicit check that the type is correct doesn’t seem too bad a tradeoff to me.

2 Likes

I have two comments here:

1.

I think that if the value being discarded is this, the check should not warn.

The caller already has a reference to this by definition and is safe to ignore.

This would improve many “quasi-fluent” Java APIs, where the last chained call can be just ignored.

2.

The check should be easily and idiomatically supressible both caller and definition site. The clearest ways of doing this I see is:

@CanIgnoreResult
def myMethod(): Result = ...

and for the caller site:

myMethod(): Unit

Both signify engineer’s explicit intent and are hard to do inadvertently.

It already works like that:

scala> def f(sb: StringBuilder): Unit = sb.append("stuff")
def f(sb: StringBuilder): Unit

scala> def f(sb: StringBuilder): Unit = sb.length
                                           ^
       warning: discarded non-Unit value of type Int
def f(sb: StringBuilder): Unit

but compare

scala> def sb = new StringBuilder
def sb: StringBuilder

scala> def f(): Unit = sb.append("stuff")
                                ^
       warning: discarded non-Unit value of type StringBuilder
def f(): Unit

The annotation for item 2 is @nowarn.

Although I originally liked the idea of “expressive style” influencing how warnings are emitted, ultimately I find it clutters the code, and I’m glad Scala 3 doesn’t adopt any such heuristics.

I don’t like warnings per se, as they are a crutch for language-level limitations. Either code is wrong or it’s not wrong. If an API is bad, improve the API.

I couldn’t agree more. Most of us consider warnings as (delayed) errors. It’s message is: First check if the code acts as expected, then fix me. But fixing should not imply “speckle with annotations”. There should be a clear way to code without annotations to avoid a warning.

For the problem i started this thread with i choose to add a generic method toUnit. A little verbose, but makes the intention very clear while still being protected by the warning when i forget to add it. In the example above:

def f(sb: StringBuilder): Unit = sb.length
                                    ^
       warning: discarded non-Unit value of type Int

the error could be forgetting to add .toUnit or the : Unit could be erronous and should be replaced by : Int. The compiler cannot know which mistake i made.

You cannot use @nowarn to suppress discarded values at the definition site. You have to suppress each call site individually.

As a provider of a library I want to say to people that ignoring the results is okay.

@CanIgnoreResult
def myMethod(): Result = ...

This isn’t the way it works now, but an enhancement would be for lints to check the parts of an expression for suppressing annotations. I should be able to annotate either myMethod or Result with nowarn, with specific config.

Scala 2 warnings have axes category, site, origin that are underused. Here, site would be the call site, and origin would be myMethod.