History
Date | Version |
---|---|
Sep 27 2019 | Pre-SIP |
Oct 15 2019 | added description of Try.discard and Scalafix rule |
Oct 22, 2019, | added variant B (same motivation, but more simple solution) [still in progress] |
Oct 27, 2019 | added handling of builder case for B variant; |
Summary:
Limit implicit value discarding.
Variant A: Allows to configure value discarding by allowing to provide appropriative implicit conversions for the given value types.
Variant B: Disable discarding values by default and using typing ascription to explicit discarding. [TODO: think about moving to separate thread]. [TODO: reference to pull request, track authoring section]
Overview:
In current scala version exists two cases, when the value is discarded:
-
When this is a value of the non-final expression in the block
-
When this is a value that should be cast to Unit.
In such cases, the evaluated value is discarded without error.We propose to limit the value discarding:
-
variant A). Allow discarding only those values, which implements a conversion to Unit (or a special marked class DiscardedUnit). If some type not supports discarded value conversion, then dropping values of this type is prohibited.
-
variant B), disallow value discarding unless explicit unit type ascription is provided if this value
â is not an instance of âthis.typeâ (to support builder pattern)
â TODO: java interoperability:StringBuilder.append("sss")
returns ```StringBuilder``
Ie in any case, we need some mechanism, for specifying such types.
Motivation:
Example 1. Limit the number of potentials defects when effects, encoded in values are not handled properly, as in the next example:
Letâs look on the following code block:
def doSomething(input:X): Try[Y] = ....
It is a common error to forgot to handle Try failure and write something like:
{
doSomething(âAâ)
doSomething(âBâ)
âCâ
}
Instead correct, but verbose
for { _ <- doSomething(âAâ)
_ <- doSomething(âBâ)
} yield C
With limited value discard:
- Variant A: Try will define an implicit discarded conversion which will check exception, simple sequencing of sentences in the block will work as expected. So, it will become possible to use API written in effects wrapping style from programs written in direct effect style.
The desugared block will look like:
{
implicitly[Try[String] => Unit].apply(doSomething(âAâ))
implicitly[Try[String]=> Unit].apply(doSomething(âBâ))
âCâ
}
- Variant B: This code will cause a compile-time error. Explicit unit type ascription will be needed for forcing value discarding. I.e., if we want to discard values of doSomething(A) and doSomething(B), we will need to write:
{
doSomething(âAâ):Unit
doSomething(âBâ):Unit
âCâ
}
Example 2. Prevent error-prone situations during the integration of async monadic constructs into control flow concurrency libraries.
Ie. Imagine, that we work with some framework, which allows developers to avoid the accidental complexity of async operations. We can do this in many ways. (for example, via macroses or project Loom[ Main - Main - OpenJDK Wiki ]),
I.e., if we have an API, which looks like:
def doSomething(input:X): Future[Unit]
And some concurrency library, which maps monadic expressions to control flow, when we can write something like:
(given ctx: ContinuationContext) =>
{
doSomething(âAâ)
If (someCondition) {
doSomething(âBâ)
}
}
Instead of verbose
val spawnA = doSomething(âAâ)
var spawnB = if (someCondition) doSomethong(âBâ) else Future success ()
for { _ <- spawnA
_ <- spawnB
} yiled ()
Then, with current value discarding, is impossible to lift effects from doSomething("A")
into control flow, without transforming full AST of enclosing block. We have no point to insert the adapter, because the value will be discarded. So, value discarding makes the task of building such a framework quite complicated.
With limited value discarding, we will be able to handle this situation locally:
*Variant A:
Defining implicit conversion:
implicit def discardFuture[T](f: Future[T]):Unit given (ctx: ContinuationContext) = {
ctx.adopt(f)
}
and then use doSomething("A")
in control flow.
- Variant B
Direct using âdoSomething(âAâ)â in control flow will be impossible. The programmer will write something like:
await doSomething(âAâ)
If (someCondition) {
await doSomething(âBâ)
}
And it will be impossible to forget put await before ``doSomething``` because the compiler will produce an error about value discard.
Needed changes:
- variant A: Allow value discarding only in case when exists an implicit conversion from type of discarded value to the unit (variant â special DiscardedUnit marker) in the given context.
The value in position when discarded (i.e., with a non-final location in a block which is not a call of the super or primary constructor) should be evaluated as follows:
- If exists conversion to the discarded unit â apply one before discarding.
- If not â generate a compile-time error.
Add to the standard library:
- empty default convertors for primitive values;
- effect convertors for Try;
- add to Try discard() method, which does nothing and return void.
- add to scalafix rule which will save the current semantics of discarded values for effects
*variant B: Disallow value discarding without type ascription.
- when the value in position should be discarded and this is not a type ascription: generate an error.
- add to scalafix rule, which transforms discarding values into explicit unit ascription.
Example 3:
Implementation:
Variant A: The proof of concept implementation (not excessive tested and should be viewed only as a starting point) is available at GitHub - rssh/dotty at no-discard (diff: Implemented -Yno-discard-values by rssh ¡ Pull Request #1 ¡ rssh/dotty ¡ GitHub ) as a patch to the dotty master branch.
The feature is enabled by â-Yno-discard-valuesâ flag. (Scalafix part yet not implemented).