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

boundary/break expression plus several examples of custom control structures #3105

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 176 additions & 1 deletion _overviews/scala3-book/control-structures.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ Scala has the control structures you expect to find in a programming language, i
- `while` loops
- `try`/`catch`/`finally`

It also has two other powerful constructs that you may not have seen before, depending on your programming background:
It also has three other powerful constructs that you may not have seen before, depending on your programming background:

- `for` expressions (also known as _`for` comprehensions_)
- `match` expressions
- `boundary`/`break` expressions

These are all demonstrated in the following sections.

Expand Down Expand Up @@ -989,4 +990,178 @@ finally

Assuming that the `openAndReadAFile` method uses the Java `java.io.*` classes to read a file and doesn't catch its exceptions, attempting to open and read a file can result in both a `FileNotFoundException` and an `IOException`, and those two exceptions are caught in the `catch` block of this example.

## boundary/break

The `boundary`/`break` expression is used to exit a block of code while optionally returning a value.
For example, it can be used to break out of a nested loop:

{% tabs control-structures-32 %}
{% tab 'Scala 3 Only' %}

```scala
import scala.util.boundary, boundary.break

val result =
boundary:
for i <- 1 to 10 do
for j <- 1 to 10 do
if i == 7 && j == 9 then break("found")
"not found"

println(result)
```

{% endtab %}
{% endtabs %}

Since `break` uses an exception to control the execution flow, mixing it with `try`/`catch` can lead to unexpected results:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one looks like a potential huge footgun if someone doesn't notice break throws an exception, so I wanted to mention it. Please notify if the try/catch example is OK and should stay.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try also swallows the control flow because internally it uses try/catch in a non-boundary/break compatible way. It might also be worth explaining how to avoid catching it. (E.g. case e: Exception if !e.isInstanceOf[Break[?]].)


{% tabs control-structures-33 %}
{% tab 'Scala 3 Only' %}

```scala
import scala.util.boundary, boundary.break

val result = boundary:
try
for i <- 1 to 10 do
some_operation(i)
if i == 5 then break(i)
catch
case e: Exception => -1

println(result) // prints -1, not 5
```

{% endtab %}
{% endtabs %}

The right way to use it would be to place the `boundary` expression inside the `try` block:

{% tabs control-structures-34 %}
{% tab 'Scala 3 Only' %}

```scala
import scala.util.boundary, boundary.break

val result =
try
boundary:
for i <- 1 to 10 do
some_operation(i)
if i == 5 then break(i)
catch
case e: Exception => -1

println(result) // 5
```

{% endtab %}
{% endtabs %}

## Custom control structures

The use of Scala 3 quiet syntax, by-name parameters, context parameters and `boundary`/`break` allows for writing code
that resembles the built-in control structures and helps with reducing the boilerplate.
In this example, the `repeat` function wraps a simple `for` loop, and the by-name `action` parameter will be executed `n`
times for its side effects.

{% tabs control-structures-35 %}
{% tab 'Scala 3 Only' %}

```scala
def repeat(n: Int)(action: => Unit): Unit =
for i <- 1 to n do
action

repeat(5):
println("test")
```

{% endtab %}
{% endtabs %}

Here's an example that simulates a `do`-`while` loop by using `boundary`/`break`:

{% tabs control-structures-36 %}
{% tab 'Scala 3 Only' %}

```scala
import scala.util.boundary, boundary.break, boundary.Label

def loop(action: Label[Unit] ?=> Unit): Unit =
boundary[Unit]:
while true do
action

def until(condition: => Boolean)(using Label[Unit]): Unit =
if condition then break()
```

{% endtab %}
{% endtabs %}

The `loop` function will continue to execute `action` indefinitely, unless the execution breaks out of the internal
`while` loop. The `until` function will stop the execution of `loop` if `condition` is met. `break()` needs a context parameter
of type `Label[Unit]`, denoting which `boundary` it is supposed to break to.
To use the `loop`/`until` structure, pass the code to be executed as an argument to `loop`, including the `until` check:

{% tabs control-structures-37 %}
{% tab 'Scala 3 Only' %}

```scala
var i = 0
loop:
println(i)
i = i + 1
until(i == 10)
```

{% endtab %}
{% endtabs %}

The following example uses a context parameter to store actions to be executed after a block of code is completed. The
`with_defer` function defines a scope in which `defer` can be used to defer execution of actions.
Comment on lines +1123 to +1124
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this because context parameters are not mentioned yet at this point. Maybe the custom control structures section should be moved to a separate page?


{% tabs control-structures-38 %}
{% tab 'Scala 3 Only' %}

```scala
import scala.collection.mutable.ArrayBuffer

class DeferredAction(action: => Unit):
def execute = action

def defer(action: => Unit)(using buffer: ArrayBuffer[DeferredAction]) =
buffer += new DeferredAction(action)

def with_defer(action: ArrayBuffer[DeferredAction] ?=> Unit) =
given deferedActions: ArrayBuffer[DeferredAction] = ArrayBuffer[DeferredAction]()
try
action
finally
deferedActions.foreach(_.execute)
```

{% endtab %}
{% endtabs %}

To use it, place the code inside the `with_defer` block. Bear in mind that there is no guarantee that a deferred action
actually executes, for example if another action throws an exception.

{% tabs control-structures-39 %}
{% tab 'Scala 3 Only' %}

```scala
with_defer:
defer:
println("this will be second")
defer:
println("this will be printed at the end")
println("this will be printed first")
```

{% endtab %}
{% endtabs %}

[matchable]: {{ site.scala3ref }}/other-new-features/matchable.html
Loading