在Haskell建立一个列表

在Haskell建立一个列表,haskell,Haskell,我想做的是创建一个随机整数列表,没有重复项。作为第一步,我有一个函数,它可以列出n个随机样本。如果不需要传入一个空列表就可以启动列表,那么如何以更为Haskell惯用的方式编写呢?我确信我遗漏了一些基本的东西 -- make a list of random integers. -- takes a size, and an empty list. -- returns a list of that length of random numbers. f :: Int -> [Int] -

我想做的是创建一个随机整数列表,没有重复项。作为第一步,我有一个函数,它可以列出n个随机样本。如果不需要传入一个空列表就可以启动列表,那么如何以更为Haskell惯用的方式编写呢?我确信我遗漏了一些基本的东西

-- make a list of random integers.
-- takes a size, and an empty list.
-- returns a list of that length of random numbers.
f :: Int -> [Int] -> IO [Int]
f l xs | length xs >= l = return (xs)
f l xs = do
  r <- randomRIO (1, 40) :: IO Int
  f l $ r : x

最终,该函数将具有检查重复插入的过滤功能,但这是一个单独的问题。虽然这看起来像是家庭作业,但它不是,而是我自己尝试处理用于随机数生成的状态monad的一部分,我发现我被困在了一个更早的位置。

好吧,您可以对递归调用的输出进行操作:

f :: Int -> IO [Int]
f 0 = return []
f n = do
    r <- randomRIO (1, 40)
    xs <- f (n-1)
    return $ r : xs
这将使函数的复杂度从线性变为二次,因为每次
++
调用都必须扫描以前生成的所有数字序列,然后再追加新的数字


但是,您可以简单地执行以下操作:

f n = sequence $ replicate n (randomRIO (1, 40))
创建由
randomRIO
操作组成的
[IO Int]
长度
n
列表,并采取
[IO a]
操作,通过按顺序执行所有操作并收集结果,将其转换为
IO[a]

更简单的是,您可以使用您想要的功能:

import Control.Monad(replicateM)

f n = replicateM n (randomRIO (1, 40))
或以无点样式:

f :: Int -> IO [Int]
f = flip replicateM $ randomRIO (1, 40)

这使用
集合
来跟踪已生成的数字:

import System.Random
import qualified Data.Set as Set

generateUniqueRandoms :: (Int, Int) -> Int -> IO [Int]
generateUniqueRandoms range@(low, high) n =
  let maxN = min (high - low) n
  in
     go maxN Set.empty

  where
     go 0 _ = return []
     go n s = do
        r <- getUniqueRandom s
        xs <- go (n-1) (Set.insert r s)
        return $ r : xs

     getUniqueRandom s = do
         r <- randomRIO range
         if (Set.member r s) then getUniqueRandom s
         else return r

但是,值得注意的是,如果
n
接近范围的宽度,则洗牌范围内所有数字的列表并获取其中的第一个
n
,效率会更高。

可以使用包装器函数隐藏空列表的传递吗?这似乎是可能的,但非常不雅。现在,下一步是在列表中添加重复插入检查(想象一组彩票号码)。使用优雅的序列和复制代码,我看不出如何将添加的逻辑添加到代码中。@andro是的,您也不能修改它们来实现这一点。但是请记住,他们限制了自己的能力,这对于正确性是一件好事。当你有一个明确的递归定义时,它可以做任何事情。使用特殊功能,如
fold
s或
replicate
等,完全消除了引入多种错误的机会。此外,编译器能够对此类代码进行优化,而显式递归代码更难优化,因为编译器无法证明泛型代码所需的所有属性。在我看来,这里传递的集合似乎代表了计算中的状态,这是过去历史的记录。这将如何用状态单子表示?
f :: Int -> IO [Int]
f = flip replicateM $ randomRIO (1, 40)
import System.Random
import qualified Data.Set as Set

generateUniqueRandoms :: (Int, Int) -> Int -> IO [Int]
generateUniqueRandoms range@(low, high) n =
  let maxN = min (high - low) n
  in
     go maxN Set.empty

  where
     go 0 _ = return []
     go n s = do
        r <- getUniqueRandom s
        xs <- go (n-1) (Set.insert r s)
        return $ r : xs

     getUniqueRandom s = do
         r <- randomRIO range
         if (Set.member r s) then getUniqueRandom s
         else return r
Main> generateUniqueRandoms (1, 40) 23
[29,22,2,17,5,8,24,27,10,16,6,3,14,37,25,34,30,28,7,31,15,20,36]

Main> generateUniqueRandoms (1, 40) 1000
[33,35,24,16,13,1,26,7,14,11,15,2,4,30,28,6,32,25,38,22,17,12,20,5,18,40,36,39,27,9,37,31,21,29,8,34,10,23,3]

Main> generateUniqueRandoms (1, 40) 0
[]