Skip to content

fix(js/ts/python/dart/rust/beam): use FSharp.Core's algorithms for random collection functions - #5197

Merged
MangelMaxime merged 5 commits into
fable-compiler:mainfrom
leonitousconforti:fix-seeded-random-parity
Oct 30, 2026
Merged

MangelMaxime merged 5 commits into
fable-compiler:mainfrom
leonitousconforti:fix-seeded-random-parity

Conversation

@leonitousconforti

Copy link
Copy Markdown
Contributor

The Array/List/Seq.random* functions now use FSharp.Core's algorithms, so on JS/TS a seeded System.Random gives the same results as on .NET. One commit per target. Related to #3966.

  • Shuffles use FSharp.Core's forward Fisher-Yates instead of a backwards one. On JS, Array.randomShuffleWith (Random 42) [|0..4|] is [|3; 1; 2; 4; 0|] like .NET (was [|2; 1; 4; 0; 3|]).
  • randomSample* uses FSharp.Core's pool and seen-index algorithm, including its extra draws on a repeated index: Array.randomSampleWith (Random 42) 3 [|0..4|] is [|3; 0; 4|] (was [|3; 1; 2|]).
  • Seq.randomChoices* draws when enumerated, like FSharp.Core.
  • The *By functions reject a randomizer result of NaN with FSharp.Core's message, and empty input and negative counts give FSharp.Core's messages.
  • Random.Shared is supported (it didn't compile), except on Rust.
  • JS/TS: seeded Random.Next(minValue, maxValue) over a range wider than Int32.MaxValue returned a value 1 too high about half the time.
  • JS/TS, Python, Dart and Beam: Random.Next(-1) and Next(5, 3) raise ArgumentOutOfRangeException with .NET's message.
  • Python: the Rust-backed Array.random*With functions ignored the Random they were given and used Python's global random.
  • Dart and Rust: List and Seq go through Array's functions, so all three give the same result for the same seed.

Notes:

  • Only JS/TS has a .NET-compatible seeded Random, so seeded values still differ from .NET on the other targets. The algorithms and draw counts match.
  • Seq.randomSample* stays eager on purpose: FSharp.Core's lazy version reuses its state across enumerations, so a second enumeration can repeat items or never finish.
  • Python is fixed in place, like fix(js/ts/python/dart/beam): add FSharp.Core's argument checks to collection functions #5154. I can switch to new names as AGENTS.md asks if you prefer. The Random.Next checks live in a new random_.py, because util.py can't import the exception types.
  • Not covered: Random.Shared and typed Random argument errors on Rust, Random.NextInt64/NextSingle, and types inheriting Random.

…unctions

The random collection functions and System.Random now give the same
results as on .NET for the same seed:

- Array, List and Seq.randomShuffle* and Array.randomShuffleInPlace* use
  FSharp.Core's forward Fisher-Yates shuffle. For example Random(42)
  shuffles [|0..4|] to [|3; 1; 2; 4; 0|] (was [|2; 1; 4; 0; 3|]), and a
  constant randomizer of 0.0 leaves the input in order.
- Array, List and Seq.randomSample* use FSharp.Core's algorithm: a pool for
  small inputs, otherwise drawing indices until an unseen one comes up, so
  the Random can be advanced more than count times, as on .NET.
  Seq.randomSample* stays eager.
- Seq.randomChoices* draws when enumerated, again on each enumeration, like
  FSharp.Core. Argument checks stay eager.
- The *By functions reject a randomizer result of NaN (they picked index
  0). Their errors, Array.randomChoice* on an empty array and negative
  counts give FSharp.Core's messages.
- Random.Shared is supported.
- Seeded Random.Next(minValue, maxValue) for ranges above Int32.MaxValue
  uses .NET's divisor, so values no longer come out 1 too high and
  maxValue is never returned.
- Random.Next gives .NET's message, ending in (Parameter 'maxValue') or
  (Parameter 'minValue'), for a negative maxValue or minValue > maxValue.
…ions

- Array.randomShuffle*With, randomChoice*With and randomSampleWith draw
  from the Random they are given instead of Python's global random, so
  the same seed gives the same result.
- Array, List and Seq.randomShuffle* and Array.randomShuffleInPlace* use
  FSharp.Core's forward Fisher-Yates shuffle. For example a constant
  randomizer of 0.0 leaves the input in order.
- Array, List and Seq.randomSample* use FSharp.Core's algorithm: a pool for
  small inputs, otherwise drawing indices until an unseen one comes up.
  Seq.randomSample* stays eager. A sample of 0 items from empty input is
  empty (it raised IndexError).
- Seq.randomChoices* draws when enumerated, again on each enumeration, like
  FSharp.Core. Argument checks stay eager.
- The *By functions reject a randomizer result of NaN (they picked index
  0). Array.randomChoice* on an empty array, List and Seq.randomChoice* on
  empty input and negative counts give FSharp.Core's messages.
- Random.Shared is supported.
- Random.Next(maxValue) and Next(minValue, maxValue) raise
  ArgumentOutOfRangeException with .NET's message for a negative maxValue
  or minValue > maxValue (was randrange's ValueError). They call new
  functions in random_.py, because util.py cannot import the exception
  types.
- Array, List and Seq.randomShuffle* and Array.randomShuffleInPlace* use
  FSharp.Core's forward Fisher-Yates shuffle. For example a constant
  randomizer of 0.0 leaves the input in order.
- Array, List and Seq.randomSample* use FSharp.Core's algorithm: a pool for
  small inputs, otherwise drawing indices until an unseen one comes up.
  Seq.randomSample* stays eager.
- The randomShuffle*With and randomSampleWith functions draw with
  Random.Next(i, n) like FSharp.Core. List and Seq go through Array's, so
  all three give the same result for the same seed.
- Seq.randomChoices* draws when enumerated, again on each enumeration, like
  FSharp.Core. Argument checks stay eager.
- The *By functions throw ArgumentOutOfRangeException for a randomizer
  result of NaN (was an UnsupportedError). Their errors,
  Array.randomChoice* on an empty array, List and Seq.randomChoice* on
  empty input and negative counts give FSharp.Core's messages.
- Random.Shared is supported.
- Random.Next raises ArgumentOutOfRangeException with .NET's message for a
  negative maxValue or minValue > maxValue (was a plain Exception).
- Array, List and Seq.randomShuffle* and Array.randomShuffleInPlace* use
  FSharp.Core's forward Fisher-Yates shuffle. For example a constant
  randomizer of 0.0 leaves the input in order.
- Array, List and Seq.randomSample* use FSharp.Core's algorithm: a pool for
  small inputs, otherwise drawing indices until an unseen one comes up.
  Seq.randomSample* stays eager.
- The randomShuffle*With and randomSampleWith functions draw with
  Random.Next(i, n) like FSharp.Core. List and Seq go through Array's, so
  all three give the same result for the same seed.
- Seq.randomChoices* draws when enumerated, again on each enumeration, like
  FSharp.Core. Argument checks stay eager.
- The *By functions reject a randomizer result of NaN (they picked index
  0). Their errors, Array.randomChoice* on an empty array, List and
  Seq.randomChoice* on empty input and negative counts give FSharp.Core's
  messages.
- Array, List and Seq.randomShuffle* and Array.randomShuffleInPlace* use
  FSharp.Core's forward Fisher-Yates shuffle. For example a constant
  randomizer of 0.0 leaves the input in order.
- Array, List and Seq.randomSample* use FSharp.Core's algorithm: a pool for
  small inputs, otherwise drawing indices until an unseen one comes up.
  Seq.randomSample* stays eager.
- Seq.randomChoices* draws when enumerated, again on each enumeration, like
  FSharp.Core. Argument checks stay eager.
- Negative counts give FSharp.Core's message.
- Random.Shared is supported, as one instance per process.
- Random.Next raises ArgumentOutOfRangeException with .NET's message for a
  negative maxValue or minValue > maxValue (was badarg), and Next(n, n)
  returns n.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants