patternMinor
Function to produce unique random numbers
Viewed 0 times
randomuniqueproducenumbersfunction
Problem
I've been learning Haskell for a few weeks after coming from a C# background. I've written a function to return a number of unique
Then to produce 5 numbers in the (inclusive) range 1 -> 10 the call would be something like:
Firstly I have a few specific questions:
On top of those if there is any advice on style, better ways to implement the above or just general advice would be greatly appreciated.
Ints from a specified range:module MrKWatkins.Random (
uniqueRandomInts
) where
import System.Random
import qualified Data.IntSet as I
uniqueRandomInts :: RandomGen g => (Int, Int) -> Int -> g -> ([Int], g)
uniqueRandomInts range@(min, max) n rng
| n max - min + 1 = error "n is too large for range"
| otherwise = recursion n I.empty ([], rng)
where
recursion n used (xs, rng)
| n == 0 = (xs, rng)
| I.member x used = recursion n used (xs, rng')
| otherwise = recursion (n-1) (I.insert x used) (x:xs, rng')
where
(x, rng') = randomR range rngThen to produce 5 numbers in the (inclusive) range 1 -> 10 the call would be something like:
uniqueRandomInts (1, 10) 5 $ mkStdGen 42Firstly I have a few specific questions:
- I've prefixed my module name with
MrKWatkinsto distinguish it as my module (much as I would with a namespace in C#). Is this common practice, or would I be better with a single word for the module name that is more descriptive?
- I've done some pre-condition checking on the parameters, throwing exceptions if they're not satisfied. Is this common practice in Haskell, and is this the best way to do it?
- I've used a nested function for the recursion. Would I be better off separating this out into a top level function that I don't export from the module?
- I've reused some variable names in the recursion function, hiding the ones from the parent scope. Should I use different names instead?
On top of those if there is any advice on style, better ways to implement the above or just general advice would be greatly appreciated.
Solution
Haskell is very different than other languages.
Here is how I would solve the problem:
What does this do?
First note that the
so you will still need to apply this parameter in order to get the values.
I've also refrained from specifying the type of the return value at this stage.... This function is a tool that could be used for any type, why artificially bind it at this point?
The
Passing around infinite lists is one of those mind blowing things to people who are coming to Haskell from other languages, but remember that the language is lazy, so no actual value will be calculated until requested (this is sort of how the human mind works.... If I ask you to imagine an infinite sequence of
We then apply
Finally, we have the mysterious
You can use this function as follows (because we kept things generic, you need to specify the type)
As to your particular questions:
-
As to the prefixing, Haskell has a vibrant community ecosystem where many people contribute code to the public, and some sort of prefixing is needed to keep things sane.... Of course this is only needed if you plan to contribute your code. I personally don't prefix at all when I am writing something small for myself (why complicate things?), and can always add it if I need later. If a personal project becomes larger, I can always add it. This is pretty much how I programmed in Java also....
-
Pre-checking is very common and valuable in Haskell, however.... so is generality. There is no reason that this particular function shouldn't be used by an numerical type over any range (would you want to rewrite the exact same function for a set of random floats over the range
-
I didn't use explicit recursion (it was used in a lower level function that I used), however I'll answer the general philosophy that I follow to determine if a subfunction should be top level or not.... I make a function top level if it describes a generic tool that others could use. I usually use a "where" clause generally only for function documentation (ie- when the name of the variable describes what it is doing). Sometimes you need to define a where clause variable for other reasons (ie- recursion or to avoid repetition), but strangely enough these are usually cases that you are better off writing around anyway (recursion and repetition are usually abstracted away in lower level calls like
-
I don't know :), however if you ever turn on
EDIT-----
Oops, as you pointed out, I misread the problem.... You need unique values.
Luckily we can still use all the code above, with a slight addition. Add this-
Then put one more filter in your
Here is how I would solve the problem:
import Control.Arrow
import System.Random
uniqueRandomInts :: (RandomGen g, Random a)
=> (a, a) -> Int -> g -> ([a], g)
uniqueRandomInts range n =
(map fst &&& snd . last) . take n . iterate getNext . randomR range
where getNext = randomR range . sndWhat does this do?
First note that the
g (generator) parameter isn't shown in the example, but it is there.... uniqueRandomInts range n returns a function of type RandomGen g => g -> ([a], g)so you will still need to apply this parameter in order to get the values.
I've also refrained from specifying the type of the return value at this stage.... This function is a tool that could be used for any type, why artificially bind it at this point?
The
iterate getNext $ randomR range g part (where I've shown where the g will be applied) creates an infinite list of (a, g), where the a's are the random values, and the g's are the generators returned at each step. (for reference, iterate f x is a function that returns [x, f x, (f.f) x, (f.f.f) x, ....]).Passing around infinite lists is one of those mind blowing things to people who are coming to Haskell from other languages, but remember that the language is lazy, so no actual value will be calculated until requested (this is sort of how the human mind works.... If I ask you to imagine an infinite sequence of
1's, you can reason about it without actually sitting there trying to enumerate all the 1's in your brain before you go on).We then apply
take n to this, to take the first n values of the sequence (thus turning it into something finite before we actually need to calculate any of the values).Finally, we have the mysterious
(map fst &&& snd . last) part. This isn't actually needed for the calculations themselves, but just to format the values in the way that you wanted them. (func1 &&& func2) x applies the functions on the value x, and returns (func1 x, func2 x). We are pulling out the random values in the first slot, and getting the last random generator for the second spot.You can use this function as follows (because we kept things generic, you need to specify the type)
main = do
g <- getStdGen
print $ randomValues (1, 10::Int) 10 gAs to your particular questions:
-
As to the prefixing, Haskell has a vibrant community ecosystem where many people contribute code to the public, and some sort of prefixing is needed to keep things sane.... Of course this is only needed if you plan to contribute your code. I personally don't prefix at all when I am writing something small for myself (why complicate things?), and can always add it if I need later. If a personal project becomes larger, I can always add it. This is pretty much how I programmed in Java also....
-
Pre-checking is very common and valuable in Haskell, however.... so is generality. There is no reason that this particular function shouldn't be used by an numerical type over any range (would you want to rewrite the exact same function for a set of random floats over the range
-1.0 to +1.0?). I would personally place the limit check and type fixing higher up in the code.... And when I did, I would use the Data.Ix.inRange function.-
I didn't use explicit recursion (it was used in a lower level function that I used), however I'll answer the general philosophy that I follow to determine if a subfunction should be top level or not.... I make a function top level if it describes a generic tool that others could use. I usually use a "where" clause generally only for function documentation (ie- when the name of the variable describes what it is doing). Sometimes you need to define a where clause variable for other reasons (ie- recursion or to avoid repetition), but strangely enough these are usually cases that you are better off writing around anyway (recursion and repetition are usually abstracted away in lower level calls like
iterate and (&&&), which are cleaner to use anyway). This isn't a hard fast rule though! Not everything can be abstracted in some high level function yet.-
I don't know :), however if you ever turn on
-Wall in the compiler, it will complain every time you shadow another variable, so to avoid the nagging I usually try to change the name. Sometimes choosing a different name becomes annoying, so I just turn off -Wall :)EDIT-----
Oops, as you pointed out, I misread the problem.... You need unique values.
Luckily we can still use all the code above, with a slight addition. Add this-
removeDuplicatesUsing::Ord b=>(a->b)->[a]->[a]
removeDuplicatesUsing f theList = [x|(Just x, _, _) <- iterate getNext (Nothing, theList, empty)]
where
getNext (_, x:xs, used)
| f x `member` used = (Nothing, xs, used)
| otherwise = (Just x, xs, insert (f x) used)Then put one more filter in your
randomValue pipeliCode Snippets
import Control.Arrow
import System.Random
uniqueRandomInts :: (RandomGen g, Random a)
=> (a, a) -> Int -> g -> ([a], g)
uniqueRandomInts range n =
(map fst &&& snd . last) . take n . iterate getNext . randomR range
where getNext = randomR range . sndRandomGen g => g -> ([a], g)main = do
g <- getStdGen
print $ randomValues (1, 10::Int) 10 gremoveDuplicatesUsing::Ord b=>(a->b)->[a]->[a]
removeDuplicatesUsing f theList = [x|(Just x, _, _) <- iterate getNext (Nothing, theList, empty)]
where
getNext (_, x:xs, used)
| f x `member` used = (Nothing, xs, used)
| otherwise = (Just x, xs, insert (f x) used)(map fst &&& snd . last) . take n . removeDuplicatesUsing fst . iterate getNext . randomR rangeContext
StackExchange Code Review Q#36671, answer score: 9
Revisions (0)
No revisions yet.