Skip to content

Commit f5ef7a7

Browse files
authored
docs: Improve and expand documentation for control flow macros (#1367)
This PR continues the effort to create comprehensive, high-quality documentation for the Gerbil core macros. The main changes include: * **Improved Structure:** Renamed the old `expression.md` to `control-flow.md` and updated its title and introduction to more accurately reflect its scope (conditionals, logic, iteration, etc.). * **New Comprehensive Docs:** Added complete documentation for the following forms: * `begin0` * `delay` and `delay-atomic` * `@list` * **Standardization:** Introduced badges (`R7RS`, `Gerbil`, etc.) to clearly distinguish the origin of each macro, as suggested by Fare. This pattern has been applied to all documented forms in this file.
1 parent 51a0731 commit f5ef7a7

File tree

1 file changed

+230
-44
lines changed

1 file changed

+230
-44
lines changed

doc/reference/gerbil/core/expression.md renamed to doc/reference/gerbil/core/control-flow.md

Lines changed: 230 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Common Expression Forms
1+
# Control Flow, Logic, and Iteration
22

3-
These are common macros used for expressions.
3+
This page covers the core macros that control the flow of evaluation in a Gerbil program. It provides the fundamental tools to direct a program's logic through **conditionals and logical operators** (`cond`, `and`, `?`), **iterative loops** (`do`, `while`), and primitives for **evaluation control** (`delay`). Together, these forms organize how and when things happen in a program.
44

5-
## cond
5+
## cond <Badge type="tip" text="R7RS" vertical="middle" />
66
```scheme
77
(cond
88
<cond-clause> ...
@@ -68,7 +68,7 @@ the `=>` clause is useful when you need to use the specific truthy value returne
6868
- [`when`](#when)
6969
- [`unless`](#unless)
7070

71-
## case
71+
## case <Badge type="tip" text="R7RS" vertical="middle" />
7272
```scheme
7373
(case expr
7474
<case-clause> ...
@@ -126,7 +126,7 @@ Use `case` when you need to compare a single value against several lists of cons
126126
- [`cond`](#cond)
127127
- `if`
128128

129-
## and
129+
## and <Badge type="tip" text="R7RS" vertical="middle" />
130130
```scheme
131131
(and expr ...)
132132
```
@@ -158,7 +158,7 @@ Use `and` for two main purposes:
158158

159159
[`or`](#or)
160160

161-
## or
161+
## or <Badge type="tip" text="R7RS" vertical="middle" />
162162
```scheme
163163
(or expr ...)
164164
```
@@ -196,7 +196,7 @@ Use `or` for two main purposes:
196196

197197
[`and`](#and)
198198

199-
## when
199+
## when <Badge type="tip" text="R7RS" vertical="middle" />
200200
```scheme
201201
(when test expr ...)
202202
=>
@@ -231,7 +231,7 @@ Delete a file if it exists.
231231
- [`cond`](#cond)
232232
- `if`
233233

234-
## unless
234+
## unless <Badge type="tip" text="R7RS" vertical="middle" />
235235
```scheme
236236
(unless test expr ...)
237237
=>
@@ -266,7 +266,7 @@ Create a directory only if it doesn't already exist.
266266
- [`cond`](#cond)
267267
- `if`
268268

269-
## ?
269+
## ? <Badge type="tip" text="Gerbil" vertical="middle" />
270270
```scheme
271271
(? <predicate-expr> expr)
272272
(? <predicate-expr>)
@@ -430,63 +430,249 @@ For simple, one-off conditional execution, `if`, `when`, or `cond` are often mor
430430
- `not`
431431
- [`cond`](#cond)
432432
433-
## Begin0
434-
```
433+
## Begin0 <Badge type="tip" text="Gerbil" vertical="middle" />
434+
```scheme
435435
(begin0 expr rest ...)
436436
=>
437437
(let (val expr)
438438
rest ...
439439
val)
440440
```
441441

442-
Evaluates a sequence of expressions and reduces to the value of the
443-
expression.
442+
Evaluates a sequence of expressions in order, but returns the value of the **first** expression.
443+
444+
The `begin0` macro first evaluates `expr` and saves its value. It then evaluates all subsequent `rest ...` expressions in order for their side effects, **ignoring their return values**. Finally, it returns the saved value of the initial `expr`.
445+
446+
This is in contrast to the standard `begin` form, which returns the value of the *last* expression.
447+
448+
449+
::: tip Example
450+
Retrieve a user's data and then evicts it from cache.
451+
452+
```scheme
453+
> (def (get-and-evict! cache key)
454+
(begin0
455+
(hash-ref cache key)
456+
(hash-remove! cache key)))
457+
458+
> (def user-cache (hash ("user:1" "Alice") ("user:2" "Bob")))
459+
460+
> (get-and-evict! user-cache "user:1")
461+
"Alice"
444462
445-
## @list
463+
;; After the call, the cache no longer contains Alice's entry.
464+
> (hash-key? user-cache "user:1")
465+
#f
446466
```
447-
\[<list-expression-body> ...\]
448-
(@list <list-expression-body> ...)
467+
:::
468+
469+
### Context and Usage
470+
471+
Use `begin0` when you need to evaluate a series of expressions for their side effects but require the result of the very first expression. It elegantly combines retrieving a value and then performing cleanup or mutation operations related to that value.
472+
473+
### See Also
449474

450-
list-expression-body ...:
451-
. '<s-expression>
452-
. `<s-expression>
453-
. tail
454-
:: <s-expr>
455-
<s-expr> \... <list-expression-body> ...
456-
<s-expr> <list-expression-body> ...
475+
`begin`
476+
477+
## @list <Badge type="tip" text="Gerbil" vertical="middle" />
478+
479+
```scheme
480+
[<list-item> ...]
481+
(@list <list-item> ...)
482+
483+
list-item:
484+
<list-expression> "..."
485+
. <tail-expression>
486+
:: <tail-expression>
487+
<expression>
457488
```
458489

459-
The list constructor expression, normally implied with the `[]` reader
460-
macro.
490+
The primary list constructor, used with the `[...]` reader macro, which supports in-place list splicing and dotted-tail notation.
491+
492+
`@list` (and its reader macro `[...]`) provides a flexible way to construct lists. It evaluates its arguments and combines them into a new list. In almost all cases, you will use the convenient `[...]` syntax.
461493

462-
## delay
494+
Unlike the standard `list` procedure, the `[...]` constructor gives special meaning to certain symbols:
495+
496+
* The ellipsis `...` has a conditional behavior based on the expression that precedes it:
497+
* If the preceding expression evaluates to a list, it acts as a **splicing operator**, unpacking the elements of that list into the new list.
498+
* If the preceding expression is *not* a list, it **discards** the value of that expression from the final result.
499+
500+
* Both the dot `.` and colon-colon `::` act as **dotted-tail constructors**. They are functionally equivalent and make the *following expression* the final `cdr` (tail) of the list. They must be the penultimate item in the expression.
501+
502+
There is also a special unwrapping rule: if a list construction with a splicing `...` results in a single-element list, the result is the element itself (e.g., `[42 ...] => 42`).
503+
504+
**Note**: The current behavior of the ... operator with non-list values is under review and may change in a future version. See issue **[`#1368`](https://github.com/mighty-gerbils/gerbil/pull/1368)**.
505+
506+
507+
::: tip Examples
508+
509+
- **Basic list construction**
510+
```scheme
511+
> [1 (+ 2 3) "hello"]
512+
'(1 5 "hello")
463513
```
514+
515+
- **Splicing with `...`**
516+
```scheme
517+
> (let ((middle-items '(b c)))
518+
['a middle-items ... 'd])
519+
'(a b c d)
520+
```
521+
522+
- **Dotted-Tail construction with `.` and `::`**
523+
```scheme
524+
;; Creating an improper list
525+
> [1 2 . 3]
526+
'(1 2 . 3)
527+
528+
> [1 2 :: 3]
529+
'(1 2 . 3)
530+
531+
;; Creating a proper list by setting the tail to another list
532+
> [1 2 . '(3 4)]
533+
'(1 2 3 4)
534+
535+
> [1 2 :: '(3 4)]
536+
'(1 2 3 4)
537+
```
538+
:::
539+
540+
### Context and Usage
541+
542+
The `[...]` syntax is the modern and idiomatic way to construct lists in Gerbil, especially when combining computed values with existing lists.
543+
* Compared to the standard list procedure, `[...]` is more versatile due to the built-in `...` splicing operator.
544+
* Compared to quasiquote (\`) , `[...]` can be more readable for list construction. The `...` operator is analogous tounquote-splicing (`,@`), but acts on the preceding element.
545+
546+
### See Also
547+
548+
- `list`
549+
- `cons`
550+
- `quasiquote`
551+
552+
## delay <Badge type="tip" text="R7RS" vertical="middle" />
553+
```scheme
464554
(delay expr)
465555
```
466556

467-
Creates a promise to evaluate `expr` when needed with the `force` primitive.
468-
The value of the expression is memoized so it will only be evaluated once.
557+
Creates a promise, an object that encapsulates a delayed computation. The promise will evaluate `expr` only when its value is requested for the first time with the `force` procedure.
558+
559+
The value of the expression is **memoized**: it is computed only once, and subsequent calls to `force` on the same promise will return the cached value without re-evaluating the expression.
560+
561+
Internally, `delay` typically calls `make-promise` with a thunk (a zero-argument procedure) that evaluates `expr`. As an optimization, if `expr` is a literal constant, `delay` may return the value directly.
562+
563+
`delay` is efficient, but it is **not thread-safe**. If a promise might be forced by multiple threads concurrently, you must use [`delay-atomic`](#delay-atomic) to ensure correctness.
564+
565+
::: tip Example
566+
This example demonstrates both lazy evaluation and memoization. The `print` statement inside the `delay` acts as a probe to show us exactly when the computation runs.
567+
568+
```scheme
569+
;; Define a promise. The code inside is not executed yet.
570+
> (def p
571+
(delay
572+
(begin (print "=> Heavy computation running...")
573+
(+ 10 20))))
574+
575+
> (println "Promise has been created.")
576+
Promise has been created.
577+
578+
;; Force the promise for the first time.
579+
> (println "Forcing the promise...")
580+
Forcing the promise...
581+
582+
> (def result1 (force p))
583+
=> Heavy computation running...
584+
585+
> (println "Result: " result1)
586+
Result: 30
469587
470-
Delay internally calls `make-promise` with a thunk that evaluates `expr`.
471-
Delay is efficient, but only safe in the simple case of evaluation in a single thread
472-
of expressions that succeed (i.e. no escaping continuations that later restart).
473-
Using it in the more complex case may cause incorrect or inconsistent behavior.
474-
To support more such complex cases (at a slight extra cost), see `delay-atomic`.
588+
;; Force the promise again. The computation does not run a second time.
589+
> (println "Forcing the promise again...")
590+
Forcing the promise again...
475591
476-
## delay-atomic
592+
> (def result2 (force p))
593+
594+
> (println "Result: " result2)
595+
Result: 30
477596
```
597+
:::
598+
599+
### Context and Usage
600+
601+
`delay` and `force` are the fundamental building blocks for lazy evaluation in Scheme.
602+
603+
* Use `delay` to defer expensive computations until their results are actually needed.
604+
* This pattern is the foundation for implementing lazy data structures like streams (infinite lists).
605+
**Important:** If a promise might be **shared and forced by multiple threads**, prefer [`delay-atomic`](#delay-atomic) to prevent race conditions.
606+
607+
### See Also
608+
609+
- `force`
610+
- [`delay-atomic`](#delay-atomic)
611+
- `make-promise`
612+
613+
## delay-atomic <Badge type="tip" text="Gerbil" vertical="middle" />
614+
```scheme
478615
(delay-atomic expr)
479616
```
480617

481-
Creates a promise to evaluate `expr` when needed with the `force` primitive.
482-
The value of the expression is memoized so it will only be evaluated once.
618+
Creates a thread-safe promise to evaluate `expr` when needed with the `force` primitive. The value of the expression is memoized so it will only be evaluated once.
619+
620+
Internally, `delay-atomic` calls `make-atomic-promise`, which wraps the computation in a mutex (a lock) to ensure that even if multiple threads try to `force` the promise simultaneously, the underlying expression is evaluated exactly once.
621+
622+
`delay-atomic` is only slightly less efficient than [`delay`](#delay) and is safe for concurrent evaluation in multiple threads. It also supports failures and escapes (via continuations) from the thunk, and will issue an error if a thread attempts to re-enter an escaped computation.
623+
624+
::: tip Example
625+
This example simulates multiple threads trying to initialize a shared resource concurrently. Because `delay-atomic` is used, the initialization logic runs exactly once, regardless of which thread gets to it first.
626+
627+
```scheme
628+
> (import :std/iter)
629+
630+
;; An atomic promise with a side effect to visualize its evaluation.
631+
> (def shared-resource
632+
(delay-atomic
633+
(begin
634+
(displayln "=> Shared resource is being initialized...")
635+
(thread-sleep! 1) ; Simulate an expensive initialization
636+
"Resource Ready")))
637+
638+
;; Procedure executed by each thread
639+
> (def (get-shared-resource id)
640+
(displayln "Thread " id " is trying to access the shared resource...")
641+
(force shared-resource))
642+
643+
;; Spawn several threads that all run the task.
644+
> (def (spawn-threads)
645+
(map thread-join!
646+
(for/collect (i (in-range 5))
647+
(spawn (cut get-shared-resource i)))))
648+
649+
> (spawn-threads)
650+
;; possible output:
651+
;; "=> Shared resource is being initialized..." is printed only once.
652+
Thread 0 is trying to access the shared resource...
653+
=> Shared resource is being initialized...
654+
Thread 1 is trying to access the shared resource...
655+
Thread 2 is trying to access the shared resource...
656+
Thread 3 is trying to access the shared resource...
657+
Thread 4 is trying to access the shared resource...
658+
...
659+
660+
```
661+
:::
662+
663+
### Context and Usage
664+
665+
If a promise might be shared between threads, or if its computation involves complex control flow (like continuations that might be re-invoked), you must use `delay-atomic`.
666+
667+
Use the simpler [`delay`](#delay) only for lazy values in a single-threaded context with straightforward computations. The slight performance cost of `delay-atomic` is a small price to pay for correctness in concurrent programs.
668+
669+
### See Also
483670

484-
Delay-atomic internally calls `make-atomic-promise` with a thunk that evaluates `expr`.
485-
Delay-atomic is only slightly less efficient than `delay`, and is safe even in the case of
486-
concurrent evaluation in a multiple threads; it will also support failures and escapes from
487-
the thunk, and will issue an error if someone attempts to reenter such escaped thunks.
671+
- [`delay`](#delay)
672+
- `force`
673+
- `make-atomic-promise`
488674

489-
## do
675+
## do <Badge type="tip" text="R7RS" vertical="middle" />
490676
```scheme
491677
(do ((var init step ...) ...)
492678
(test result ...)
@@ -542,7 +728,7 @@ this example shows how `do` can build a result without a loop body. The logic is
542728
- `foldl`
543729
- [`do-while`](#do-while)
544730

545-
## do-while
731+
## do-while <Badge type="tip" text="Gerbil" vertical="middle" />
546732
```scheme
547733
(do-while ((var init step ...) ...)
548734
(test result ...)
@@ -590,7 +776,7 @@ Use `do-while` for situations where an action must be performed before the condi
590776
- [`until`](#until)
591777

592778

593-
## while
779+
## while <Badge type="tip" text="Gerbil" vertical="middle" />
594780
```scheme
595781
(while test body ...)
596782
```
@@ -628,7 +814,7 @@ A simple counter from 0 to 4.
628814
- [`until`](#until)
629815

630816

631-
## until
817+
## until <Badge type="tip" text="Gerbil" vertical="middle" />
632818
```scheme
633819
(until test body ...)
634820
=>

0 commit comments

Comments
 (0)