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

Introduce Unit.void[A](x: A): Unit #7530

Closed
wants to merge 3 commits into from
Closed

Conversation

dwijnand
Copy link
Member

@dwijnand dwijnand commented Dec 14, 2018

This is useful when you want to explicitly discard a value, without
having to val it and force your expression to be a block.

Ideally, this would be defined on the Unit type rather than the Unit
companion object, but I couldn't figure out the backend changes
necessary to get it to handle that...

Possible alternative names: discard, drop, forget, unit, ...

TODO:

  • Finalise the name
  • Finalise the location
  • Fix the test so it absolutely tests

Review (and help wanted) by @lrytz or @adriaanm, kindly.

This is useful when you want to explicitly discard a value, without
having to val it and force your expression to be a block.

Ideally this would be defined on the Unit type rather than the Unit
companion object, but I couldn't figure out the backend changes
necessary to get it to handle that...
@scala-jenkins scala-jenkins added this to the 2.13.0-RC1 milestone Dec 14, 2018
@joshlemer joshlemer added the release-notes worth highlighting in next release notes label Dec 14, 2018
@Ichoran
Copy link
Contributor

Ichoran commented Dec 14, 2018

What is wrong with { x; () } instead of Unit.void(x)?

@dwijnand
Copy link
Member Author

I'd prefer having a method, and not need a block.

The best way is having it as a method on Any (like toUnit), which we can do if it's preferred.

@som-snytt
Copy link
Contributor

This proposal will run afoul of disallowing references to Unit companion because folks confuse it with the unit value.

I already made a similar joke, but actually why not co-opt asInstanceOf.

@dwijnand
Copy link
Member Author

This proposal will run afoul of disallowing references to Unit companion because folks confuse it with the unit value.

I agree Unit should be its own companion object, rather than have one. But I'm ignoring that for now and just doing what's easier to implement.

I already made a similar joke, but actually why not co-opt asInstanceOf.

If you mean co-opt in the standard library, then co-opt away. But in user-land I'd like a function somewhere.

@dwijnand
Copy link
Member Author

What is wrong with { x; () } instead of Unit.void(x)?

I forgot another good reason, @Ichoran: to make it explicit that you're discarding a value. Obviously, your example is pretty obvious (and made me forget), but it would be nice to have the option of being explicit.

Also: that doesn't work for the use case that I noticed in a project that has a local version of this:

class StringContextOps private[skunk](sc: StringContext) {
  void(sc)

  def sql(argSeq: Any*): Any =
    macro StringContextOps.StringOpsMacros.sql_impl

  def id(): Identifier =
    macro StringContextOps.StringOpsMacros.identifier_impl

}

@som-snytt
Copy link
Contributor

The explicit version is { x ;() }, a winking laughing face.

The joke on the other ticket was to use sc.asInstanceOf[Unit] to silence unused warnings.

@Ichoran
Copy link
Contributor

Ichoran commented Dec 15, 2018

@dwijnand - What is the advantage of a method here over a block? It's just another way to do things. It's not like { foo(); () } is mysterious, is it? The method alone isn't sufficient documentation for what is going on. I guess your best bet, that way, for the method name is from. Unit from "salmon" makes it pretty clear what you should expect--a ()--even though it's a bit odd that you would need it "from" anything.

(Also, I don't know what void(sc) is supposed to do that { sc; () } doesn't do.)

@dwijnand
Copy link
Member Author

The advantage is it's a function, which has a name. It's not just a jumble of symbols (2 brackets, 2 parens and a semicolon). It's an express decision to transform a value to unit.

The method alone isn't sufficient documentation for what is going on.

I disagree. (Unless you mean "void isn't documented", in that case, fair point)

@dwijnand
Copy link
Member Author

(Also, I don't know what void(sc) is supposed to do that { sc; () } doesn't do.)

Well, I guess you could've done that. But I didn't even think you could, because I'm already in a block (the class constructor). In retrospect it's obvious. But it would also be unnecessary if we just had... a function available in the standard library that allowed me to discard a value, without having to do the secret symbol dance of { sc; () }.

@dwijnand
Copy link
Member Author

Actually { sc; () } doesn't work because sc goes unused:

[error] /d/skunk/modules/core/src/main/scala/syntax/StringContextOps.scala:13:5: a pure expression does nothing in statement position
[error]   { sc; () }
[error]     ^
[error] one error found

so then you try { val _ = sc; () } but that doesn't work either:

[error] /d/skunk/modules/core/src/main/scala/syntax/StringContextOps.scala:13:3: a pure expression does nothing in statement position; multiline expressions may require enclosing parentheses
[error]   { val _ = sc; () }
[error]   ^

So then you just go back to using (and therefore locally defining) void... 🙄

@dwijnand
Copy link
Member Author

val _ = sc works, but I'd rather use the more expressive void(sc)

@Ichoran
Copy link
Contributor

Ichoran commented Dec 15, 2018

@dwijnand - I mean if I see Unit.void(m.replace(k, v)), I have no intuition for what void does. I can learn, of course--I can go read the docs, or I can think for a while about why a Unit.void might exist, and possibly stumble into the idea that it's generating (). But it's unfriendly.

Unit.discard(m.replace(k, v)) is somewhat better, but it's rather puzzling why Unit is the one discarding the expression. Why not just discard(m.replace(k, v))? And anyway, if you discard it but everything is an expression, what's left? null and () are two possible choices, and it's reasonable to surmise that perhaps it's Unit.discard(m.replace(k,v)) instead of Null.discard(m.replace(k, v)) because it's unit?

In contrast, { m.replace(k, v); () } makes it really clear what is happening. The problem of course is that it's less clear why it's happening, but at least you know what.

The only method name that might possibly have this property which isn't a full sentence is from. Then it's really clear, also, that you're generating a (). I'm still not convinced it's better than { ; () }, but at least it tells you what is actually being returned instead of what your goals were when you used it.

Also, with your example, why does either void(sc) or { sc; () } actually do anything? sc isn't a by-name parameter. The whole thing seems pointless, unless it's to avoid some other implementation detail that you also don't want to care about.

@viktorklang
Copy link
Contributor

viktorklang commented Dec 15, 2018 via email

@adriaanm
Copy link
Contributor

adriaanm commented Dec 15, 2018 via email

@dwijnand
Copy link
Member Author

@Ichoran

@dwijnand - I mean if I see Unit.void(m.replace(k, v)), I have no intuition for what void does. I can learn, of course--I can go read the docs, or I can think for a while about why a Unit.void might exist, and possibly stumble into the idea that it's generating (). But it's unfriendly.

I don't think these are strong arguments. It's hard to prove intuition and friendliness I think. I think it's partially based on what you know. Clearly, you don't know Unit.void because it's never existed before as that, and I'm up for finding a better location and name for it, but I don't think it's a difficult concept to teach, and I agree with you that should include why it exists (TODO for me).

Unit.discard(m.replace(k, v)) is somewhat better, but it's rather puzzling why Unit is the one discarding the expression. Why not just discard(m.replace(k, v))?

You're right, and I'm looking for ideas:

  • I don't like @hepin1989's Unit.point idea
  • I don't think Unit.unit is a strong candidate
  • I don't understand @viktorklang's Unit.eval idea - Viktor, what's the reasoning for that name?
  • While Unit.discard feels somewhat better, it looks like we're discarding a unit, which is backwards...
  • discard in Predef would mirror identity but I'm unsure we should add to Predef
  • Given the target of the function is any type, Any#discard() would follow the convention and feels great when you use it, but I don't think we should add to Any either.

And anyway, if you discard it but everything is an expression, what's left? null and () are two possible choices, and it's reasonable to surmise that perhaps it's Unit.discard(m.replace(k,v)) instead of Null.discard(m.replace(k, v)) because it's unit?

Asides from not everything is an expression (there are also imports and definitions, TIL, SLS 6.25), everything is an expression but also we discard non-unit values and insert units in its place (which sometimes means no bytecode, sometimes means boxed unit).

In contrast, { m.replace(k, v); () } makes it really clear what is happening. The problem of course is that it's less clear why it's happening, but at least you know what.

Again, here it's clear to you what's happened because of what you know. To someone else it might not be clear what's happening to that value replace returned. (just for the sake of argument, one could go on and say the dot notation is unclear, the semi-colon is unclear, it's all relative and therefore a difficult argument to stand on IMHO)

The only method name that might possibly have this property which isn't a full sentence is from. Then it's really clear, also, that you're generating a (). I'm still not convinced it's better than { ; () }, but at least it tells you what is actually being returned instead of what your goals were when you used it.

I don't hate Unit.from. The other idea I came up with is on the Function object - it could sit next to its brother const.

Also, with your example, why does either void(sc) or { sc; () } actually do anything? sc isn't a by-name parameter. The whole thing seems pointless, unless it's to avoid some other implementation detail that you also don't want to care about.

According to that compilation unit that sc is completely unused, so it warns (or errors if you fatal warning). So I guess you could argue one could teach Xlint to not warn on constructor params when a class contains macros, but I think I'd rather silence it explicitly. One way is with this as a better expressing (IMHO!) alternative to val _ = sc, or with @deprecated("unused", "")/@unused (#7529) but that's weird because it's not actually unused... 😕

@dwijnand
Copy link
Member Author

@adriaanm

How about a type alias type Unused = Unit @unchecked? And then sc(): Unused

Interesting idea! But it doesn't work 😕

[error] /d/skunk/modules/core/src/main/scala/syntax/StringContextOps.scala:15:3: type mismatch;
[error]  found   : StringContext
[error]  required: StringContextOps.this.Unused
[error]     (which expands to)  Unit @unchecked
[error]   sc: Unused
[error]   ^

@viktorklang
Copy link
Contributor

viktorklang commented Dec 15, 2018 via email

@dwijnand
Copy link
Member Author

Another idea is to call it ”of” so: Unit.of(expr) or have a toUnit on AnyVal.

Don't mind Unit.of(expr) either.

Why toUnit on AnyVal? Assuming you mean AnyVal type and that uses this, that wouldn't make it available for any reference types, which should be able to be discarded.

@viktorklang
Copy link
Contributor

@dwijnand Sorry, meant Any! :)

@adriaanm
Copy link
Contributor

adriaanm commented Dec 15, 2018 via email

@dwijnand
Copy link
Member Author

True.

Now I don't hate the idea, but why do you propose it over a method? For instance, as a comparison, why do Predef.identity and Function.const get exist, but not void?

@He-Pin
Copy link
Contributor

He-Pin commented Dec 16, 2018

True.

Now I don't hate the idea, but why do you propose it over a method? For instance, as a comparison, why do Predef.identity and Function.const get exist, but not void?

Shouldn't deprecate the const and introduce identity?

I think Function.ignore will be good.

@NthPortal
Copy link
Contributor

We could add an apply method to Unit (it would be synthetic, obviously), so that you could call
()(value) 😉

In all seriousness, discard seems like the best name to me, but I'm also not convinced of its value.

@joshlemer
Copy link
Member

Respectfully, I think that with the goals in mind of:

  • trying to limit ways in which there are needlessly multiple ways of doing something
  • simplifying the language and syntax
  • keeping the standard library small

I would prefer to this not be added. I think that when you want to explicitly discard an expression e and return (), the language itself provides a good mechanism for this like @Ichoran says: e; ()

Maybe if I saw some really nasty examples where having to do this hurts readability, I could be convinced though.

@viktorklang
Copy link
Contributor

viktorklang commented Dec 16, 2018 via email

@He-Pin
Copy link
Contributor

He-Pin commented Dec 16, 2018

The last () is not needed.

object Function {
  type Ignored = Unit
  def ignore[T](t: T): Ignored = {
    val _ = t
  }
}

@dwijnand
Copy link
Member Author

True, the compiler can insert that unit for you.

@som-snytt
Copy link
Contributor

som-snytt commented Dec 16, 2018

Note that as of recently you can use the underscore trick multiple times in scope. M5 errors with multiple defs, which was one motivation for a macro that silently consumes a value.

scala> locally[Unit] { val x,y = 42 }
                           ^
       warning: local val x in value res0 is never used
                             ^
       warning: local val y in value res0 is never used

scala> locally[Unit] { val x,y = 42; val _ = x; val _ = y }
scala 2.13.0-M5> class C { private val x,y = 42; private[this] val _ = x; private[this] val _ = y }
                                                                                            ^
                 error: _ is already defined as value _

scala 2.13.0-M5> locally[Unit] { val x,y = 42; val _ = x; val _ = y }
                                                              ^
                 error: _ is already defined as value _

Maybe the pure expression does nothing in statement position warning could be nuanced to noticed the { x ; () } idiom, and obviate underscores. Maybe notice { x ; y ; () }.

@tarsa
Copy link

tarsa commented Dec 16, 2018

IIUC simple type ascription should solve the problem:

        // all constructs below compile fine under Scala 2.12.1
	5: Unit
	"dfs": Unit
	(5 + 5): Unit
	Option(5: Unit)

@tarsa
Copy link

tarsa commented Dec 16, 2018

I saw two examples in this thread.

One is with unused constructor parameter. We can turn it into private val, so it will always be a field in the bytecode.

Another one is about changing return type to Unit. Instead of doing { val _ = m.replace(k, v) } we can do m.replace(k, v): Unit.

WDYT?

@adriaanm
Copy link
Contributor

adriaanm commented Dec 16, 2018 via email

@dwijnand
Copy link
Member Author

I'd be fine with that, provided we can implement that in time...

@OlegYch
Copy link

OlegYch commented Dec 16, 2018

'ignore' is the name i use for this function
it has to be a function since you can't attach docs to syntax tricks and because syntax tricks, well, interfere with syntax

@lrytz
Copy link
Member

lrytz commented Dec 17, 2018

This PR LGTM in its current form. Other alternatives work as well.

@tarsa
Copy link

tarsa commented Dec 17, 2018

What about:

object Unit {
  def cast(ignored: Unit): Unit = ()
}

?
Simpler and immediately obvious the value is ignored.

@dwijnand
Copy link
Member Author

@tarsa That doesn't work because the value would be ignored at the call-site, which is part of the whole point of this. Also cast implies something different, something that this function isn't doing.

I encourage you to try your ideas in the scala -Xlint -Ywarn-value-discard REPL.

@Ichoran
Copy link
Contributor

Ichoran commented Dec 17, 2018

If we're going to do this, ergonomically it would usually be more convenient as

implicit class AnythingCanBeDiscarded[A](private val thing: A) extends AnyVal {
  def discard: Unit = ()
}

@dwijnand
Copy link
Member Author

I'd be happy to provide that, ala import scala.util.chaining._.

Perhaps import scala.util.discard._, or discarding? valueDiscard?

@NthPortal
Copy link
Contributor

NthPortal commented Dec 18, 2018

for reference on what is considered discarding a value:

scala> def foo(x: String): Unit = { x.length; () }
foo: (x: String)Unit

scala> def foo(x: String): Unit = { val _ = x.length }
foo: (x: String)Unit

scala> def foo(x: String): Unit = { x.length }
<console>:11: warning: discarded non-Unit value
       def foo(x: String): Unit = { x.length }
                                      ^
foo: (x: String)Unit

@adriaanm
Copy link
Contributor

why do you propose it [a type ascription] over a method? For instance, as a comparison, why do Predef.identity and Function.const get exist, but not void?

You can't express those with a type ascription, and they actually have a run time component (i.e., you want to evaluate the function). In this case, we're just communicating intent / silencing warnings. I'm not in favor of adding a method for this.

@dwijnand
Copy link
Member Author

I think just like () is a type, but doesn't have a runtime component, A => () is a perfectly valid function, so I don't see why it should be shunned from the standard library. Fair enough it can be elided from the bytecode like () is.

I'd actually be fine with your idea, it's just harder to implement and I don't know if I have the chops to do it in time...

@adriaanm
Copy link
Contributor

adriaanm commented Dec 20, 2018 via email

@dwijnand
Copy link
Member Author

dwijnand commented Dec 21, 2018

Closed in favour of #7560.

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