docs(course): consistency pass
- m04: F# mulberry32 hints and skeleton route * and + through uint64 helpers; new 'F# in the browser' note on the Fable uint32 trap - m01-m05: 'About test names' notes like m06-m11; m00 explains JS vs Scala/F# titles - m00/m09: first build-web.sh run restores Fable; m09 JS file list (main.js is given) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1a80d57499
commit
82f47fb3ee
7 files changed
+26
-10
No files matched your search
@@ -82,13 +82,15 @@ The three tracks must produce **bit-identical** numbers, otherwise the shared fi
|
||||
| | JavaScript | Scala | F# |
|
||||
|---|---|---|---|
|
||||
| Seed type | `number` (a 64-bit float) | `Long` (holds 0 until 2^32) | `uint32` |
|
||||
| 32-bit multiply | `Math.imul(a, b)` | `Int` `*` wraps around | `uint32` `*` wraps around |
|
||||
| 32-bit multiply | `Math.imul(a, b)` | `Int` `*` wraps around | `uint32` `*` wraps on .NET, but not in the browser (see below) |
|
||||
| Unsigned shift | `>>>` | `>>>` on `Int` | `>>>` on `uint32` (unsigned type, so logical) |
|
||||
| XOR / OR | `^` / `\|` | `^` / `\|` | `^^^` / `\|\|\|` |
|
||||
| Back to unsigned | `x >>> 0` | `x.toLong & 0xFFFFFFFFL` | already unsigned |
|
||||
|
||||
The usual bugs: in JS, forgetting that `+` and `*` on numbers do not wrap at 2^32 (so you get huge or fractional values), or that `^` and `|` produce *signed* 32-bit results (so you get negative numbers). In Scala, doing the arithmetic in `Long` (which does not wrap at 32 bits) or forgetting to convert the final `Int` back to unsigned. The fixture `m04-random.json` checks ten consecutive draws from seeds `0`, `1` and `4294967295` (the largest seed), which catches every one of these mistakes.
|
||||
|
||||
> **F# in the browser.** The F# tests run on .NET, where `uint32` `+` and `*` wrap around at 2^32 exactly as the table says. The browser page, however, is compiled to JavaScript by Fable (Module 9), and Fable turns `uint32` `+` and `*` into plain JavaScript `+` and `*`. Those do not wrap, and a product of two large 32-bit numbers is not even exact in a JavaScript `number`. So a `nextUint32` written with plain `*` and `+` passes every test on .NET and then places the apples differently (or on cells that do not exist) in the browser. The tests cannot see this, because they never run the Fable output. The fix is to do each `*` and `+` through two tiny helpers that go up to `uint64` and back: `let mul (a: uint32) (b: uint32) = uint32 (uint64 a * uint64 b)`, and `add` the same way. The product of two `uint32` values always fits in a `uint64`, and converting back to `uint32` keeps the low 32 bits, so the helpers are exact on .NET and in the browser. (`^^^`, `|||` and `>>>` behave the same on both and need no helper.) The doc comment in `fsharp/src/Random.fs` says the same.
|
||||
|
||||
### Threading seeds by hand
|
||||
|
||||
Suppose you have `nextUint32 : seed → [value, seed]` and you want a die roll, then the sum of two dice. Written directly (JavaScript, as an illustration):
|
||||
@@ -310,6 +312,8 @@ And now the payoff: `newGame(42)` is the same game every time you call it. Any s
|
||||
|
||||
## Exercises
|
||||
|
||||
> **About test names.** The test titles quoted under "Test"/"Tests" are the JavaScript ones; the JS runner prints them with the module in front (`m04 intBelow always lands in [0, n)`). The Scala and F# suites test the same things one-to-one with the same titles, except that F# says `bind` where JS and Scala say `flatMap` (`bind threads the seed through both steps`). Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M04Random.test.scala` and `M04Spawn.test.scala`, or `fsharp/tests/M04Random.fs` and `M04Spawn.fs`. Fixture-based tests have the same case names in every track (JS `m04 nextUint32: seed-0-step-1`, Scala/F# `nextUint32: seed-0-step-1`).
|
||||
|
||||
### Exercise 4.1 — `nextUint32`
|
||||
|
||||
Implement mulberry32 as described above (the formula is also in the doc comment of your stub).
|
||||
@@ -329,7 +333,7 @@ let nextUint32 : Random<uint32> // Random.run 0u Random.nextUint3
|
||||
|
||||
Fixture: `fixtures/m04-random.json`, cases `seed-0-step-1` ... `seed-4294967295-step-10`. Each step starts from the previous step's new seed.
|
||||
|
||||
Watch out: see the 32-bit table above. Both the value **and** the new seed must be unsigned and below 2^32. In Scala, do the arithmetic in `Int` (where `*` wraps) and convert at the end.
|
||||
Watch out: see the 32-bit table above. Both the value **and** the new seed must be unsigned and below 2^32. In Scala, do the arithmetic in `Int` (where `*` wraps) and convert at the end. In F#, route every `*` and `+` through the `uint64` helpers (see "F# in the browser"), or the game will pass the tests and still misbehave in the browser.
|
||||
|
||||
### Exercise 4.2 — `pure`, `map`, `flatMap`, `run`
|
||||
|
||||
@@ -457,7 +461,7 @@ Translate the pseudo-code line by line, then fix the integer behaviour. Start wi
|
||||
|
||||
- **JS:** `(seed + 0x6D2B79F5) >>> 0` wraps the sum into an unsigned 32-bit number. `Math.imul(a, b)` multiplies like a 32-bit machine. `^`, `|`, `>>>` work on 32-bit integers; `^` and `|` give *signed* results, so finish the value with `>>> 0`. Use `const` for each intermediate (you cannot reassign `t`; call them `t1`, `t2`).
|
||||
- **Scala:** `val s = seed.toInt + 0x6D2B79F5` (Int addition wraps). Then the `Int` formula with `*`, `^`, `|`, `>>>`. Return `(value.toLong & 0xFFFFFFFFL, s.toLong & 0xFFFFFFFFL)`. Remember to wrap the function: `Random(seed => ...)`.
|
||||
- **F#:** `let s = seed + 0x6D2B79F5u` (uint32 addition wraps). Then `^^^`, `|||`, `>>>`, `*` on `uint32`; literals need the `u` suffix (`1u`, `61u`). No conversion needed at the end. Wrap the function: `Random(fun seed -> ...)`.
|
||||
- **F#:** first write the two helpers `mul` and `add` from "F# in the browser" above, and use them for every `*` and `+` (on .NET plain `uint32` arithmetic would wrap too, but the browser build would break). Then `let s = add seed 0x6D2B79F5u`, and `^^^`, `|||`, `>>>` on `uint32`; literals need the `u` suffix (`1u`, `61u`). No conversion needed at the end. Wrap the function: `Random(fun seed -> ...)`.
|
||||
|
||||
If seed 0 passes but seed 4294967295 fails, your addition is not wrapping.
|
||||
|
||||
@@ -483,11 +487,15 @@ lazy val nextUint32: Random[Long] = Random { seed =>
|
||||
}
|
||||
```
|
||||
```fsharp
|
||||
// wrap-around * and + that also work in the browser (Fable): via uint64 and back
|
||||
let private mul (a: uint32) (b: uint32) : uint32 = uint32 (uint64 a * uint64 b)
|
||||
let private add (a: uint32) (b: uint32) : uint32 = ___
|
||||
|
||||
let nextUint32 : Random<uint32> =
|
||||
Random(fun seed ->
|
||||
let s = seed + 0x6D2B79F5u
|
||||
let t1 = (s ^^^ (s >>> 15)) * ___
|
||||
let t2 = t1 ^^^ (t1 + ___ * ___)
|
||||
let s = add seed 0x6D2B79F5u
|
||||
let t1 = mul (s ^^^ (s >>> 15)) ___
|
||||
let t2 = t1 ^^^ add t1 (mul ___ ___)
|
||||
(t2 ^^^ (t2 >>> 14), ___))
|
||||
```
|
||||
|
||||
|
||||
Reference in new issue
Block a user