Skip to content

Commit 54a1997

Browse files
Lanayxpsfinaki
andauthored
FS-1135 implementation - random functions for collections (#17277)
* 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>
1 parent e94b941 commit 54a1997

17 files changed

Lines changed: 2660 additions & 3 deletions

File tree

docs/release-notes/.FSharp.Core/8.0.400.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### Added
44

5+
* `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))
6+
57
### Changed
68

79
* Cache delegate in query extensions. ([PR #17130](https://github.com/dotnet/fsharp/pull/17130))

src/FSharp.Core/FSharp.Core.fsproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@
9191
<Compile Include="prim-types.fs">
9292
<Link>Primitives/prim-types.fs</Link>
9393
</Compile>
94+
<Compile Include="Random.fsi">
95+
<Link>Random/Random.fsi</Link>
96+
</Compile>
97+
<Compile Include="Random.fs">
98+
<Link>Random/Random.fs</Link>
99+
</Compile>
94100
<Compile Include="local.fsi">
95101
<Link>Collections/local.fsi</Link>
96102
</Compile>

src/FSharp.Core/Random.fs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+
namespace Microsoft.FSharp.Core
4+
5+
open System
6+
open System.Runtime.CompilerServices
7+
open System.Threading
8+
9+
[<AbstractClass; Sealed>]
10+
type internal ThreadSafeRandom() =
11+
12+
[<DefaultValue>]
13+
[<ThreadStatic>]
14+
static val mutable private random: Random
15+
16+
[<MethodImpl(MethodImplOptions.NoInlining)>]
17+
static member private Create() =
18+
ThreadSafeRandom.random <- Random()
19+
ThreadSafeRandom.random
20+
21+
// Don't pass the returned Random object between threads
22+
static member Shared =
23+
match ThreadSafeRandom.random with
24+
| null -> ThreadSafeRandom.Create()
25+
| random -> random

src/FSharp.Core/Random.fsi

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Microsoft.FSharp.Core
2+
3+
open System
4+
5+
[<AbstractClass; Sealed>]
6+
type internal ThreadSafeRandom =
7+
static member Shared: Random

src/FSharp.Core/array.fs

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1936,6 +1936,211 @@ module Array =
19361936

19371937
result
19381938

1939+
[<CompiledName("RandomShuffleWith")>]
1940+
let randomShuffleWith (random: Random) (source: 'T array) : 'T array =
1941+
checkNonNull "random" random
1942+
checkNonNull "source" source
1943+
1944+
let result = copy source
1945+
1946+
Microsoft.FSharp.Primitives.Basics.Random.shuffleArrayInPlaceWith random result
1947+
1948+
result
1949+
1950+
[<CompiledName("RandomShuffleBy")>]
1951+
let randomShuffleBy (randomizer: unit -> float) (source: 'T array) : 'T array =
1952+
checkNonNull "source" source
1953+
1954+
let result = copy source
1955+
1956+
Microsoft.FSharp.Primitives.Basics.Random.shuffleArrayInPlaceBy randomizer result
1957+
1958+
result
1959+
1960+
[<CompiledName("RandomShuffle")>]
1961+
let randomShuffle (source: 'T array) : 'T array =
1962+
randomShuffleWith ThreadSafeRandom.Shared source
1963+
1964+
[<CompiledName("RandomShuffleInPlaceWith")>]
1965+
let randomShuffleInPlaceWith (random: Random) (source: 'T array) =
1966+
checkNonNull "random" random
1967+
checkNonNull "source" source
1968+
1969+
Microsoft.FSharp.Primitives.Basics.Random.shuffleArrayInPlaceWith random source
1970+
1971+
[<CompiledName("RandomShuffleInPlaceBy")>]
1972+
let randomShuffleInPlaceBy (randomizer: unit -> float) (source: 'T array) =
1973+
checkNonNull "source" source
1974+
1975+
Microsoft.FSharp.Primitives.Basics.Random.shuffleArrayInPlaceBy randomizer source
1976+
1977+
[<CompiledName("RandomShuffleInPlace")>]
1978+
let randomShuffleInPlace (source: 'T array) =
1979+
randomShuffleInPlaceWith ThreadSafeRandom.Shared source
1980+
1981+
[<CompiledName("RandomChoiceWith")>]
1982+
let randomChoiceWith (random: Random) (source: 'T array) : 'T =
1983+
checkNonNull "random" random
1984+
checkNonNull "source" source
1985+
1986+
let inputLength = source.Length
1987+
1988+
if inputLength = 0 then
1989+
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString
1990+
1991+
let i = random.Next(0, inputLength)
1992+
source[i]
1993+
1994+
[<CompiledName("RandomChoiceBy")>]
1995+
let randomChoiceBy (randomizer: unit -> float) (source: 'T array) : 'T =
1996+
checkNonNull "source" source
1997+
1998+
let inputLength = source.Length
1999+
2000+
if inputLength = 0 then
2001+
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString
2002+
2003+
let i = Microsoft.FSharp.Primitives.Basics.Random.next randomizer 0 inputLength
2004+
source[i]
2005+
2006+
[<CompiledName("RandomChoice")>]
2007+
let randomChoice (source: 'T array) : 'T =
2008+
randomChoiceWith ThreadSafeRandom.Shared source
2009+
2010+
[<CompiledName("RandomChoicesWith")>]
2011+
let randomChoicesWith (random: Random) (count: int) (source: 'T array) : 'T array =
2012+
checkNonNull "random" random
2013+
checkNonNull "source" source
2014+
2015+
if count < 0 then
2016+
invalidArgInputMustBeNonNegative "count" count
2017+
2018+
let inputLength = source.Length
2019+
2020+
if inputLength = 0 then
2021+
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString
2022+
2023+
let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked count
2024+
2025+
for i = 0 to count - 1 do
2026+
let j = random.Next(0, inputLength)
2027+
result[i] <- source[j]
2028+
2029+
result
2030+
2031+
[<CompiledName("RandomChoicesBy")>]
2032+
let randomChoicesBy (randomizer: unit -> float) (count: int) (source: 'T array) : 'T array =
2033+
checkNonNull "source" source
2034+
2035+
if count < 0 then
2036+
invalidArgInputMustBeNonNegative "count" count
2037+
2038+
let inputLength = source.Length
2039+
2040+
if inputLength = 0 then
2041+
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString
2042+
2043+
let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked count
2044+
2045+
for i = 0 to count - 1 do
2046+
let j = Microsoft.FSharp.Primitives.Basics.Random.next randomizer 0 inputLength
2047+
result[i] <- source[j]
2048+
2049+
result
2050+
2051+
[<CompiledName("RandomChoices")>]
2052+
let randomChoices (count: int) (source: 'T array) : 'T array =
2053+
randomChoicesWith ThreadSafeRandom.Shared count source
2054+
2055+
[<CompiledName("RandomSampleWith")>]
2056+
let randomSampleWith (random: Random) (count: int) (source: 'T array) : 'T array =
2057+
checkNonNull "random" random
2058+
checkNonNull "source" source
2059+
2060+
if count < 0 then
2061+
invalidArgInputMustBeNonNegative "count" count
2062+
2063+
let inputLength = source.Length
2064+
2065+
if inputLength = 0 then
2066+
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString
2067+
2068+
if count > inputLength then
2069+
invalidArg "count" (SR.GetString(SR.notEnoughElements))
2070+
2071+
let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked count
2072+
2073+
let setSize =
2074+
Microsoft.FSharp.Primitives.Basics.Random.getMaxSetSizeForSampling count
2075+
2076+
if inputLength <= setSize then
2077+
let pool = copy source
2078+
2079+
for i = 0 to count - 1 do
2080+
let j = random.Next(0, inputLength - i)
2081+
result[i] <- pool[j]
2082+
pool[j] <- pool[inputLength - i - 1]
2083+
else
2084+
let selected = HashSet()
2085+
2086+
for i = 0 to count - 1 do
2087+
let mutable j = random.Next(0, inputLength)
2088+
2089+
while not (selected.Add j) do
2090+
j <- random.Next(0, inputLength)
2091+
2092+
result[i] <- source[j]
2093+
2094+
result
2095+
2096+
[<CompiledName("RandomSampleBy")>]
2097+
let randomSampleBy (randomizer: unit -> float) (count: int) (source: 'T array) : 'T array =
2098+
checkNonNull "source" source
2099+
2100+
if count < 0 then
2101+
invalidArgInputMustBeNonNegative "count" count
2102+
2103+
let inputLength = source.Length
2104+
2105+
if inputLength = 0 then
2106+
invalidArg "source" LanguagePrimitives.ErrorStrings.InputArrayEmptyString
2107+
2108+
if count > inputLength then
2109+
invalidArg "count" (SR.GetString(SR.notEnoughElements))
2110+
2111+
let result = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked count
2112+
2113+
// algorithm taken from https://github.com/python/cpython/blob/69b3e8ea569faabccd74036e3d0e5ec7c0c62a20/Lib/random.py#L363-L456
2114+
let setSize =
2115+
Microsoft.FSharp.Primitives.Basics.Random.getMaxSetSizeForSampling count
2116+
2117+
if inputLength <= setSize then
2118+
let pool = copy source
2119+
2120+
for i = 0 to count - 1 do
2121+
let j =
2122+
Microsoft.FSharp.Primitives.Basics.Random.next randomizer 0 (inputLength - i)
2123+
2124+
result[i] <- pool[j]
2125+
pool[j] <- pool[inputLength - i - 1]
2126+
else
2127+
let selected = HashSet()
2128+
2129+
for i = 0 to count - 1 do
2130+
let mutable j =
2131+
Microsoft.FSharp.Primitives.Basics.Random.next randomizer 0 inputLength
2132+
2133+
while not (selected.Add j) do
2134+
j <- Microsoft.FSharp.Primitives.Basics.Random.next randomizer 0 inputLength
2135+
2136+
result[i] <- source[j]
2137+
2138+
result
2139+
2140+
[<CompiledName("RandomSample")>]
2141+
let randomSample (count: int) (source: 'T array) : 'T array =
2142+
randomSampleWith ThreadSafeRandom.Shared count source
2143+
19392144
module Parallel =
19402145
open System.Threading
19412146
open System.Threading.Tasks

0 commit comments

Comments
 (0)