diff --git a/.github/workflows/gh_pages.yaml b/.github/workflows/gh_pages.yaml index a4aa4d5..9026bd3 100644 --- a/.github/workflows/gh_pages.yaml +++ b/.github/workflows/gh_pages.yaml @@ -18,4 +18,4 @@ jobs: - uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs + publish_dir: ./generated-docs diff --git a/README.md b/README.md index c91a509..5d9bddd 100644 --- a/README.md +++ b/README.md @@ -11,25 +11,36 @@ Contributions & feedback are very welcome! :) ## Examples ```coffee -app "example_simple" - packages { pf: "./roc/examples/interactive/cli-platform" } - imports [ pf.Stdout.{ line }, pf.Task.{ await }, Random ] - provides [ main ] to pf +# Print a list of 10 random numbers in the range 25-75 inclusive. +main = + # Initialise "randomness" + initialSeed = Random.seed16 42 -main = - - state = Random.seed 42 # `state` stores the "randomness", initialized by the user/platform. - int = Random.int 0 100 # `int` generates values from 0-100 (inclusive) and updates state. - x = int state # x == { value: 9, state: { value: -60952905, ... } } - y = x |> Random.next int # y == { value: 61, state: { value: 1561666408, ... } } - - _ <- await (line (Num.toStr x.value |> \s -> "x: \(s)")) # This will print `x: 9`. - _ <- await (line (Num.toStr y.value |> \s -> "y: \(s)")) # This will print `x: 61`. - line "These values will be the same on every run, because we use a constant seed (42)." + # Create a generator for values from 25-75 (inclusive) + u16 = Random.u16 25 75 + + # Create a list of random numbers + result = + List.range { start: At 0, end: Before 10 } + |> List.walk { seed: initialSeed, numbers: [] } \state, _ -> + + random = u16 state.seed + seed = random.state + numbers = List.append state.numbers random.value + + { seed, numbers } + + # Format as a string + numbersListStr = + result.numbers + |> List.map Num.toStr + |> Str.joinWith "," + + Stdout.line "Random numbers are: \(numbersListStr)" ``` -See the `example_*.roc` files for more examples. +See the `examples/*.roc` files for more examples. ## Documentation diff --git a/docs/favicon.svg b/docs/favicon.svg deleted file mode 100644 index e0cff74..0000000 --- a/docs/favicon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 1ffe290..0000000 --- a/docs/index.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - -
- -
-
- -
- -
-
-
-

Random

Types

-

Generator uint value : (State uint) -> (Generation uint value)

A psuedorandom value generator

-

Generation uint value : - { - value : value, - state : (State uint) - }

A psuedorandom value, paired with its Generator's output state (for chaining)

-

State uint

Internal state for Generators

-

State constructors

-

seed : U32 -> (State U32)

Construct a "seed"

-

A "seed" is an initial State for Generators.

-

This is an alias for seed32.

-

seedVariant : U32, U32 -> (State U32)

Construct a specific "variant" of a "seed"

-

A "seed" is an initial State for Generators.

-

A "variant" is a State that specifies a c.updateIncrement constant, -to produce a sequence of internal values that shares no consecutive pairs -with other variants of the same State.

-

Odd numbers are recommended for the update increment, -to double the repetition period of sequences (by hitting odd values).

-

seed8Variant : U8, U8 -> (State U8)

Construct an initial State from 8 bits of noise and a specific increment for updating

-

seed16 : U16 -> (State U16)

Construct an initial State from 16 bits of noise

-

seed16Variant : U16, U16 -> (State U16)

Construct an initial State from 16 bits of noise and a specific increment for updating

-

seed32 : U32 -> (State U32)

Construct an initial State from 32 bits of noise

-

seed32Variant : U32, U32 -> (State U32)

Construct an initial State from 32 bits of noise and a specific increment for updating

-

Generator helpers

-

next : (Generation uint *), (Generator uint value) -> (Generation uint value)

Generate a new Generation from an old Generation's state

-

step : (State uint), (Generator uint value) -> (Generation uint value)

Generate a Generation from a state

-

Primitive Generator constructors

-

int : I32, I32 -> (Generator U32 I32)

Construct a Generator for 32-bit unsigned integers between two boundaries (inclusive)

-

This is an alias for i32.

-

i8 : I8, I8 -> (Generator U8 I8)

Construct a Generator for 8-bit signed integers between two boundaries (inclusive)

-

i16 : I16, I16 -> (Generator U16 I16)

Construct a Generator for 16-bit signed integers between two boundaries (inclusive)

-

i32 : I32, I32 -> (Generator U32 I32)

Construct a Generator for 32-bit signed integers between two boundaries (inclusive)

-

u8 : U8, U8 -> (Generator U8 U8)

Construct a Generator for 8-bit unsigned integers between two boundaries (inclusive)

-

u16 : U16, U16 -> (Generator U16 U16)

Construct a Generator for 16-bit unsigned integers between two boundaries (inclusive)

-

u32 : U32, U32 -> (Generator U32 U32)

Construct a Generator for 32-bit unsigned integers between two boundaries (inclusive)

- -
- - - - diff --git a/docs/search.js b/docs/search.js deleted file mode 100644 index fc0c041..0000000 --- a/docs/search.js +++ /dev/null @@ -1,44 +0,0 @@ -(() => { - let sidebar = document.getElementById("sidebar-nav"); - let searchBox = document.getElementById("module-search"); - - function search() { - let text = searchBox.value.toLowerCase(); // Search is case-insensitive. - - if (text === "") { - // Un-hide everything - sidebar.querySelectorAll(".sidebar-entry a").forEach((entry) => entry.classList.remove("hidden")); - - // Re-hide all the sub-entries except for those of the current module - let currentModuleName = document.querySelector('.module-name').textContent; - - sidebar.querySelectorAll(".sidebar-entry").forEach((entry) => { - let entryName = entry.querySelector('.sidebar-module-link').textContent; - if (currentModuleName === entryName) return; - entry.querySelectorAll(".sidebar-sub-entries a").forEach((subEntry) => subEntry.classList.add("hidden")); - }) - } else { - // First, show/hide all the sub-entries within each module (top-level functions etc.) - sidebar.querySelectorAll(".sidebar-sub-entries a").forEach((entry) => { - if (entry.textContent.toLowerCase().includes(text)) { - entry.classList.remove("hidden"); - } else { - entry.classList.add("hidden"); - } - }); - - // Then, show/hide modules based on whether they match, or any of their sub-entries matched - sidebar.querySelectorAll(".sidebar-module-link").forEach((entry) => { - if (entry.textContent.toLowerCase().includes(text) || entry.parentNode.querySelectorAll(".sidebar-sub-entries a:not(.hidden)").length > 0) { - entry.classList.remove("hidden"); - } else { - entry.classList.add("hidden"); - } - }); - } - } - - searchBox.addEventListener("input", search); - - search(); -})(); \ No newline at end of file diff --git a/docs/styles.css b/docs/styles.css deleted file mode 100644 index 1c65d6a..0000000 --- a/docs/styles.css +++ /dev/null @@ -1,521 +0,0 @@ -:root { - --link-color: #612bde; - --code-link-color: #5721d4; - --text-color: #333333; - --code-color: #222222; - --code-bg-color: #eeeeee; - --body-bg-color: #fdfdfd; - --border-color: #e9e9e9; - --faded-color: #4c4c4c; - --font-sans: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; - --font-mono: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; - --top-header-height: 67px; - --sidebar-width: 280px; - --top-bar-bg: #8257e5; - --top-bar-fg: #ffffff; - --nav-link-hover-color: #000000; -} - -a { - color: #972395; -} - -.logo { - padding: 2px 8px; -} - -.logo svg { - height: 48px; - width: 48px; - fill: var(--top-bar-fg); -} - -.logo:hover { - text-decoration: none; -} - -.logo svg:hover { - fill: var(--nav-link-hover-color); -} - -.pkg-full-name { - color: var(--text-color); - display: flex; - align-items: center; - font-size: 32px; - margin: 0 8px; - font-weight: normal; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - height: 100%; -} - -.entry-name { - white-space: pre-wrap; -} - -.pkg-full-name a { - padding-top: 12px; - padding-bottom: 16px; -} - -a { - text-decoration: none; -} - -a:hover, a:hover code { - text-decoration: underline; -} - -.pkg-and-logo { - min-width: 0; /* necessary for text-overflow: ellipsis to work in descendants */ - display: flex; - align-items: center; - height: 100%; - background-color: var(--top-bar-bg); -} - -.pkg-and-logo a, .pkg-and-logo a:visited { - color: var(--top-bar-fg); -} - -.pkg-and-logo a:hover { - color: var(--nav-link-hover-color); - text-decoration: none; -} - -.search-button { - flex-shrink: 0; /* always shrink the package name before these; they have a relatively constrained length */ - padding: 12px 18px; - margin-right: 42px; - display: none; /* only show this in the mobile view */ -} - -.version { - padding: 18px 10px; - min-width: 48px; - margin-right: 8px; -} - -body { - display: grid; - grid-template-columns: [before-sidebar] 1fr [sidebar] var(--sidebar-width) [main-content] fit-content(calc(1280px - var(--sidebar-width))) [end] 1fr; - grid-template-rows: [top-header] var(--top-header-height) [above-footer] auto [footer] auto; - box-sizing: border-box; - margin: 0; - padding: 0; - font-family: var(--font-sans); - color: var(--text-color); - background-color: var(--body-bg-color); -} - -main { - grid-column-start: main-content; - grid-column-end: main-content; - grid-row-start: above-footer; - grid-row-end: above-footer; - box-sizing: border-box; - position: relative; - font-size: 18px; - line-height: 1.85em; - margin-top: 2px; - padding: 48px; - - min-width: 0; /* necessary for text-overflow: ellipsis to work in descendants */ -} - -#sidebar-nav { - grid-column-start: sidebar; - grid-column-end: sidebar; - grid-row-start: above-footer; - grid-row-end: above-footer; - position: relative; - display: flex; - flex-direction: column; - box-sizing: border-box; - padding-left: 56px; - padding-top: 6px; - width: 100%; -} - -.top-header-extension { - grid-column-start: before-sidebar; - grid-column-end: sidebar; - grid-row-start: top-header; - grid-row-end: top-header; - background-color: var(--top-bar-bg); -} - -.top-header { - grid-column-start: sidebar; - grid-column-end: end; - grid-row-start: top-header; - grid-row-end: top-header; - display: flex; - flex-direction: row; - align-items: center; - flex-wrap: nowrap; - box-sizing: border-box; - font-family: var(--font-sans); - font-size: 24px; - height: 100%; - min-width: 0; /* necessary for text-overflow: ellipsis to work in descendants */ -} - -.top-header-triangle { - /* This used to be a clip-path, but Firefox on Android (at least as of early 2020) - * rendered the page extremely slowly in that version. With this approach it's super fast. - */ - width: 0; - height: 0; - border-style: solid; - border-width: var(--top-header-height) 0 0 48px; - border-color: transparent transparent transparent var(--top-bar-bg); -} - -p { - overflow-wrap: break-word; - margin: 24px 0; -} - -footer { - grid-column-start: main-content; - grid-column-end: main-content; - grid-row-start: footer; - grid-row-end: footer; - max-width: var(--main-content-max-width); - font-size: 14px; - box-sizing: border-box; - padding: 16px; -} - -footer p { - display: inline-block; - margin-top: 0; - margin-bottom: 8px; -} - -.content { - box-sizing: border-box; - display: flex; - flex-direction: row; - justify-content: space-between; -} - -.sidebar-entry ul { - list-style-type: none; - margin: 0; -} - -.sidebar-entry a { - box-sizing: border-box; - min-height: 48px; - min-width: 48px; - padding: 12px 16px; - font-family: var(--font-mono); -} - -.sidebar-sub-entries a { - display: block; - line-height: 24px; - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - padding-left: 36px; -} - -.module-name { - font-size: 56px; - line-height: 1em; - font-family: var(--font-mono); - font-weight: bold; - margin-top: 18px; - margin-bottom: 48px; -} - -.module-name a, .module-name a:visited { - color: inherit; -} - -.sidebar-module-link { - box-sizing: border-box; - font-size: 18px; - line-height: 24px; - font-family: var(--font-mono); - font-weight: bold; - display: block; - width: 100%; - padding: 8px 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -a, a:visited { - color: var(--link-color); -} - -h3 { - font-size: 32px; - margin: 48px 0 24px 0; -} - -h4 { - font-size: 24px; -} - -.type-def { - font-size: 24px; - color: var(--link-color); -} - -.code-snippet { - padding: 12px 16px; - display: block; - box-sizing: border-box; - font-family: var(--font-mono); - background-color: var(--code-bg-color); -} - -code { - font-family: var(--font-mono); - color: var(--code-color); - background-color: var(--code-bg-color); - display: inline-block; -} - -code a, a code { - text-decoration: none; - color: var(--code-link-color); - background: none; - padding: 0; -} - -code a:visited, a:visited code { - color: var(--code-link-color); -} - -pre { - margin: 36px 0; - padding: 8px; - box-sizing: border-box; - background-color: var(--code-bg-color); - overflow-x: auto; -} - -.hidden { - /* Use !important to win all specificity fights. */ - display: none !important; -} - -.syntax-number { - color: #60B7BF; -} -.syntax-string { - color:#F7577C; -} - -.syntax-bracket { - color:#FF335F; -} -.syntax-closure-dash, -.syntax-closure-arrow, -.syntax-operator -{ - color: #ffffff; -} -.syntax-comma { - color: #9573E6; -} -.syntax-comment { - color: #ff0000; -} - -#module-search:placeholder-shown { - padding: 0; - opacity: 0; - height: 0; -} - -#module-search, #module-search:focus { - opacity: 1; - padding: 12px 16px; - height: 48px; -} - -/* Show the "Search" label link when the text input has a placeholder */ -#module-search:placeholder-shown + #search-link { - display: flex; -} - -/* Hide the "Search" label link when the text input has focus */ -#module-search:focus + #search-link { - display: none; -} - -#module-search { - display: block; - box-sizing: border-box; - background-color: var(--code-bg-color); - width: 100%; - box-sizing: border-box; - font-size: 18px; - line-height: 18px; - margin-top: 6px; - border: none; - color: var(--faded-color); - background-color: var(--code-bg-color); - font-family: var(--font-serif); -} - -#module-search::placeholder { - color: var(--faded-color); - opacity: 1; -} - -#search-link { - box-sizing: border-box; - display: none; - align-items: center; - font-size: 18px; - line-height: 18px; - padding: 12px 16px; - height: 48px; - cursor: pointer; - color: var(--link-color); -} - -#search-link:hover { - text-decoration: underline; -} - -@media (prefers-color-scheme: dark) { - :root { - --body-bg-color: #303030; - --code-bg-color: #393939; - --border-color: #555555; - --code-color: #eeeeee; - --text-color: #cccccc; - --logo-solid: #777777; - --faded-color: #bbbbbb; - --link-color: #c5a8ff; - --code-link-color: #b894ff; - --top-bar-bg: #6845b9; - --top-bar-fg: #eeeeee; - } - - html { - scrollbar-color: #444444 #2f2f2f; - } -} - -@media only screen and (max-device-width: 480px) and (orientation: portrait) { - .search-button { - display: block; /* This is only visible in mobile. */ - } - - .top-header { - justify-content: space-between; - width: auto; - } - - .pkg-full-name { - margin-left: 8px; - margin-right: 12px; - font-size: 24px; - padding-bottom: 14px; - } - - .pkg-full-name a { - vertical-align: middle; - padding: 18px 0; - } - - .logo { - padding-left: 2px; - width: 50px; - height: 54px; - } - - .version { - margin: 0; - font-weight: normal; - font-size: 18px; - padding-bottom: 16px; - } - - .module-name { - font-size: 36px; - margin-top: 8px; - margin-bottom: 8px; - max-width: calc(100% - 18px); - overflow: hidden; - text-overflow: ellipsis; - } - - main { - grid-column-start: none; - grid-column-end: none; - grid-row-start: above-footer; - grid-row-end: above-footer; - padding: 18px; - font-size: 16px; - } - - #sidebar-nav { - grid-column-start: none; - grid-column-end: none; - grid-row-start: sidebar; - grid-row-end: sidebar; - margin-top: 0; - padding-left: 0; - width: auto; - } - - #sidebar-heading { - font-size: 24px; - margin: 16px; - } - - h3 { - font-size: 18px; - margin: 0; - padding: 0; - } - - h4 { - font-size: 16px; - } - - body { - grid-template-columns: auto; - grid-template-rows: [top-header] var(--top-header-height) [before-sidebar] auto [sidebar] auto [above-footer] auto [footer] auto; -/* [before-sidebar] 1fr [sidebar] var(--sidebar-width) [main-content] fit-content(calc(1280px - var(--sidebar-width))) [end] 1fr; */ - - margin: 0; - min-width: 320px; - max-width: 100%; - } - - .top-header-triangle { - display: none; - } - - .pkg-and-logo { - width: 100%; - } - - .pkg-full-name { - flex-grow: 1; - } - - .pkg-full-name a { - padding-top: 24px; - padding-bottom: 12px; - } -} - -h1 { - margin-top: 60px; -} diff --git a/example_simple.roc b/example_simple.roc deleted file mode 100644 index e34e3ab..0000000 --- a/example_simple.roc +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env roc - -app "example_simple" - packages { pf: "./roc/examples/interactive/cli-platform/main.roc" } - imports [ pf.Stdout.{ line }, pf.Task.{ await }, Random ] - provides [ main ] to pf - - -main = - - state = Random.seed 42 # `state` stores the "randomness", initialized by the user/platform. - int = Random.int 0 100 # `int` generates values from 0-100 (inclusive) and updates state. - x = int state # x == { value: 9, state: { value: -60952905, ... } } - y = x |> Random.next int # y == { value: 61, state: { value: 1561666408, ... } } - - _ <- await (line (Num.toStr x.value |> \s -> "x: \(s)")) # This will print `x: 74`. - _ <- await (line (Num.toStr y.value |> \s -> "y: \(s)")) # This will print `x: 96`. - line "These values will be the same on every run, because we use a constant seed (42)." diff --git a/example_complex.roc b/examples/complex.roc similarity index 80% rename from example_complex.roc rename to examples/complex.roc index 1869c68..35ec5b7 100644 --- a/example_complex.roc +++ b/examples/complex.roc @@ -1,8 +1,11 @@ -#!/usr/bin/env roc - +# !/usr/bin/env roc app "example_complex" - packages { pf: "roc/examples/interactive/cli-platform/main.roc" } - imports [pf.Stdout.{ line }, pf.Task.{ await }, Random] + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.3.1/97mY3sUwo433-pcnEQUlMhn-sWiIf_J9bPhcAFZoqY4.tar.br" } + imports [ + pf.Stdout.{ line }, + pf.Task.{ await }, + Random, + ] provides [main] to pf main = @@ -17,11 +20,14 @@ main = _ <- await (line (Num.toStr b.value.y |> \s -> "b.y == 78 == \(s)")) _ <- await (line (Num.toStr b.value.z |> \s -> "b.z == -64 == \(s)")) _ <- await (line (Num.toStr b.value.t |> \s -> "b.t == -20 == \(s)")) + line "These values will be the same on every run, because we use a constant seed." # Complex `Generator`s can be created by chaining primitive `Generator`s. -Point a : { x : a, y : a, z : a, t : a } -point : Random.Generator (Point I32) (Random.State U32) +# Point a : { x : a, y : a, z : a, t : a } + +# TODO fix this type annotation +# point : Random.Generator (Point I32) (Random.State U32) point = \state -> min = -100 max = 100 diff --git a/example_digits.roc b/examples/digits.roc similarity index 90% rename from example_digits.roc rename to examples/digits.roc index 2f02e16..71a2051 100644 --- a/example_digits.roc +++ b/examples/digits.roc @@ -1,10 +1,8 @@ -#!/usr/bin/env roc - +# !/usr/bin/env roc app "example_digits" - packages { pf: "./roc/examples/interactive/cli-platform" } - imports [ pf.Stdout.{ line }, pf.Task.{ await }, Random ] - provides [ main ] to pf - + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.3.1/97mY3sUwo433-pcnEQUlMhn-sWiIf_J9bPhcAFZoqY4.tar.br" } + imports [pf.Stdout.{ line }, pf.Task.{ await }, Random] + provides [main] to pf main = diff --git a/examples/simple.roc b/examples/simple.roc new file mode 100644 index 0000000..109d428 --- /dev/null +++ b/examples/simple.roc @@ -0,0 +1,35 @@ +# !/usr/bin/env roc +app "example_simple" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.3.1/97mY3sUwo433-pcnEQUlMhn-sWiIf_J9bPhcAFZoqY4.tar.br" } + imports [ + pf.Stdout, + Random, + ] + provides [main] to pf + +main = + + # Initialise "randomness" + initialSeed = Random.seed16 42 + + # Create a generator for values from 0-100 (inclusive) + u16 = Random.u16 0 100 + + # Create a list of random numbers + result = + List.range { start: At 1, end: At 1000 } + |> List.walk { seed: initialSeed, numbers: [] } \state, _ -> + + random = u16 state.seed + seed = random.state + numbers = List.append state.numbers random.value + + { seed, numbers } + + # Format as a string + numbersListStr = + result.numbers + |> List.map Num.toStr + |> Str.joinWith "," + + Stdout.line "Random numbers are: \(numbersListStr)" diff --git a/Random.roc b/package/Random.roc similarity index 66% rename from Random.roc rename to package/Random.roc index 6bcf10b..1136048 100644 --- a/Random.roc +++ b/package/Random.roc @@ -1,3 +1,13 @@ +## ## PCG algorithms, constants, and wrappers +## +## Based on this paper [PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms for Random Number Generation](https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf) +## and this C++ header: [pcg_variants.h](https://github.com/imneme/pcg-c/blob/master/include/pcg_variants.h) +## +## ## Abbreviations: +## - M = Multiplication (see section 6.3.4 on page 45 in the paper) +## - PCG = Permuted Congruential Generator +## - RXS = Random XorShift (see section 5.5.1 on page 36 in the paper) +## - XS = XorShift (see section 5.5 on page 34 in the paper) interface Random exposes [ Generation, @@ -23,9 +33,6 @@ interface Random ] imports [] - -## # Types - ## A psuedorandom value generator Generator uint value : State uint -> Generation uint value @@ -34,16 +41,15 @@ Generation uint value : { value : value, state : State uint } ## Internal state for [Generator]s State uint := { s : uint, c : AlgorithmConstants uint } + AlgorithmConstants uint : { permuteMultiplier : uint, permuteRandomXorShift : uint, permuteRandomXorShiftIncrement : uint, permuteXorShift : uint, updateIncrement : uint, - updateMultiplier : uint } - - -## # [State] constructors + updateMultiplier : uint, +} ## Construct a "seed" ## @@ -75,15 +81,16 @@ seed8 = \s -> seed8Variant s defaultU8UpdateIncrement ## Construct an initial [State] from 8 bits of noise and a specific increment for updating seed8Variant : U8, U8 -> State U8 seed8Variant = \s, uI -> - @State { - s, - c: { - permuteMultiplier: defaultU8PermuteMultiplier, - permuteRandomXorShift: defaultU8PermuteRandomXorShift, - permuteRandomXorShiftIncrement: defaultU8PermuteRandomXorShiftIncrement, - permuteXorShift: defaultU8PermuteXorShift, - updateIncrement: uI, - updateMultiplier: defaultU8UpdateMultiplier } } + c = { + permuteMultiplier: defaultU8PermuteMultiplier, + permuteRandomXorShift: defaultU8PermuteRandomXorShift, + permuteRandomXorShiftIncrement: defaultU8PermuteRandomXorShiftIncrement, + permuteXorShift: defaultU8PermuteXorShift, + updateIncrement: uI, + updateMultiplier: defaultU8UpdateMultiplier, + } + + @State { s, c } ## Construct an initial [State] from 16 bits of noise seed16 : U16 -> State U16 @@ -92,15 +99,16 @@ seed16 = \s -> seed16Variant s defaultU16UpdateIncrement ## Construct an initial [State] from 16 bits of noise and a specific increment for updating seed16Variant : U16, U16 -> State U16 seed16Variant = \s, uI -> - @State { - s, - c: { - permuteMultiplier: defaultU16PermuteMultiplier, - permuteRandomXorShift: defaultU16PermuteRandomXorShift, - permuteRandomXorShiftIncrement: defaultU16PermuteRandomXorShiftIncrement, - permuteXorShift: defaultU16PermuteXorShift, - updateIncrement: uI, - updateMultiplier: defaultU16UpdateMultiplier } } + c = { + permuteMultiplier: defaultU16PermuteMultiplier, + permuteRandomXorShift: defaultU16PermuteRandomXorShift, + permuteRandomXorShiftIncrement: defaultU16PermuteRandomXorShiftIncrement, + permuteXorShift: defaultU16PermuteXorShift, + updateIncrement: uI, + updateMultiplier: defaultU16UpdateMultiplier, + } + + @State { s, c } ## Construct an initial [State] from 32 bits of noise seed32 : U32 -> State U32 @@ -109,18 +117,16 @@ seed32 = \s -> seed32Variant s defaultU32UpdateIncrement ## Construct an initial [State] from 32 bits of noise and a specific increment for updating seed32Variant : U32, U32 -> State U32 seed32Variant = \s, uI -> - @State { - s, - c: { - permuteMultiplier: defaultU32PermuteMultiplier, - permuteRandomXorShift: defaultU32PermuteRandomXorShift, - permuteRandomXorShiftIncrement: defaultU32PermuteRandomXorShiftIncrement, - permuteXorShift: defaultU32PermuteXorShift, - updateIncrement: uI, - updateMultiplier: defaultU32UpdateMultiplier } } + c = { + permuteMultiplier: defaultU32PermuteMultiplier, + permuteRandomXorShift: defaultU32PermuteRandomXorShift, + permuteRandomXorShiftIncrement: defaultU32PermuteRandomXorShiftIncrement, + permuteXorShift: defaultU32PermuteXorShift, + updateIncrement: uI, + updateMultiplier: defaultU32UpdateMultiplier, + } - -## # [Generator] helpers + @State { s, c } ## Generate a new [Generation] from an old [Generation]'s state next : Generation uint *, Generator uint value -> Generation uint value @@ -130,9 +136,6 @@ next = \x, g -> g x.state step : State uint, Generator uint value -> Generation uint value step = \s, g -> g s - -## # Primitive [Generator] constructors - ## Construct a [Generator] for 32-bit unsigned integers between two boundaries (inclusive) ## ## This is an alias for [i32]. @@ -142,7 +145,7 @@ int = i32 ## Construct a [Generator] for 8-bit signed integers between two boundaries (inclusive) i8 : I8, I8 -> Generator U8 I8 i8 = \x, y -> - Pair minimum maximum = sort x y + (minimum, maximum) = sort x y # TODO: Remove these `I64` dependencies. range = maximum - minimum + 1 |> Num.toI64 \state -> @@ -154,7 +157,7 @@ i8 = \x, y -> ## Construct a [Generator] for 16-bit signed integers between two boundaries (inclusive) i16 : I16, I16 -> Generator U16 I16 i16 = \x, y -> - Pair minimum maximum = sort x y + (minimum, maximum) = sort x y # TODO: Remove these `I64` dependencies. range = maximum - minimum + 1 |> Num.toI64 \state -> @@ -166,7 +169,7 @@ i16 = \x, y -> ## Construct a [Generator] for 32-bit signed integers between two boundaries (inclusive) i32 : I32, I32 -> Generator U32 I32 i32 = \x, y -> - Pair minimum maximum = sort x y + (minimum, maximum) = sort x y # TODO: Remove these `I64` dependencies. range = maximum - minimum + 1 |> Num.toI64 \state -> @@ -187,16 +190,19 @@ u16 = \x, y -> betweenUnsigned x y u32 : U32, U32 -> Generator U32 U32 u32 = \x, y -> betweenUnsigned x y - -#### Helpers for the above constructors +# Helpers for the above constructors ------------------------------------------- betweenUnsigned = \x, y -> - Pair minimum maximum = sort x y + (minimum, maximum) = sort x y range = maximum - minimum + 1 + \s -> # TODO: Analyze this. The mod-ing might be biased towards a smaller offset! + value = minimum + (permute s) % range - { value, state: update s } + state = update s + + { value, state } mapToI8 : U8 -> I8 mapToI8 = \x -> @@ -224,20 +230,9 @@ mapToI32 = \x -> sort = \x, y -> if x < y then - Pair x y + (x, y) else - Pair y x - - -#### PCG algorithms, constants, and wrappers -# -# Based on this paper: https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf -# Based on this C++ header: https://github.com/imneme/pcg-c/blob/master/include/pcg_variants.h -# Abbreviations: -# M = Multiplication (see section 6.3.4 on page 45 in the paper) -# PCG = Permuted Congruential Generator -# RXS = Random XorShift (see section 5.5.1 on page 36 in the paper) -# XS = XorShift (see section 5.5 on page 34 in the paper) + (y, x) # See `RXS M XS` constants (line 168?) # and `_DEFAULT_` constants (line 276?) @@ -276,23 +271,93 @@ defaultU32UpdateMultiplier = 747_796_405 # defaultU128UpdateMultiplier = (Num.shiftLeftBy 64 2_549_297_995_355_413_924) + 4_865_540_595_714_422_341 # See `pcg_output_rxs_m_xs_8_8` (on line 170?) in the PCG C++ header (see link above). +permute : State (Int uint) -> Int uint permute = \@State { s, c } -> pcgRxsMXs s c.permuteRandomXorShift c.permuteRandomXorShiftIncrement c.permuteMultiplier c.permuteXorShift # See section 6.3.4 on page 45 in the PCG paper (see link above). +pcgRxsMXs : Int uint, Int uint, Int uint, Int uint, Int uint -> Int uint pcgRxsMXs = \state, randomXorShift, randomXorShiftIncrement, multiplier, xorShift -> - partial = Num.mulWrap multiplier ( - Num.bitwiseXor state ( - Num.shiftRightZfBy ( - Num.addWrap (Num.shiftRightZfBy randomXorShift state) randomXorShiftIncrement - ) state - )) - Num.bitwiseXor partial (Num.shiftRightZfBy xorShift partial) + + inner = + randomXorShift + |> Num.shiftRightZfBy (Num.intCast state) + |> Num.addWrap randomXorShiftIncrement + |> Num.shiftRightZfBy (Num.intCast state) + + partial = + state + |> Num.bitwiseXor inner + |> Num.mulWrap multiplier + + Num.bitwiseXor partial (Num.shiftRightZfBy xorShift (Num.intCast partial)) # See section 4.1 on page 20 in the PCG paper (see link above). +pcgStep : Int uint, Int uint, Int uint -> Int uint pcgStep = \state, multiplier, increment -> - Num.addWrap (Num.mulWrap state multiplier) increment + state + |> Num.mulWrap multiplier + |> Num.addWrap increment # See `pcg_oneseq_8_step_r` (line 409?) in the PCG C++ header (see link above). +update : State (Int uint) -> State (Int uint) update = \@State { s, c } -> - @State { s: pcgStep s c.updateMultiplier c.updateIncrement, c } + + sNew : Int uint + sNew = pcgStep s c.updateMultiplier c.updateIncrement + + @State { s: sNew, c } + +# Test U8 generation +# TODO confirm this is the right value for this seed +expect + testGenerator = u8 0 250 + testSeed = seed8 123 + actual = testGenerator testSeed + expected = 67u8 + actual.value == expected + +# Test U16 generation +# TODO confirm this is the right value for this seed +expect + testGenerator = u16 0 250 + testSeed = seed16 123 + actual = testGenerator testSeed + expected = 183u16 + actual.value == expected + +# Test U32 generation +# TODO confirm this is the right value for this seed +expect + testGenerator = u32 0 250 + testSeed = seed32 123 + actual = testGenerator testSeed + expected = 143u32 + actual.value == expected + +# Test I8 generation +# TODO confirm this is the right value for this seed +expect + testGenerator = i8 0 9 + testSeed = seed8 6 + actual = testGenerator testSeed + expected = 2i8 + actual.value == expected + +# Test I16 generation +# TODO confirm this is the right value for this seed +expect + testGenerator = i16 0 9 + testSeed = seed16 6 + actual = testGenerator testSeed + expected = 4i16 + actual.value == expected + +# Test I32 generation +# TODO confirm this is the right value for this seed +expect + testGenerator = i32 0 9 + testSeed = seed32 6 + actual = testGenerator testSeed + expected = 2i32 + actual.value == expected diff --git a/package/main.roc b/package/main.roc new file mode 100644 index 0000000..0614d18 --- /dev/null +++ b/package/main.roc @@ -0,0 +1,5 @@ +package "random" + exposes [ + Random, + ] + packages {}