PRE-SIP: limiting value discards:

  • Sorry, I can’t get what you refer to.

OK. Thanks for some rough explanations. I still don’t have any idea why would anyone want to unpack all Throwables kept in scala.util.Failure when using Fibers. It seems very arbitrary. You can represent failures using other constructs, e.g. Either (i.e. Left) or Option (i.e. None) - would you want to unpack them and throw exceptions?

I guess only Try have general direct meaning of failure and can be converted.
For Either and Option better not to provide any conversion, so discarding such value will be compile error.

For instances of MonadError and other effects (Future systems, where conversion can be await, or runUnsafe … etc …) conversion can be provided on library level by library authors.

Because I want to insert Futures (maybe special FiberFuture, but let think Future) into direct control flow.
i.e.

localOperation1()
doRemoteCall(a)
localOperation2()

instead

localOperation1()
doRemoteCall().flatMap(_ => localOperation2())

(Sorry, it was not answer, but explanation, how Fiber relates)

With Try situation is much simpler:
Exists common novice error: Discard value of Try without error handling. Disabling automatic discarding (and provide a conversion to effect when this conversion is clean and well-defined) prevent this novice error. That’s all.

What if the _ <- and () weren’t necessary?

I’m strongly in favour of the motivation behind this SIP (I authored the initial version of that “Allow suppressing value-discard warning via type ascription to Unit” PR) but I wonder: how much of this proposal would be necessary if instead for notation supported:

for {
  doSomething("A")
  doSomething("B")
} yield "C"

// and

for {
  spawnA
  spawnB
}

Can better monadic comprehension use instead? We have two sides here:

  1. Disallow discarding values until the effect of discarding (or absence of one) nod defined. (Ie. make it harder to shoot themself).
    This part is orthogonal to better monadic comprehension. (I.e. PRE-SIP is steel needed)
  2. Allow a style of programming when monad composition used instead of control flow. Better monads can help here. From the other side - this is part of the long-long discussion when people testes are different.
    I think that wrapping all in yet one layer is add a small cognitive load for the developer when writing each operation (as brackets). And if when we can work without an extra layer, better not use one.
    Here ( http://blog.paralleluniverse.co/2015/08/07/scoped-continuations/ ) is a detailed description of the CPS/Monad dilemma. In the functional land, effect algebras can achieve the same: we can reinterpret control flow syntax in monads and vice-versa. So, better monads ( ‘ideal monadic syntax’) will be the same as control flow. Which brings to the table yet one question - why we need to have two languages (monadic and control-flow)? With two different camps behind, of course.

Since we (SIP people) were specifically called out about this topic, I’ll give my opinion.

TBH I don’t think the proposal is a good fit for Scala. Currently Scala has statements and expressions, and they are what they are. It also has for comprehensions that specially call out as being rewritten into maps and flatMaps. Having a feature where normal statements are rewritten into method chains introduces another, more hidden way of doing things that can already be done, safely, with for comprehensions.

The concerns about some values being discarded unsafely can already be addressed with -Ywarn-value-discard, as was mentioned earlier.

5 Likes

– problem, that it is possible to lost effect, writing discarded value without for comprehension. I.e. the main point is a disallow errors,

How you think about just forbidding discarding values, and propose some syntax for explicit discard, instead? (i.e. make something like -Yerror-value-discard enabled by default)

That is going to cause difficulties for a large portion of the programmers who are not into the pure FP mindset, so I am unlikely to look favorably on such a proposal.

However, it stands a chance, IMO. I am not the only person on the SIP committee, and I can see an argument being made for that variant.

2 Likes

I’ll chime in, the least of the SIP committee members.

I like the idea of what you’re doing, but the syntax / expression still doesn’t seem “scala” to me. What you want is a way to lift an expression into a context so that you’re operating inside that context, not the current one.

Theoretically, this is a syntax currently best suited for macros (indeed, scala async was one such macro). I think you’ll want to do a few things to improve the proposal:

  1. Very rigorous rewrite rules including what contexts/monads you can use. The presence of “if” is curious here. If we allow pattern matching, what happens then? I’d like some more detail on that, because I can’t line up in my head how this would work.
  2. A clear syntactic-designation that magic is happening (or something different is happening). Right now it’s a bit too invisible to the writer for my taste.
2 Likes

This proposal does not include any rewrite rules behind the conversion of discarded values to unit.
The second code example was about the possibilities of combining that conversion with JVM-level continuations, which will become possible.

It was my tactic error to mention continuations here because all started to discuss this example instead of the very concrete and small origin proposal. In one sentence, it was: ‘Allow to define a custom operation applied before discarding value or disallow discarding if such conversion not exists’ .

Sorry again for confusion, this would work without any rewriting and interpretation of if and pattern matching will be unchanged. An example was pretended with the phrase ‘when continuations would be available’ [i.e., on JDK14], so no [this proposal] magic here: Loom does all magic.
The proposal was needed to handle only one problematic case: when Monad with value discarded and we can’t insert effect handling there. Ability to define custom operation prevents one of the most commons errors when coding in typescript: forgetting to write async before the call of a function, which returns Promise<Unit>.
I will write a detailed informal post about scala concurrency later in another thread; it’s interesting itself and already implemented without language change, except the case described above.

Returning to ‘this PRE-SIP’: note that problem is not unique for scala, most other languages provide type or function annotations:

Some languages require explicit discard Nim Manual

Anyway, it looks like an idea to allow customizing value discarding will be not supported here. Technically, to exclude problematic case, disallowing implicit value discarding will work too.

So, the next goal - is to disallow discarding values without typed ascription [as implemented by @dwijnan in Scala2 ]

Hypothesis: actually, discarding value is rarely used, and adding type ascription will not be difficult for programmers. I will recheck this by analyzing some corpus of code, somewhat later.

Ah, sorry for my misunderstanding. If the only part of the proposal that’s relevant is removal of value discarding, or forcing unit conversion (or some other syntax), then I think you should focus the proposal on that.

The motivating example on concurrency is good, but I’m sure you’d find more examples that could motivate this change. In fact, IIRC we’ve discussed the possibility of this in the past.

I think the thing to focus on in your proposal:

  1. Focus the motivation to specifically the thing you want to fix. The example you show has LOTS of things to clean up, but your proposal is for a small piece. Clean that up so that others don’t make the same mistake I did.
  2. (minor) Get to the proposed change quickly in the discussion. Possibly put this before the motivation so it’s clear when reading the motivation how the proposed change helps.
  3. You’ll want a good section on migration path for existing Scala users. The analysis you suggest on existing usage of value discarding would be a great start. The big open question on changing something so fundamental in Scala 2 is “how can we do this without breaking lots of working code”. The goal is to not have unwritten code break, and that’s a good goal. Remember to balance this with the migration costs. It could be your proposal would need a phased approach to account for that.
2 Likes

Thanks. Will try to move this forward :wink:

1 Like

An interesting avenue of exploration would be to do the following:

  • Put an implicit conversion from Any to Unit in Predef, and drop the automatic Unit discarding in the typechecker. Does everything still compile? If yes, we have saved a special rule in the type checker, which is always a good thing! If not, we have to figure out how manageable the breakage is.

If this first step is successful, we can then fiddle with where to put this conversion

  • In Predef, so that it can be unimported. That’s the most conservative option. Or:
  • In the companion object of a trait DiscardedUnit. Inheriting that trait in a type would make the conversion available.

One other note: If we do proper effect checking (which is planned but will still take a number of years to materialize), the need to disable unit discarding diminishes. In such a system writing

def f(): Try[T] = ...

f()
result

would give a “pure expression in statement position” warning on f().

1 Like

I think that would also allow disabling conversion for specific types by introducing an ambiguous conversion. That seems more useful than enabling it for specific types.

3 Likes

Or discarding v means invoking v.discard(implicit d: Discardable[V]).

Enable for all by import Discardable.Any.

@implicitNotFound("${A} is too precious to toss away like pearls before swine")
trait Discardable[A]

@deprecated("Did you intend to fire and forget?")
def forgotten: Discardable[Future[_]] = null

My concern with this approach is this would move from a situation where you can easily enable discard safety on a project level with a simple flag, to one where you’d need to put together an object to contain the exclusion and reference that in the new -Yimports: flag (if I’m understanding #6764 correctly).

It’s not that much more work, but it is considerably more arcane. This could be avoided by providing such an object in the standard library, so the flag to enable discard safety would change to something which could be easily shared:

-Yimports:scala.Predef,scala.predef.disable.ValueDiscard

If the implementer is feeling particularly generous, objects providing common unimports could be included (like disabling any2stringadd).

1 Like

@morgan-peschke Dotty uses another mechanism to suppress imports. To take suppressing any2stringadd as an example. You can write in the source code:

  import Predef.{any2stringadd -> _, _}

More generally, any explicit import of an otherwise automatically imported object or package replaces the automatic import.

It works for modules but not packages, in scala 2 and 3, it seems.

-Yimports in scala 2 keeps that behavior, where only modules are suppressed. I never saw a justification for the distinction, but I also never heard anyone complain.

For example, this compiles:

import scala.{Int => _, _}

class C {
  def x: Double = ???
  def y: Int = ???
}

With -Yno-imports, Double is still correctly imported, but Int errors.

@morgan-peschke People have asked for a simple -Ypreamble to prefix every source, so you could write -Ypreamble:scala.Predef.{badconvo => _, _}.

Currently, you can:

package mypre

object MyPre {
  def any2stringadd: Nothing = ???
}

and rely on shadowing to disable it:

$ scalac -Yimports:java.lang,scala,scala.Predef,mypre.MyPre noadd.scala
noadd.scala:5: error: value + is not a member of C
  def f(c: C) = c + " plus text"
                  ^
one error found

They removed various “language-forking” options. Maybe a simple flag would fall into that category.

Also, like Blanche DuBois, Scala has always depended on the kindness of strangers to contribute stuff.

Some preliminary results from usage analysis (unfinished), based on playing with dotty-community-build

Near all occurrences of discarded value are:

  • builder pattern (i.e. StringBuilder.append returns StringBuilder), so, total disabling without exceptions, as in Plan B, will be problematic: afraid, that sb.append("aaaa"):Unit will looks ugly for the big percent of users.
  • += (which is strange, according to Scala specifications this should be translated to assignment)
  • package object statement. (error in defs, should be readed ‘except object statement’)

Now running next experiment, where total conversion is in predef