Skip to content

Commit

Permalink
FS-1135 implementation - random functions for collections (#17277)
Browse files Browse the repository at this point in the history
* Random functions: old version rebased

* Random functions: Rename functions according to the RFC

* Random functions: More naming refactoring and documentation

* Random functions: More documentation coments

* Random functions: Added randomShuffleInPlace functions and docs

* Random functions: refactoring

* Added null checks

* Added *by functions

* Random functions: Array random functions tests

* Added tests for lists and seqs

* Random functions: addded *By tests to arrays and sequences

* Random functions: addded *By tests to lists

* Random functions: try fix CI

* Random functions: review fixes

* Try fix CI

* Changed thread local implementation to thread static for performance per review

* PR review fix

* PR fix

* PR review fix

* Reverted HashSet constructor improvement since not netstandard2.0 compatible

* Fix formatting

* Fixed nan case for randomizer function

* PR review fixes

* PR review changes

* Fixed input length check logic for sample

---------

Co-authored-by: Petr <psfinaki@users.noreply.github.com>
  • Loading branch information
Lanayx and psfinaki authored Jun 26, 2024
1 parent e94b941 commit 54a1997
Show file tree
Hide file tree
Showing 17 changed files with 2,660 additions and 3 deletions.
2 changes: 2 additions & 0 deletions docs/release-notes/.FSharp.Core/8.0.400.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Added

* `Random functions for collections` ([RFC #1135](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1135-random-functions-for-collections.md), [PR #17277](https://github.com/dotnet/fsharp/pull/17277))

### Changed

* Cache delegate in query extensions. ([PR #17130](https://github.com/dotnet/fsharp/pull/17130))
Expand Down
6 changes: 6 additions & 0 deletions src/FSharp.Core/FSharp.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@
<Compile Include="prim-types.fs">
<Link>Primitives/prim-types.fs</Link>
</Compile>
<Compile Include="Random.fsi">
<Link>Random/Random.fsi</Link>
</Compile>
<Compile Include="Random.fs">
<Link>Random/Random.fs</Link>
</Compile>
<Compile Include="local.fsi">
<Link>Collections/local.fsi</Link>
</Compile>
Expand Down
25 changes: 25 additions & 0 deletions src/FSharp.Core/Random.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace Microsoft.FSharp.Core

open System
open System.Runtime.CompilerServices
open System.Threading

[<AbstractClass; Sealed>]
type internal ThreadSafeRandom() =

[<DefaultValue>]
[<ThreadStatic>]
static val mutable private random: Random

[<MethodImpl(MethodImplOptions.NoInlining)>]
static member private Create() =
ThreadSafeRandom.random <- Random()
ThreadSafeRandom.random

// Don't pass the returned Random object between threads
static member Shared =
match ThreadSafeRandom.random with
| null -> ThreadSafeRandom.Create()
| random -> random
7 changes: 7 additions & 0 deletions src/FSharp.Core/Random.fsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Microsoft.FSharp.Core

open System

[<AbstractClass; Sealed>]
type internal ThreadSafeRandom =
static member Shared: Random
205 changes: 205 additions & 0 deletions src/FSharp.Core/array.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1936,6 +1936,211 @@ module Array =

result

[<CompiledName("RandomShuffleWith")>]
let randomShuffleWith (random: Random) (source: 'T array) : 'T array =
checkNonNull "random" random
checkNonNull "source" source

let result = copy source

Microsoft.FSharp.Primitives.Basics.Random.shuffleArrayInPlaceWith random result

result

[<CompiledName("RandomShuffleBy")>]
let randomShuffleBy (randomizer: unit -> float) (source: 'T array) : 'T array =
checkNonNull "source" source

let result = copy source

Microsoft.FSharp.Primitives.Basics.Random.shuffleArrayInPlaceBy randomizer result

result

[<CompiledName("RandomShuffle")>]
let randomShuffle (source: 'T array) : 'T array =
randomShuffleWith ThreadSafeRandom.Shared source

[<CompiledName("RandomShuffleInPlaceWith")>]
let randomShuffleInPlaceWith (random: Random) (source: 'T array) =
checkNonNull "random" random
checkNonNull "source" source

Microsoft.FSharp.Primitives.Basics.Random.shuffleArrayInPlaceWith random source

[<CompiledName("RandomShuffleInPlaceBy")>]
let randomShuffleInPlaceBy (randomizer: unit -> float) (source: 'T array) =
checkNonNull "source" source

Microsoft.FSharp.Primitives.Basics.Random.shuffleArrayInPlaceBy randomizer source

[<CompiledName("RandomShuffleInPlace")>]
let randomShuffleInPlace (source: 'T array) =
randomShuffleInPlaceWith ThreadSafeRandom.Shared source

[<CompiledName("RandomChoiceWith")>]
let randomChoiceWith (random: Random) (source: 'T array) : 'T =
checkNonNull "random" random
checkNonNull "source" source

let inputLength = source.Length

if inputLength = 0 then
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString

let i = random.Next(0, inputLength)
source[i]

[<CompiledName("RandomChoiceBy")>]
let randomChoiceBy (randomizer: unit -> float) (source: 'T array) : 'T =
checkNonNull "source" source

let inputLength = source.Length

if inputLength = 0 then
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString

let i = Microsoft.FSharp.Primitives.Basics.Random.next randomizer 0 inputLength
source[i]

[<CompiledName("RandomChoice")>]
let randomChoice (source: 'T array) : 'T =
randomChoiceWith ThreadSafeRandom.Shared source

[<CompiledName("RandomChoicesWith")>]
let randomChoicesWith (random: Random) (count: int) (source: 'T array) : 'T array =
checkNonNull "random" random
checkNonNull "source" source

if count < 0 then
invalidArgInputMustBeNonNegative "count" count

let inputLength = source.Length

if inputLength = 0 then
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString

let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked count

for i = 0 to count - 1 do
let j = random.Next(0, inputLength)
result[i] <- source[j]

result

[<CompiledName("RandomChoicesBy")>]
let randomChoicesBy (randomizer: unit -> float) (count: int) (source: 'T array) : 'T array =
checkNonNull "source" source

if count < 0 then
invalidArgInputMustBeNonNegative "count" count

let inputLength = source.Length

if inputLength = 0 then
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString

let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked count

for i = 0 to count - 1 do
let j = Microsoft.FSharp.Primitives.Basics.Random.next randomizer 0 inputLength
result[i] <- source[j]

result

[<CompiledName("RandomChoices")>]
let randomChoices (count: int) (source: 'T array) : 'T array =
randomChoicesWith ThreadSafeRandom.Shared count source

[<CompiledName("RandomSampleWith")>]
let randomSampleWith (random: Random) (count: int) (source: 'T array) : 'T array =
checkNonNull "random" random
checkNonNull "source" source

if count < 0 then
invalidArgInputMustBeNonNegative "count" count

let inputLength = source.Length

if inputLength = 0 then
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString

if count > inputLength then
invalidArg "count" (SR.GetString(SR.notEnoughElements))

let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked count

let setSize =
Microsoft.FSharp.Primitives.Basics.Random.getMaxSetSizeForSampling count

if inputLength <= setSize then
let pool = copy source

for i = 0 to count - 1 do
let j = random.Next(0, inputLength - i)
result[i] <- pool[j]
pool[j] <- pool[inputLength - i - 1]
else
let selected = HashSet()

for i = 0 to count - 1 do
let mutable j = random.Next(0, inputLength)

while not (selected.Add j) do
j <- random.Next(0, inputLength)

result[i] <- source[j]

result

[<CompiledName("RandomSampleBy")>]
let randomSampleBy (randomizer: unit -> float) (count: int) (source: 'T array) : 'T array =
checkNonNull "source" source

if count < 0 then
invalidArgInputMustBeNonNegative "count" count

let inputLength = source.Length

if inputLength = 0 then
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString

if count > inputLength then
invalidArg "count" (SR.GetString(SR.notEnoughElements))

let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked count

// algorithm taken from https://github.com/python/cpython/blob/69b3e8ea569faabccd74036e3d0e5ec7c0c62a20/Lib/random.py#L363-L456
let setSize =
Microsoft.FSharp.Primitives.Basics.Random.getMaxSetSizeForSampling count

if inputLength <= setSize then
let pool = copy source

for i = 0 to count - 1 do
let j =
Microsoft.FSharp.Primitives.Basics.Random.next randomizer 0 (inputLength - i)

result[i] <- pool[j]
pool[j] <- pool[inputLength - i - 1]
else
let selected = HashSet()

for i = 0 to count - 1 do
let mutable j =
Microsoft.FSharp.Primitives.Basics.Random.next randomizer 0 inputLength

while not (selected.Add j) do
j <- Microsoft.FSharp.Primitives.Basics.Random.next randomizer 0 inputLength

result[i] <- source[j]

result

[<CompiledName("RandomSample")>]
let randomSample (count: int) (source: 'T array) : 'T array =
randomSampleWith ThreadSafeRandom.Shared count source

module Parallel =
open System.Threading
open System.Threading.Tasks
Expand Down
Loading

0 comments on commit 54a1997

Please sign in to comment.