Skip to content
Merged
Show file tree
Hide file tree
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
8 changes: 6 additions & 2 deletions concepts/combinators/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ need to `dup`, then run F, then keep the original" and just write
| different ops on two values | `swap F swap G` | `[ F ] [ G ] bi*`|

The `2`-prefixed family (`2bi`, `2dip`, `2tri`, `2dup`, `2drop`,
`2swap`) does the same when each operation needs *two* inputs.
The `n*`-suffixed family scales arbitrarily.
`2nip`, `2swap`) does the same when each operation needs *two*
inputs; `3dup` and `4dup` extend the duplicating idiom to three
or four top-of-stack values.

Beyond the cleave/dip/bi family, [`kernel`][kernel] also offers
control-flow variants that propagate `f` cleanly:
Expand All @@ -29,6 +30,9 @@ control-flow variants that propagate `f` cleanly:
| `pop*` | discarding variant of `pop` (mutates a vector) |
| `dupd` | duplicate the *second*-from-top |
| `pick` | copy the *third*-from-top to the top |
| `swapd` | swap the second and third from the top |
| `rotd` | rotate the bottom three of a four-deep stack |
| `tuck` | copy the top under the second-from-top |
| `with` | bake a fixed value into a quotation for HOS |
| `while` | loop while a predicate holds |
| `until` | loop until a predicate holds |
Expand Down
6 changes: 6 additions & 0 deletions concepts/concurrency/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"authors": [
"keiravillekode"
],
"blurb": "Run cooperative threads, fork-join with parallel-map and promises, and protect shared state with locks."
}
52 changes: 52 additions & 0 deletions concepts/concurrency/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# About

Factor's threads are *cooperative coroutines* on a single OS thread.
A thread runs until it explicitly yields or performs blocking I/O,
at which point the scheduler picks another runnable thread. Two
implications:

- Anything between yields is effectively atomic.
- True multi-core parallelism requires going outside the standard
`threads` vocab (e.g. `concurrency.distributed` for multi-process
designs).

The standard concurrency primitives are layered:

| Vocab | Provides |
|-----------------------------|----------------------------------------|
| `threads` | `spawn`, `yield`, thread identity |
| `concurrency.promises` | `<promise>`, `fulfill`, `?promise` |
| `concurrency.locks` | `<lock>`, `with-lock` |
| `concurrency.combinators` | `parallel-map`, `parallel-each` |
| `concurrency.semaphores` | `<semaphore>`, `acquire`, `release` |
| `concurrency.mailboxes` | per-thread inboxes, `send`, `receive` |
| `concurrency.channels` | rendezvous handles, `to`, `from` |

```factor
USING: concurrency.combinators concurrency.locks
concurrency.promises kernel sequences threads ;

! parallel-map: fork-join, in order
{ "alpha" "beta" "gamma" } [ length ] parallel-map .
! => { 5 4 5 }

! spawn + promise: hand-rolled fork-join
<promise> [
[ "hello, world" swap fulfill ] curry "greeter" spawn drop
] keep ?promise . ! => "hello, world"

! <lock> + with-lock: protect a shared mutable slot
<lock> :> guard
guard [ "do thing under lock" drop ] with-lock
```

`parallel-map` is the high-level API and what idiomatic Factor reaches
for first. The `spawn` / `<promise>` / `?promise` triple is what
`parallel-map` is built from — handy when the iteration shape doesn't
fit map (e.g. spawning workers that talk back through other channels).

Locks protect *shared mutable state* — typically a tuple slot or a
hashtable that more than one thread reads or writes. Treat both reads
and writes as needing the lock if there's any non-atomic compound
update; missing a lock on either side is the classic recipe for a
torn read.
10 changes: 10 additions & 0 deletions concepts/concurrency/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Introduction

Factor runs cooperative threads in a single OS process. They yield
control at I/O and at explicit `yield` calls, which makes
synchronisation simpler than in pre-emptively scheduled languages —
but races are still possible, since any thread can run between two
non-atomic operations on shared state. Factor's concurrency vocabs
provide the standard toolkit: `spawn` to start a thread, promises
for single-shot result handoff, `parallel-map` for fork/join over a
sequence, and locks for mutual exclusion.
18 changes: 18 additions & 0 deletions concepts/concurrency/links.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"url": "https://docs.factorcode.org/content/vocab-threads.html",
"description": "threads vocabulary reference"
},
{
"url": "https://docs.factorcode.org/content/vocab-concurrency.combinators.html",
"description": "concurrency.combinators vocabulary reference"
},
{
"url": "https://docs.factorcode.org/content/vocab-concurrency.locks.html",
"description": "concurrency.locks vocabulary reference"
},
{
"url": "https://docs.factorcode.org/content/vocab-concurrency.promises.html",
"description": "concurrency.promises vocabulary reference"
}
]
13 changes: 13 additions & 0 deletions concepts/stack-effect/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ a compile-time error, which catches a class of bugs that would be
runtime errors in a dynamically-typed language without this kind of
declaration.

The handful of `kernel` shuffle words that come up in the very first
exercises:

```
dup ( x -- x x )
swap ( x y -- y x )
over ( x y -- x y x )
```

The full `kernel` shuffle family (`pick`, `rot`, `-rot`, `nip`,
`tuck`, etc.) and the larger `2`-prefixed cousins are covered in
`booleans` and `combinators`.

By convention:

- Predicates end in `?` and produce a boolean (`even?`, `empty?`).
Expand Down
3 changes: 2 additions & 1 deletion concepts/strings/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ The string-specific words live mostly in `splitting`, `ascii`, and
| `>lower` | `ascii` | lowercase (ASCII) |
| `>upper` | `ascii` | uppercase (ASCII) |
| `[ blank? ] trim` | `sequences` | strip leading/trailing whitespace |
| `first`, `second`, `first2`, `first3` | `sequences` | unpack the leading element(s) |
| `first`, `second`, `third`, `fourth` | `sequences` | the leading slot |
| `first2`, `first3` | `sequences` | unpack two or three leading slots |
| `>string` | `strings` | turn a sequence of chars into a string |

For numeric ↔ string round-tripping, [`math.parser`][math.parser]
Expand Down
1 change: 1 addition & 0 deletions concepts/unicode/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ LETTER? ( c -- ? ) ! uppercase letter
letter? ( c -- ? ) ! lowercase letter
Letter? ( c -- ? ) ! letter, either case
digit? ( c -- ? ) ! decimal digit
digit> ( c -- n ) ! the integer value of a digit char
blank? ( c -- ? ) ! whitespace
alpha? ( c -- ? ) ! letter or decimal digit
```
Expand Down
31 changes: 31 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,20 @@
"combinators"
],
"status": "beta"
},
{
"slug": "quayside-crew",
"name": "Quayside Crew",
"uuid": "e520b985-797e-42b6-a366-990db2d58796",
"concepts": [
"concurrency"
],
"prerequisites": [
"higher-order-sequences",
"tuples",
"locals"
],
"status": "beta"
}
],
"practice": [
Expand Down Expand Up @@ -1252,6 +1266,18 @@
],
"difficulty": 6
},
{
"slug": "parallel-letter-frequency",
"name": "Parallel Letter Frequency",
"uuid": "ebcacb36-d583-4e3e-ac27-c72fc1a1b4c4",
"practices": [],
"prerequisites": [
"concurrency",
"unicode",
"assocs"
],
"difficulty": 6
},
{
"slug": "rail-fence-cipher",
"name": "Rail Fence Cipher",
Expand Down Expand Up @@ -1565,6 +1591,11 @@
"uuid": "c8a58a3d-d6e4-4325-b0c9-d2ee4bfc9df3",
"slug": "dynamic-variables",
"name": "Dynamic Variables"
},
{
"uuid": "4b136c82-48c0-4dd6-859d-e5e76c898540",
"slug": "concurrency",
"name": "Concurrency"
}
],
"key_features": [
Expand Down
3 changes: 3 additions & 0 deletions exercises/concept/annalyns-infiltration/.docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ rot ( x y z -- y z x )
rotd ( w x y z -- w y z x )
spin ( x y z -- z y x )
nip ( x y -- y )
tuck ( x y -- y x y )

2dup ( x y -- x y x y )
3dup ( x y z -- x y z x y z )
4dup ( w x y z -- w x y z w x y z )
2drop ( x y -- )
2nip ( x y z -- z )
2swap ( x y z w -- z w x y )
Expand Down
3 changes: 3 additions & 0 deletions exercises/concept/joiners-journey/.docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ rot ( x y z -- y z x )
rotd ( w x y z -- w y z x )
spin ( x y z -- z y x )
nip ( x y -- y )
tuck ( x y -- y x y )

2dup ( x y -- x y x y )
3dup ( x y z -- x y z x y z )
4dup ( w x y z -- w x y z w x y z )
2drop ( x y -- )
2nip ( x y z -- z )
2swap ( x y z w -- z w x y )
Expand Down
50 changes: 50 additions & 0 deletions exercises/concept/quayside-crew/.docs/hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Hints

## 1. `weigh-crate`

- One word from `math.statistics` does it.

## 2. `weigh-all`

- `parallel-map` (in [`concurrency.combinators`][combinators])
has the same shape as `map` — pass a quotation that
transforms one crate into one weight.

## 3. `<crane>`

- Define the crane with `TUPLE: crane lock tonnage ;` so the
slots are named.
- Use `<lock>` (in [`concurrency.locks`][locks]) for the lock
slot and `0` for the starting tonnage. `boa` constructs a
tuple from the values on the stack in slot order.

## 4. `hoist-crate`

- `with-lock ( lock quot -- )` runs the quotation while holding
the lock. The quotation needs to read the crane's tonnage,
add `weight`, and store the result back. `change-tonnage`
(auto-generated by `TUPLE:`) does exactly the read-modify-
write step in one word.
- Locals make this readable: `:: hoist-crate ( weight crane -- )`
lets you refer to `weight` and `crane` inside the lock body.

## 5. `crane-tonnage`

- The reader needs the lock too, so a write in flight can't be
half-finished when you observe it.

## 6. `load-cargo`

- For each crate, create a `<promise>` and `spawn` a thread that
weighs the crate, hoists it, and `fulfill`s the promise. The
fulfilled value can be anything — a sentinel — since the
promise is only used to signal completion.
- Collect the promises into a sequence as you spawn (locals make
this neat; `map` with a lambda works well).
- After every thread is spawned, walk the sequence of promises
and `?promise` each one. `?promise` blocks until that thread
has finished, so by the time the last one returns, every
hoist has completed.

[combinators]: https://docs.factorcode.org/content/vocab-concurrency.combinators.html
[locks]: https://docs.factorcode.org/content/vocab-concurrency.locks.html
87 changes: 87 additions & 0 deletions exercises/concept/quayside-crew/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Instructions

It's the night shift down at Hull's quayside. The freighter
*Olwen* is moored alongside, and a crew of dockhands works the
manifest in parallel. Every crate must be weighed before it
goes aboard, and the harbour master keeps a single running
**tonnage tally** that every hoist updates. The catch: there is
only one quayside crane, so the tally cannot be torn by two
dockhands updating it at the same time.

Each crate is an array of item weights. Crate weight is the sum
of those items.

## 1. Weigh a crate

Define `weigh-crate` to take a crate and return its total weight.
This is plain sequential work — one crate, one number out.

```factor
{ 12 8 15 } weigh-crate .
! => 35
```

## 2. Weigh the whole manifest in parallel

Define `weigh-all` to take an array of crates and return the
array of their weights, in the same order. Use `parallel-map`
so the per-crate work happens concurrently.

```factor
{ { 5 5 } { 10 } { 3 4 5 } } weigh-all .
! => { 10 10 12 }
```

## 3. Build a fresh crane

Define `<crane>` to construct a new crane: a tuple with a fresh
`<lock>` and a tonnage of `0`. The crane is the shared resource
that subsequent tasks will protect.

```factor
<crane> tonnage>> .
! => 0
```

## 4. Hoist a crate onto the running tally

Define `hoist-crate` to take a `weight` and a `crane` and add
the weight to the crane's tonnage **under the crane's lock**, so
the read-add-write is atomic against other dockhands hoisting
at the same time.

```factor
<crane>
dup 35 swap hoist-crate
dup 17 swap hoist-crate
tonnage>> .
! => 52
```

## 5. Read the running tonnage

Define `crane-tonnage` to return the crane's current tonnage —
also under the lock, so the read can't see a torn value mid-
hoist.

```factor
<crane>
dup 35 swap hoist-crate
crane-tonnage .
! => 35
```

## 6. Load the cargo

Define `load-cargo` to take an array of crates and a crane,
and **for each crate, spawn a dockhand thread** that weighs the
crate and hoists the weight onto the crane. The word returns
only once every dockhand has finished. Use `<promise>` and
`?promise` to coordinate the join.

```factor
<crane> :> crane
{ { 5 5 } { 10 } { 3 4 5 } } crane load-cargo
crane crane-tonnage .
! => 32
```
Loading