From 9080cf276a9235042bc8cd1d26a7b4e607182d66 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Wed, 29 Apr 2026 19:44:26 +1000 Subject: [PATCH] Quayside Crew We introduce multithreading. --- concepts/combinators/about.md | 8 +- concepts/concurrency/.meta/config.json | 6 + concepts/concurrency/about.md | 52 ++++ concepts/concurrency/introduction.md | 10 + concepts/concurrency/links.json | 18 ++ concepts/stack-effect/about.md | 13 + concepts/strings/about.md | 3 +- concepts/unicode/about.md | 1 + config.json | 31 +++ .../.docs/introduction.md | 3 + .../joiners-journey/.docs/introduction.md | 3 + .../concept/quayside-crew/.docs/hints.md | 50 ++++ .../quayside-crew/.docs/instructions.md | 87 ++++++ .../quayside-crew/.docs/introduction.md | 90 +++++++ .../concept/quayside-crew/.meta/config.json | 17 ++ .../concept/quayside-crew/.meta/design.md | 46 ++++ .../quayside-crew/.meta/exemplar.factor | 33 +++ .../quayside-crew/quayside-crew-tests.factor | 56 ++++ .../quayside-crew/quayside-crew.factor | 22 ++ .../.docs/instructions.md | 7 + .../.meta/config.json | 17 ++ .../.meta/example.factor | 9 + .../.meta/generator.jl | 28 ++ .../.meta/tests.toml | 49 ++++ .../parallel-letter-frequency-tests.factor | 255 ++++++++++++++++++ .../parallel-letter-frequency.factor | 5 + 26 files changed, 916 insertions(+), 3 deletions(-) create mode 100644 concepts/concurrency/.meta/config.json create mode 100644 concepts/concurrency/about.md create mode 100644 concepts/concurrency/introduction.md create mode 100644 concepts/concurrency/links.json create mode 100644 exercises/concept/quayside-crew/.docs/hints.md create mode 100644 exercises/concept/quayside-crew/.docs/instructions.md create mode 100644 exercises/concept/quayside-crew/.docs/introduction.md create mode 100644 exercises/concept/quayside-crew/.meta/config.json create mode 100644 exercises/concept/quayside-crew/.meta/design.md create mode 100644 exercises/concept/quayside-crew/.meta/exemplar.factor create mode 100644 exercises/concept/quayside-crew/quayside-crew/quayside-crew-tests.factor create mode 100644 exercises/concept/quayside-crew/quayside-crew/quayside-crew.factor create mode 100644 exercises/practice/parallel-letter-frequency/.docs/instructions.md create mode 100644 exercises/practice/parallel-letter-frequency/.meta/config.json create mode 100644 exercises/practice/parallel-letter-frequency/.meta/example.factor create mode 100644 exercises/practice/parallel-letter-frequency/.meta/generator.jl create mode 100644 exercises/practice/parallel-letter-frequency/.meta/tests.toml create mode 100644 exercises/practice/parallel-letter-frequency/parallel-letter-frequency/parallel-letter-frequency-tests.factor create mode 100644 exercises/practice/parallel-letter-frequency/parallel-letter-frequency/parallel-letter-frequency.factor diff --git a/concepts/combinators/about.md b/concepts/combinators/about.md index 0e855c7d..df55fb21 100644 --- a/concepts/combinators/about.md +++ b/concepts/combinators/about.md @@ -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: @@ -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 | diff --git a/concepts/concurrency/.meta/config.json b/concepts/concurrency/.meta/config.json new file mode 100644 index 00000000..169559fa --- /dev/null +++ b/concepts/concurrency/.meta/config.json @@ -0,0 +1,6 @@ +{ + "authors": [ + "keiravillekode" + ], + "blurb": "Run cooperative threads, fork-join with parallel-map and promises, and protect shared state with locks." +} diff --git a/concepts/concurrency/about.md b/concepts/concurrency/about.md new file mode 100644 index 00000000..bb601d77 --- /dev/null +++ b/concepts/concurrency/about.md @@ -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` | ``, `fulfill`, `?promise` | +| `concurrency.locks` | ``, `with-lock` | +| `concurrency.combinators` | `parallel-map`, `parallel-each` | +| `concurrency.semaphores` | ``, `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 + [ + [ "hello, world" swap fulfill ] curry "greeter" spawn drop +] keep ?promise . ! => "hello, world" + +! + with-lock: protect a shared mutable slot + :> 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` 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. diff --git a/concepts/concurrency/introduction.md b/concepts/concurrency/introduction.md new file mode 100644 index 00000000..a373e4d2 --- /dev/null +++ b/concepts/concurrency/introduction.md @@ -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. diff --git a/concepts/concurrency/links.json b/concepts/concurrency/links.json new file mode 100644 index 00000000..27ee5a17 --- /dev/null +++ b/concepts/concurrency/links.json @@ -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" + } +] diff --git a/concepts/stack-effect/about.md b/concepts/stack-effect/about.md index 1ec9b339..1cc29529 100644 --- a/concepts/stack-effect/about.md +++ b/concepts/stack-effect/about.md @@ -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?`). diff --git a/concepts/strings/about.md b/concepts/strings/about.md index fd9450ce..3916c308 100644 --- a/concepts/strings/about.md +++ b/concepts/strings/about.md @@ -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] diff --git a/concepts/unicode/about.md b/concepts/unicode/about.md index 65288dc3..358be3db 100644 --- a/concepts/unicode/about.md +++ b/concepts/unicode/about.md @@ -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 ``` diff --git a/config.json b/config.json index 9bdccd22..f78bef13 100644 --- a/config.json +++ b/config.json @@ -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": [ @@ -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", @@ -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": [ diff --git a/exercises/concept/annalyns-infiltration/.docs/introduction.md b/exercises/concept/annalyns-infiltration/.docs/introduction.md index eab81ac4..52262115 100644 --- a/exercises/concept/annalyns-infiltration/.docs/introduction.md +++ b/exercises/concept/annalyns-infiltration/.docs/introduction.md @@ -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 ) diff --git a/exercises/concept/joiners-journey/.docs/introduction.md b/exercises/concept/joiners-journey/.docs/introduction.md index b4cedbcc..1ad6556a 100644 --- a/exercises/concept/joiners-journey/.docs/introduction.md +++ b/exercises/concept/joiners-journey/.docs/introduction.md @@ -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 ) diff --git a/exercises/concept/quayside-crew/.docs/hints.md b/exercises/concept/quayside-crew/.docs/hints.md new file mode 100644 index 00000000..17c47869 --- /dev/null +++ b/exercises/concept/quayside-crew/.docs/hints.md @@ -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. `` + +- Define the crane with `TUPLE: crane lock tonnage ;` so the + slots are named. +- Use `` (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 `` 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 diff --git a/exercises/concept/quayside-crew/.docs/instructions.md b/exercises/concept/quayside-crew/.docs/instructions.md new file mode 100644 index 00000000..0694fe48 --- /dev/null +++ b/exercises/concept/quayside-crew/.docs/instructions.md @@ -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 `` to construct a new crane: a tuple with a fresh +`` and a tonnage of `0`. The crane is the shared resource +that subsequent tasks will protect. + +```factor + 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 + +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 + +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 `` and +`?promise` to coordinate the join. + +```factor + :> crane +{ { 5 5 } { 10 } { 3 4 5 } } crane load-cargo +crane crane-tonnage . +! => 32 +``` diff --git a/exercises/concept/quayside-crew/.docs/introduction.md b/exercises/concept/quayside-crew/.docs/introduction.md new file mode 100644 index 00000000..cb2896aa --- /dev/null +++ b/exercises/concept/quayside-crew/.docs/introduction.md @@ -0,0 +1,90 @@ +# Introduction + +Factor's threads are *cooperative coroutines* — they share one OS +thread, hand control to each other at I/O and at explicit yield +points, and never run truly simultaneously. Anything you do +*between* yields is effectively atomic. But if a compound +operation (read-modify-write) yields midway, another thread can +slip in and change the world before you finish — and that's a race. + +This exercise covers four primitives that come up over and over: + +## `parallel-map` — fork/join over a sequence + +``` +parallel-map ( seq quot: ( elt -- newelt ) -- newseq ) +``` + +In [`concurrency.combinators`][combinators]. Spawns one thread per +element, runs the quotation, and collects the results back into a +new sequence in the same order: + +```factor +USING: concurrency.combinators sequences ; + +{ "alpha" "beta" "gamma" } [ length ] parallel-map . +! => { 5 4 5 } +``` + +## `spawn` — start a thread + +``` +spawn ( quot name -- thread ) +``` + +In [`threads`][threads]. The quotation runs concurrently; `name` +is a label for debugging. The returned thread handle is mostly +uninteresting at this level — `drop` it. + +## Promises — single-shot value handoff + +``` + ( -- promise ) +fulfill ( value promise -- ) +?promise ( promise -- value ) +``` + +In [`concurrency.promises`][promises]. A promise is a one-shot +mailbox: one thread fulfils it, others read it. `?promise` blocks +until the promise has been fulfilled, then returns the value. + +```factor +USING: concurrency.promises kernel threads ; + + [ + [ 42 swap fulfill ] curry "answer" spawn drop +] keep ?promise . +! => 42 +``` + +`spawn` + `` is what `parallel-map` is built from; +hand-rolling them is useful when each task does something other +than "compute one element." + +## Locks — mutual exclusion + +``` + ( -- lock ) +with-lock ( lock quot -- ) +``` + +In [`concurrency.locks`][locks]. Only one thread can hold a given +lock at a time. `with-lock` acquires, runs the quotation, +releases — even if the quotation throws. + +```factor +USING: concurrency.locks kernel ; + + :> guard +guard [ ! exclusive section ! ] with-lock +``` + +Use a lock whenever a value is *read or written by more than one +thread* and the operations on it aren't naturally atomic. Both +reads and writes need the lock — a read that yields midway can see +torn state. + +[combinators]: https://docs.factorcode.org/content/vocab-concurrency.combinators.html +[locks]: https://docs.factorcode.org/content/vocab-concurrency.locks.html +[promises]: https://docs.factorcode.org/content/vocab-concurrency.promises.html +[threads]: https://docs.factorcode.org/content/vocab-threads.html diff --git a/exercises/concept/quayside-crew/.meta/config.json b/exercises/concept/quayside-crew/.meta/config.json new file mode 100644 index 00000000..10a3930c --- /dev/null +++ b/exercises/concept/quayside-crew/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "quayside-crew/quayside-crew.factor" + ], + "test": [ + "quayside-crew/quayside-crew-tests.factor" + ], + "exemplar": [ + ".meta/exemplar.factor" + ] + }, + "blurb": "Run the quayside night shift with cooperative threads, parallel-map, promises, and a lock-protected tonnage tally." +} diff --git a/exercises/concept/quayside-crew/.meta/design.md b/exercises/concept/quayside-crew/.meta/design.md new file mode 100644 index 00000000..fff806b6 --- /dev/null +++ b/exercises/concept/quayside-crew/.meta/design.md @@ -0,0 +1,46 @@ +# Design + +## Goal + +Introduce Factor's standard concurrency toolkit — cooperative +threads, promises, the parallel-map combinator, and locks — by +having the student build up from a sequential per-crate +operation to a fork/join with a lock-protected shared tally. + +## Learning objectives + +- Use `parallel-map` for the high-level fork/join pattern. +- Use `` and `with-lock` to protect shared mutable state + across threads. +- Use `spawn`, ``, `fulfill`, and `?promise` to + hand-roll fork/join when the iteration shape doesn't fit + `parallel-map`. +- Recognise that *reads* of shared state need synchronisation + too, not just writes — a thread can yield mid-read. + +## Out of scope + +- Mailboxes, channels, semaphores — message-passing primitives + for problems with a different shape than this one. +- Multi-process / distributed parallelism via + `concurrency.distributed`. +- True multi-core parallelism (Factor's threads are + cooperative on a single OS thread). +- Atomic CAS via `alien.atomic`. + +## Concepts + +- `concurrency`: cooperative threads, promises, parallel + combinators, and locks. + +## Prerequisites + +- `higher-order-sequences` — taught in `boutique-bookkeeping`. + `parallel-map` has the same shape as `map`, and the + exemplar uses `each` and `map` for the dockhand fan-out. +- `tuples` — taught in `role-playing-game`. `TUPLE: crane` + with named slots and the `change-tonnage` accessor are the + obvious way to model a crane. +- `locals` — taught in `lasagna-luminary`. The hoist-crate + body, and the per-crate dockhand spawn, are much clearer + with `::` and `[| | … |]` than with bare stack shuffling. diff --git a/exercises/concept/quayside-crew/.meta/exemplar.factor b/exercises/concept/quayside-crew/.meta/exemplar.factor new file mode 100644 index 00000000..8cd6fa6a --- /dev/null +++ b/exercises/concept/quayside-crew/.meta/exemplar.factor @@ -0,0 +1,33 @@ +USING: accessors concurrency.combinators concurrency.locks +concurrency.promises kernel locals math math.statistics sequences +threads ; +IN: quayside-crew + +: weigh-crate ( crate -- weight ) + sum ; + +: weigh-all ( crates -- weights ) + [ weigh-crate ] parallel-map ; + +TUPLE: crane lock tonnage ; + +: ( -- crane ) + 0 crane boa ; + +:: hoist-crate ( weight crane -- ) + crane lock>> [ + crane [ weight + ] change-tonnage drop + ] with-lock ; + +:: crane-tonnage ( crane -- tonnage ) + crane lock>> [ crane tonnage>> ] with-lock ; + +:: load-cargo ( crates crane -- ) + crates [| crate | + :> p + [ + crate weigh-crate crane hoist-crate + t p fulfill + ] "dockhand" spawn drop + p + ] map [ ?promise drop ] each ; diff --git a/exercises/concept/quayside-crew/quayside-crew/quayside-crew-tests.factor b/exercises/concept/quayside-crew/quayside-crew/quayside-crew-tests.factor new file mode 100644 index 00000000..41a664ac --- /dev/null +++ b/exercises/concept/quayside-crew/quayside-crew/quayside-crew-tests.factor @@ -0,0 +1,56 @@ +USING: accessors arrays kernel math quayside-crew sequences tools.test ; +IN: quayside-crew.tests + +! TASK: 1 weigh-crate +{ 35 } [ { 12 8 15 } weigh-crate ] unit-test +{ 0 } [ { } weigh-crate ] unit-test +{ 7 } [ { 7 } weigh-crate ] unit-test + +! TASK: 2 weigh-all +{ { 10 10 12 } } [ { { 5 5 } { 10 } { 3 4 5 } } weigh-all ] unit-test +{ { } } [ { } weigh-all ] unit-test +{ { 35 } } [ { { 12 8 15 } } weigh-all ] unit-test + +! TASK: 3 +{ 0 } [ tonnage>> ] unit-test + +! a fresh crane each call — mutating one doesn't affect another +{ 5 0 } [ + dup 5 swap hoist-crate tonnage>> + tonnage>> +] unit-test + +! TASK: 4 hoist-crate +{ 35 } [ dup 35 swap hoist-crate tonnage>> ] unit-test +{ 52 } +[ + + dup 35 swap hoist-crate + dup 17 swap hoist-crate + tonnage>> +] unit-test + +! TASK: 5 crane-tonnage +{ 0 } [ crane-tonnage ] unit-test +{ 35 } [ dup 35 swap hoist-crate crane-tonnage ] unit-test + +! TASK: 6 load-cargo +{ 32 } [ + + { { 5 5 } { 10 } { 3 4 5 } } over load-cargo + crane-tonnage +] unit-test + +{ 0 } [ + + { } over load-cargo + crane-tonnage +] unit-test + +! many crates — verifies the lock prevents lost updates +{ 5050 } [ + + 100 [ 1 + 1array ] map + over load-cargo + crane-tonnage +] unit-test diff --git a/exercises/concept/quayside-crew/quayside-crew/quayside-crew.factor b/exercises/concept/quayside-crew/quayside-crew/quayside-crew.factor new file mode 100644 index 00000000..faf07cb5 --- /dev/null +++ b/exercises/concept/quayside-crew/quayside-crew/quayside-crew.factor @@ -0,0 +1,22 @@ +USING: kernel ; +IN: quayside-crew + +: weigh-crate ( crate -- weight ) + "unimplemented" throw ; + +: weigh-all ( crates -- weights ) + "unimplemented" throw ; + +TUPLE: crane ; + +: ( -- crane ) + "unimplemented" throw ; + +: hoist-crate ( weight crane -- ) + "unimplemented" throw ; + +: crane-tonnage ( crane -- tonnage ) + "unimplemented" throw ; + +: load-cargo ( crates crane -- ) + "unimplemented" throw ; diff --git a/exercises/practice/parallel-letter-frequency/.docs/instructions.md b/exercises/practice/parallel-letter-frequency/.docs/instructions.md new file mode 100644 index 00000000..6147b90a --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/.docs/instructions.md @@ -0,0 +1,7 @@ +# Instructions + +Count the frequency of letters in texts using parallel computation. + +Parallelism is about doing things in parallel that can also be done sequentially. +A common example is counting the frequency of letters. +Employ parallelism to calculate the total frequency of each letter in a list of texts. diff --git a/exercises/practice/parallel-letter-frequency/.meta/config.json b/exercises/practice/parallel-letter-frequency/.meta/config.json new file mode 100644 index 00000000..fa2e7bd7 --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "parallel-letter-frequency/parallel-letter-frequency.factor" + ], + "test": [ + "parallel-letter-frequency/parallel-letter-frequency-tests.factor" + ], + "example": [ + ".meta/example.factor" + ] + }, + "blurb": "Count the frequency of letters in texts using parallel computation." +} diff --git a/exercises/practice/parallel-letter-frequency/.meta/example.factor b/exercises/practice/parallel-letter-frequency/.meta/example.factor new file mode 100644 index 00000000..c8487c80 --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/.meta/example.factor @@ -0,0 +1,9 @@ +USING: assocs concurrency.combinators fry kernel sequences unicode ; +IN: parallel-letter-frequency + +: letters-of ( text -- letters ) + [ Letter? ] filter [ ch>lower ] map ; + +: calculate-frequencies ( texts -- counts ) + [ letters-of ] parallel-map concat + H{ } clone tuck '[ _ inc-at ] each ; diff --git a/exercises/practice/parallel-letter-frequency/.meta/generator.jl b/exercises/practice/parallel-letter-frequency/.meta/generator.jl new file mode 100644 index 00000000..fdfc7798 --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/.meta/generator.jl @@ -0,0 +1,28 @@ +module ParallelLetterFrequency + +function escape_factor(s) + replace(s, "\\" => "\\\\", "\"" => "\\\"") +end + +function gen_test_case(case) + texts = case["input"]["texts"] + expected = case["expected"] + + if isempty(texts) + texts_str = "{ }" + else + quoted = ["\"$(escape_factor(t))\"" for t in texts] + texts_str = "{\n " * join(quoted, "\n ") * "\n }" + end + + if isempty(expected) + expected_str = "H{ }" + else + pairs = ["{ CHAR: $(k) $(v) }" for (k, v) in expected] + expected_str = "H{\n " * join(pairs, "\n ") * "\n }" + end + + return """{ $(expected_str) }\n[\n $(texts_str) calculate-frequencies\n] unit-test""" +end + +end diff --git a/exercises/practice/parallel-letter-frequency/.meta/tests.toml b/exercises/practice/parallel-letter-frequency/.meta/tests.toml new file mode 100644 index 00000000..0c974f7f --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/.meta/tests.toml @@ -0,0 +1,49 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[c054d642-c1fa-4234-8007-9339f2337886] +description = "no texts" + +[818031be-49dc-4675-b2f9-c4047f638a2a] +description = "one text with one letter" + +[c0b81d1b-940d-4cea-9f49-8445c69c17ae] +description = "one text with multiple letters" + +[708ff1e0-f14a-43fd-adb5-e76750dcf108] +description = "two texts with one letter" + +[1b5c28bb-4619-4c9d-8db9-a4bb9c3bdca0] +description = "two texts with multiple letters" + +[6366e2b8-b84c-4334-a047-03a00a656d63] +description = "ignore letter casing" + +[92ebcbb0-9181-4421-a784-f6f5aa79f75b] +description = "ignore whitespace" + +[bc5f4203-00ce-4acc-a5fa-f7b865376fd9] +description = "ignore punctuation" + +[68032b8b-346b-4389-a380-e397618f6831] +description = "ignore numbers" + +[aa9f97ac-3961-4af1-88e7-6efed1bfddfd] +description = "Unicode letters" + +[7b1da046-701b-41fc-813e-dcfb5ee51813] +description = "combination of lower- and uppercase letters, punctuation and white space" + +[4727f020-df62-4dcf-99b2-a6e58319cb4f] +description = "large texts" + +[adf8e57b-8e54-4483-b6b8-8b32c115884c] +description = "many small texts" diff --git a/exercises/practice/parallel-letter-frequency/parallel-letter-frequency/parallel-letter-frequency-tests.factor b/exercises/practice/parallel-letter-frequency/parallel-letter-frequency/parallel-letter-frequency-tests.factor new file mode 100644 index 00000000..a3d2c06c --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/parallel-letter-frequency/parallel-letter-frequency-tests.factor @@ -0,0 +1,255 @@ +USING: io kernel lexer parallel-letter-frequency tools.test unicode ; +IN: parallel-letter-frequency.tests + +: STOP-HERE ( -- ) lexer get [ text>> length ] keep line<< ; parsing + +"Parallel Letter Frequency:" print + +"no texts" print +{ H{ } } +[ + { } calculate-frequencies +] unit-test + +STOP-HERE + +"one text with one letter" print +{ H{ + { CHAR: a 1 } + } } +[ + { + "a" + } calculate-frequencies +] unit-test + +"one text with multiple letters" print +{ H{ + { CHAR: c 3 } + { CHAR: b 2 } + { CHAR: d 1 } + } } +[ + { + "bbcccd" + } calculate-frequencies +] unit-test + +"two texts with one letter" print +{ H{ + { CHAR: f 1 } + { CHAR: e 1 } + } } +[ + { + "e" + "f" + } calculate-frequencies +] unit-test + +"two texts with multiple letters" print +{ H{ + { CHAR: g 2 } + { CHAR: h 3 } + { CHAR: i 1 } + } } +[ + { + "ggh" + "hhi" + } calculate-frequencies +] unit-test + +"ignore letter casing" print +{ H{ + { CHAR: m 2 } + } } +[ + { + "m" + "M" + } calculate-frequencies +] unit-test + +"ignore whitespace" print +{ H{ } } +[ + { + " " + "\t" + "\r\n" + } calculate-frequencies +] unit-test + +"ignore punctuation" print +{ H{ } } +[ + { + "!" + "?" + ";" + "," + "." + } calculate-frequencies +] unit-test + +"ignore numbers" print +{ H{ } } +[ + { + "1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + } calculate-frequencies +] unit-test + +"Unicode letters" print +{ H{ + { CHAR: ø 1 } + { CHAR: 本 1 } + { CHAR: φ 1 } + { CHAR: ほ 1 } + } } +[ + { + "本" + "φ" + "ほ" + "ø" + } calculate-frequencies +] unit-test + +"combination of lower- and uppercase letters, punctuation and white space" print +{ H{ + { CHAR: f 7 } + { CHAR: c 6 } + { CHAR: e 37 } + { CHAR: b 4 } + { CHAR: r 17 } + { CHAR: a 32 } + { CHAR: h 29 } + { CHAR: t 30 } + { CHAR: s 16 } + { CHAR: d 14 } + { CHAR: i 19 } + { CHAR: v 2 } + { CHAR: g 8 } + { CHAR: w 9 } + { CHAR: y 4 } + { CHAR: k 6 } + { CHAR: o 22 } + { CHAR: l 12 } + { CHAR: m 7 } + { CHAR: u 9 } + { CHAR: p 7 } + { CHAR: n 19 } + } } +[ + { + "There, peeping among the cloud-wrack above a dark tower high up in the mountains, Sam saw a white star twinkle for a while. The beauty of it smote his heart, as he looked up out of the forsaken land, and hope returned to him. For like a shaft, clear and cold, the thought pierced him that in the end, the shadow was only a small and passing thing: there was light and high beauty forever beyond its reach." + } calculate-frequencies +] unit-test + +"large texts" print +{ H{ + { CHAR: f 222 } + { CHAR: c 278 } + { CHAR: e 1143 } + { CHAR: b 155 } + { CHAR: x 7 } + { CHAR: r 432 } + { CHAR: a 845 } + { CHAR: h 507 } + { CHAR: t 1043 } + { CHAR: q 8 } + { CHAR: s 700 } + { CHAR: d 359 } + { CHAR: i 791 } + { CHAR: v 111 } + { CHAR: g 187 } + { CHAR: w 223 } + { CHAR: y 251 } + { CHAR: j 12 } + { CHAR: k 67 } + { CHAR: o 791 } + { CHAR: l 423 } + { CHAR: m 288 } + { CHAR: u 325 } + { CHAR: p 197 } + { CHAR: n 833 } + } } +[ + { + "I am a sick man.... I am a spiteful man. I am an unattractive man.\nI believe my liver is diseased. However, I know nothing at all about my disease, and do not\nknow for certain what ails me. I don't consult a doctor for it,\nand never have, though I have a respect for medicine and doctors.\nBesides, I am extremely superstitious, sufficiently so to respect medicine,\nanyway (I am well-educated enough not to be superstitious, but I am superstitious).\nNo, I refuse to consult a doctor from spite.\nThat you probably will not understand. Well, I understand it, though.\nOf course, I can't explain who it is precisely that I am mortifying in this case by my spite:\nI am perfectly well aware that I cannot \"pay out\" the doctors by not consulting them;\nI know better than anyone that by all this I am only injuring myself and no one else.\nBut still, if I don't consult a doctor it is from spite.\nMy liver is bad, well - let it get worse!\nI have been going on like that for a long time - twenty years. Now I am forty.\nI used to be in the government service, but am no longer.\nI was a spiteful official. I was rude and took pleasure in being so.\nI did not take bribes, you see, so I was bound to find a recompense in that, at least.\n(A poor jest, but I will not scratch it out. I wrote it thinking it would sound very witty;\nbut now that I have seen myself that I only wanted to show off in a despicable way -\nI will not scratch it out on purpose!) When petitioners used to come for\ninformation to the table at which I sat, I used to grind my teeth at them,\nand felt intense enjoyment when I succeeded in making anybody unhappy.\nI almost did succeed. For the most part they were all timid people - of course,\nthey were petitioners. But of the uppish ones there was one officer in particular\nI could not endure. He simply would not be humble, and clanked his sword in a disgusting way.\nI carried on a feud with him for eighteen months over that sword. At last I got the better of him.\nHe left off clanking it. That happened in my youth, though. But do you know,\ngentlemen, what was the chief point about my spite? Why, the whole point,\nthe real sting of it lay in the fact that continually, even in the moment of the acutest spleen,\nI was inwardly conscious with shame that I was not only not a spiteful but not even an embittered man,\nthat I was simply scaring sparrows at random and amusing myself by it.\nI might foam at the mouth, but bring me a doll to play with, give me a cup of tea with sugar in it,\nand maybe I should be appeased. I might even be genuinely touched,\nthough probably I should grind my teeth at myself afterwards and lie awake at night with shame for\nmonths after. That was my way. I was lying when I said just now that I was a spiteful official.\nI was lying from spite. I was simply amusing myself with the petitioners and with the officer,\nand in reality I never could become spiteful. I was conscious every moment in myself of many,\nvery many elements absolutely opposite to that. I felt them positively swarming in me,\nthese opposite elements. I knew that they had been swarming in me all my life and craving some outlet from me,\nbut I would not let them, would not let them, purposely would not let them come out.\nThey tormented me till I was ashamed: they drove me to convulsions and - sickened me, at last,\nhow they sickened me!" + "Gentlemen, I am joking, and I know myself that my jokes are not brilliant\n,but you know one can take everything as a joke. I am, perhaps, jesting against the grain.\nGentlemen, I am tormented by questions; answer them for me. You, for instance, want to cure men of their\nold habits and reform their will in accordance with science and good sense.\nBut how do you know, not only that it is possible, but also that it is\ndesirable to reform man in that way? And what leads you to the conclusion that man's\ninclinations need reforming? In short, how do you know that such a reformation will be a benefit to man?\nAnd to go to the root of the matter, why are you so positively convinced that not to act against\nhis real normal interests guaranteed by the conclusions of reason and arithmetic is certainly always\nadvantageous for man and must always be a law for mankind? So far, you know,\nthis is only your supposition. It may be the law of logic, but not the law of humanity.\nYou think, gentlemen, perhaps that I am mad? Allow me to defend myself. I agree that man\nis pre-eminently a creative animal, predestined to strive consciously for an object and to engage in engineering -\nthat is, incessantly and eternally to make new roads, wherever\nthey may lead. But the reason why he wants sometimes to go off at a tangent may just be that he is\npredestined to make the road, and perhaps, too, that however stupid the \"direct\"\npractical man may be, the thought sometimes will occur to him that the road almost always does lead\nsomewhere, and that the destination it leads to is less important than the process\nof making it, and that the chief thing is to save the well-conducted child from despising engineering,\nand so giving way to the fatal idleness, which, as we all know,\nis the mother of all the vices. Man likes to make roads and to create, that is a fact beyond dispute.\nBut why has he such a passionate love for destruction and chaos also?\nTell me that! But on that point I want to say a couple of words myself. May it not be that he loves\nchaos and destruction (there can be no disputing that he does sometimes love it)\nbecause he is instinctively afraid of attaining his object and completing the edifice he is constructing?\nWho knows, perhaps he only loves that edifice from a distance, and is by no means\nin love with it at close quarters; perhaps he only loves building it and does not want to live in it,\nbut will leave it, when completed, for the use of les animaux domestiques -\nsuch as the ants, the sheep, and so on. Now the ants have quite a different taste.\nThey have a marvellous edifice of that pattern which endures for ever - the ant-heap.\nWith the ant-heap the respectable race of ants began and with the ant-heap they will probably end,\nwhich does the greatest credit to their perseverance and good sense. But man is a frivolous and\nincongruous creature, and perhaps, like a chess player, loves the process of the game, not the end of it.\nAnd who knows (there is no saying with certainty), perhaps the only goal on earth\nto which mankind is striving lies in this incessant process of attaining, in other words,\nin life itself, and not in the thing to be attained, which must always be expressed as a formula,\nas positive as twice two makes four, and such positiveness is not life, gentlemen,\nbut is the beginning of death." + "But these are all golden dreams. Oh, tell me, who was it first announced,\nwho was it first proclaimed, that man only does nasty things because he does not know his own interests;\nand that if he were enlightened, if his eyes were opened to his real normal interests,\nman would at once cease to do nasty things, would at once become good and noble because,\nbeing enlightened and understanding his real advantage, he would see his own advantage in the\ngood and nothing else, and we all know that not one man can, consciously, act against his own interests,\nconsequently, so to say, through necessity, he would begin doing good? Oh, the babe! Oh, the pure,\ninnocent child! Why, in the first place, when in all these thousands of years has there been a time\nwhen man has acted only from his own interest? What is to be done with the millions of facts that bear\nwitness that men, consciously, that is fully understanding their real interests, have left them in the\nbackground and have rushed headlong on another path, to meet peril and danger,\ncompelled to this course by nobody and by nothing, but, as it were, simply disliking the beaten track,\nand have obstinately, wilfully, struck out another difficult, absurd way, seeking it almost in the darkness.\nSo, I suppose, this obstinacy and perversity were pleasanter to them than any advantage....\nAdvantage! What is advantage? And will you take it upon yourself to define with perfect accuracy in what the\nadvantage of man consists? And what if it so happens that a man's advantage, sometimes, not only may,\nbut even must, consist in his desiring in certain cases what is harmful to himself and not advantageous.\nAnd if so, if there can be such a case, the whole principle falls into dust. What do you think -\nare there such cases? You laugh; laugh away, gentlemen, but only answer me: have man's advantages been\nreckoned up with perfect certainty? Are there not some which not only have not been included but cannot\npossibly be included under any classification? You see, you gentlemen have, to the best of my knowledge,\ntaken your whole register of human advantages from the averages of statistical figures and\npolitico-economical formulas. Your advantages are prosperity, wealth, freedom, peace - and so on, and so on.\nSo that the man who should, for instance, go openly and knowingly in opposition to all that list would to your thinking,\nand indeed mine, too, of course, be an obscurantist or an absolute madman: would not he? But, you know, this is\nwhat is surprising: why does it so happen that all these statisticians, sages and lovers of humanity,\nwhen they reckon up human advantages invariably leave out one? They don't even take it into their reckoning\nin the form in which it should be taken, and the whole reckoning depends upon that. It would be no greater matter,\nthey would simply have to take it, this advantage, and add it to the list. But the trouble is, that this strange\nadvantage does not fall under any classification and is not in place in any list. I have a friend for instance ...\nEch! gentlemen, but of course he is your friend, too; and indeed there is no one, no one to whom he is not a friend!" + "Yes, but here I come to a stop! Gentlemen, you must excuse me for being over-philosophical;\nit's the result of forty years underground! Allow me to indulge my fancy. You see, gentlemen, reason is an excellent thing,\nthere's no disputing that, but reason is nothing but reason and satisfies only the rational side of man's nature,\nwhile will is a manifestation of the whole life, that is, of the whole human life including reason and all the impulses.\nAnd although our life, in this manifestation of it, is often worthless, yet it is life and not simply extracting square roots.\nHere I, for instance, quite naturally want to live, in order to satisfy all my capacities for life, and not simply my capacity\nfor reasoning, that is, not simply one twentieth of my capacity for life. What does reason know? Reason only knows what it has\nsucceeded in learning (some things, perhaps, it will never learn; this is a poor comfort, but why not say so frankly?)\nand human nature acts as a whole, with everything that is in it, consciously or unconsciously, and, even it if goes wrong, it lives.\nI suspect, gentlemen, that you are looking at me with compassion; you tell me again that an enlightened and developed man,\nsuch, in short, as the future man will be, cannot consciously desire anything disadvantageous to himself, that that can be proved mathematically.\nI thoroughly agree, it can - by mathematics. But I repeat for the hundredth time, there is one case, one only, when man may consciously, purposely,\ndesire what is injurious to himself, what is stupid, very stupid - simply in order to have the right to desire for himself even what is very stupid\nand not to be bound by an obligation to desire only what is sensible. Of course, this very stupid thing, this caprice of ours, may be in reality,\ngentlemen, more advantageous for us than anything else on earth, especially in certain cases. And in particular it may be more advantageous than\nany advantage even when it does us obvious harm, and contradicts the soundest conclusions of our reason concerning our advantage -\nfor in any circumstances it preserves for us what is most precious and most important - that is, our personality, our individuality.\nSome, you see, maintain that this really is the most precious thing for mankind; choice can, of course, if it chooses, be in agreement\nwith reason; and especially if this be not abused but kept within bounds. It is profitable and some- times even praiseworthy.\nBut very often, and even most often, choice is utterly and stubbornly opposed to reason ... and ... and ... do you know that that,\ntoo, is profitable, sometimes even praiseworthy? Gentlemen, let us suppose that man is not stupid. (Indeed one cannot refuse to suppose that,\nif only from the one consideration, that, if man is stupid, then who is wise?) But if he is not stupid, he is monstrously ungrateful!\nPhenomenally ungrateful. In fact, I believe that the best definition of man is the ungrateful biped. But that is not all, that is not his worst defect;\nhis worst defect is his perpetual moral obliquity, perpetual - from the days of the Flood to the Schleswig-Holstein period." + } calculate-frequencies +] unit-test + +"many small texts" print +{ H{ + { CHAR: c 150 } + { CHAR: b 100 } + { CHAR: a 50 } + } } +[ + { + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + "abbccc" + } calculate-frequencies +] unit-test diff --git a/exercises/practice/parallel-letter-frequency/parallel-letter-frequency/parallel-letter-frequency.factor b/exercises/practice/parallel-letter-frequency/parallel-letter-frequency/parallel-letter-frequency.factor new file mode 100644 index 00000000..0069c595 --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/parallel-letter-frequency/parallel-letter-frequency.factor @@ -0,0 +1,5 @@ +USING: kernel ; +IN: parallel-letter-frequency + +: calculate-frequencies ( texts -- counts ) + "unimplemented" throw ;