How can I select a random value from a list using F#

前端 未结 3 2061
独厮守ぢ
独厮守ぢ 2020-12-18 01:17

I\'m new to F# and I\'m trying to figure out how to return a random string value from a list/array of strings.

I have a list like this:



        
相关标签:
3条回答
  • 2020-12-18 01:31

    Your problem is that you are mixing Arrays and F# Lists (*type*[] is a type notation for Array). You could modify it like this to use lists:

    let getrandomitem () =  
      let rnd = System.Random()  
      fun (combos : string list) -> List.nth combos (rnd.Next(combos.Length))
    

    That being said, indexing into a List is usually a bad idea since it has O(n) performance since an F# list is basically a linked-list. You would be better off making combos into an array if possible like this:

    let combos = [|"win8FF40";"win10Chrome45";"win7IE11"|]  
    
    0 讨论(0)
  • 2020-12-18 01:34

    Both the answers given here by latkin and mydogisbox are good, but I still want to add a third approach that I sometimes use. This approach isn't faster, but it's more flexible and more composable, and fast enough for small sequences. Depending on your needs, you can use one of higher performance options given here, or you can use the following.

    Single-argument function using Random

    Instead of directly enabling you to select a single element, I often define a shuffleR function like this:

    open System
    
    let shuffleR (r : Random) xs = xs |> Seq.sortBy (fun _ -> r.Next())
    

    This function has the type System.Random -> seq<'a> -> seq<'a>, so it works with any sort of sequence: lists, arrays, collections, and lazily evaluated sequences (although not with infinite sequences).

    If you want a single random element from a list, you can still do that:

    > [1..100] |> shuffleR (Random ()) |> Seq.head;;
    val it : int = 85
    

    but you can also take, say, three randomly picked elements:

    > [1..100] |> shuffleR (Random ()) |> Seq.take 3;;
    val it : seq<int> = seq [95; 92; 12]
    

    No-argument function

    Sometimes, I don't care about having to pass in that Random value, so I instead define this alternative version:

    let shuffleG xs = xs |> Seq.sortBy (fun _ -> Guid.NewGuid())
    

    It works in the same way:

    > [1..100] |> shuffleG |> Seq.head;;
    val it : int = 11
    > [1..100] |> shuffleG |> Seq.take 3;;
    val it : seq<int> = seq [69; 61; 42]
    

    Although the purpose of Guid.NewGuid() isn't to provide random numbers, it's often random enough for my purposes - random, in the sense of being unpredictable.

    Generalised function

    Neither shuffleR nor shuffleG are truly random. Due to the ways Random and Guid.NewGuid() work, both functions may result in slightly skewed distributions. If this is a concern, you can define an even more general-purpose shuffle function:

    let shuffle next xs = xs |> Seq.sortBy (fun _ -> next())
    

    This function has the type (unit -> 'a) -> seq<'b> -> seq<'b> when 'a : comparison. It can still be used with Random:

    > let r = Random();;    
    val r : Random    
    > [1..100] |> shuffle (fun _ -> r.Next()) |> Seq.take 3;;
    val it : seq<int> = seq [68; 99; 54]
    > [1..100] |> shuffle (fun _ -> r.Next()) |> Seq.take 3;;
    val it : seq<int> = seq [99; 63; 11]
    

    but you can also use it with some of the cryptographically secure random number generators provided by the Base Class Library:

    open System.Security.Cryptography
    open System.Collections.Generic
    
    let rng = new RNGCryptoServiceProvider ()
    let bytes = Array.zeroCreate<byte> 100
    rng.GetBytes bytes
    
    let q = bytes |> Queue
    

    FSI:

    > [1..100] |> shuffle (fun _ -> q.Dequeue()) |> Seq.take 3;;
    val it : seq<int> = seq [74; 82; 61]
    

    Unfortunately, as you can see from this code, it's quite cumbersome and brittle. You have to know the length of the sequence up front; RNGCryptoServiceProvider implements IDisposable, so you should make sure to dispose of rng after use; and items will be removed from q after use, which means it's not reusable.

    Cryptographically random sort or selection

    Instead, if you really need a cryptographically correct sort or selection, it'd be easier to do it like this:

    let shuffleCrypto xs =
        let a = xs |> Seq.toArray
    
        use rng = new RNGCryptoServiceProvider ()
        let bytes = Array.zeroCreate a.Length
        rng.GetBytes bytes
    
        Array.zip bytes a |> Array.sortBy fst |> Array.map snd
    

    Usage:

    > [1..100] |> shuffleCrypto |> Array.head;;
    val it : int = 37
    > [1..100] |> shuffleCrypto |> Array.take 3;;
    val it : int [] = [|35; 67; 36|]
    

    This isn't something I've ever had to do, though, but I thought I'd include it here for the sake of completeness. While I haven't measured it, it's most likely not the fastest implementation, but it should be cryptographically random.

    0 讨论(0)
  • 2020-12-18 01:40

    I wrote a blog post on exactly this topic a while ago: http://latkin.org/blog/2013/11/16/selecting-a-random-element-from-a-linked-list-3-approaches-in-f/

    3 approaches are given there, with discussion of performance and tradeoffs of each.

    To summarize:

    // pro: simple, fast in practice
    // con: 2-pass (once to get length, once to select nth element)
    let method1 lst (rng : Random)  =
        List.nth lst (rng.Next(List.length lst))
    
    // pro: ~1 pass, list length is not bound by int32
    // con: more complex, slower in practice
    let method2 lst (rng : Random) =
        let rec step remaining picks top =
            match (remaining, picks) with
            | ([], []) -> failwith "Don't pass empty list"
            //  if only 1 element is picked, this is the result
            | ([], [p]) -> p
            // if multiple elements are picked, select randomly from them
            | ([], ps) -> step ps [] -1
            | (h :: t, ps) ->
                match rng.Next() with
                // if RNG makes new top number, picks list is reset
                | n when n > top -> step t [h] n
                // if RNG ties top number, add current element to picks list
                | n when n = top -> step t (h::ps) top
                // otherwise ignore and move to next element
                | _ -> step t ps top
        step lst [] -1
    
    // pro: exactly 1 pass
    // con: more complex, slowest in practice due to tuple allocations
    let method3 lst (rng : Random) =
        snd <| List.fold (fun (i, pick) elem ->
                   if rng.Next(i) = 0 then (i + 1, elem)
                   else (i + 1, pick)
               ) (0, List.head lst) lst
    

    Edit: I should clarify that above shows a few ways to get a random element from a list, assuming you must use a list. If it fits with the rest of your program's design, it is definitely more efficient to take a random element from an array.

    0 讨论(0)
提交回复
热议问题