我是功能性世界的新手并且非常感谢这个。
我想从这个简单的函数中 SUPERCEDE 丑陋的命令式代码,但不知道该怎么做。
我想要的是随机选择IEnumerable中的一些元素(F#中的seq)与概率值 - 元组中的第二项(所以“概率”0.7的项目将比0.1更频繁地被选中。)
/// seq<string * float>
let probabilitySeq = seq [ ("a", 0.7); ("b", 0.6); ("c", 0.5); ("d", 0.1) ]
/// seq<'a * float> -> 'a
let randomPick probSeq =
let sum = Seq.fold (fun s dir -> s + snd dir) 0.0 probSeq
let random = (new Random()).NextDouble() * sum
// vvvvvv UGLY vvvvvv
let mutable count = random
let mutable ret = fst (Seq.hd probSeq )
let mutable found = false
for item in probSeq do
count <- count - snd item
if (not found && (count < 0.0)) then
ret <- fst item //return ret; //in C#
found <- true
// ^^^^^^ UGLY ^^^^^^
ret
////////// at FSI: //////////
> randomPick probabilitySeq;;
val it : string = "a"
> randomPick probabilitySeq;;
val it : string = "c"
> randomPick probabilitySeq;;
val it : string = "a"
> randomPick probabilitySeq;;
val it : string = "b"
我认为randomPick
在命令式上实施起来非常简单,但在功能上呢?
这是有效的,但是 列表 而非 seq (想要)。
//('a * float) list -> 'a
let randomPick probList =
let sum = Seq.fold (fun s dir -> s + snd dir) 0.0 probList
let random = (new Random()).NextDouble() * sum
let rec pick_aux p list =
match p, list with
| gt, h::t when gt >= snd h -> pick_aux (p - snd h) t
| lt, h::t when lt < snd h -> fst h
| _, _ -> failwith "Some error"
pick_aux random probList
答案 0 :(得分:4)
使用Matajon建议原则的F#解决方案:
let randomPick probList =
let ps = Seq.skip 1 (Seq.scan (+) 0.0 (Seq.map snd probList))
let random = (new Random()).NextDouble() * (Seq.fold (fun acc e -> e) 0.0 ps)
Seq.find (fun (p, e) -> p >= random)
(Seq.zip ps (Seq.map fst probList))
|> snd
但在这种情况下我可能也会使用基于列表的方法,因为无论如何都需要预先计算概率值的总和......
答案 1 :(得分:2)
我将只提供Haskell版本,因为我的笔记本上没有F#,它应该是类似的。原则是将序列转换为
之类的序列[(0.7,"a"),(1.3,"b"),(1.8,"c"),(1.9,"d")]
其中元组中的每个第一个元素表示不可能,但类似于范围。然后很容易,从0到最后一个数字(1.9)中选择一个随机数并检查它属于哪个范围。例如,如果选择0.5,则它将是“a”,因为0.5低于0.7。
Haskell代码 -
probabilitySeq = [("a", 0.7), ("b", 0.6), ("c", 0.5), ("d", 0.1)]
modifySeq :: [(String, Double)] -> [(Double, String)]
modifySeq seq = modifyFunction 0 seq where
modifyFunction (_) [] = []
modifyFunction (acc) ((a, b):xs) = (acc + b, a) : modifyFunction (acc + b) xs
pickOne :: [(Double, String)] -> IO String
pickOne seq = let max = (fst . last) seq in
do
random <- randomRIO (0, max)
return $ snd $ head $ dropWhile (\(a, b) -> a < random) seq
result :: [(String, Double)] -> IO String
result = pickOne . modifySeq
示例 -
*Main> result probabilitySeq
"b"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"d"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"b"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"c"
*Main> result probabilitySeq
"a"
*Main> result probabilitySeq
"c"
答案 2 :(得分:2)
我理解它的方式,你的逻辑是这样的:
对所有权重求和,然后在0和所有权重之和之间选择一个随机双精度。找到与您的概率相对应的项目。
换句话说,您希望按如下方式映射列表:
Item Val Offset Max (Val + Offset)
---- --- ------ ------------------
a 0.7 0.0 0.7
b 0.6 0.7 1.3
c 0.5 1.3 1.8
d 0.1 1.8 1.9
将(item, probability)
列表转换为(item, max)
非常简单:
let probabilityMapped prob =
[
let offset = ref 0.0
for (item, probability) in prob do
yield (item, probability + !offset)
offset := !offset + probability
]
虽然这可以追溯到变量,它的纯粹,确定性和可读代码的精神。如果你坚持避免可变状态,你可以使用它(不是尾递归):
let probabilityMapped prob =
let rec loop offset = function
| [] -> []
| (item, prob)::xs -> (item, prob + offset)::loop (prob + offset) xs
loop 0.0 prob
虽然我们在列表中处理状态,但我们正在执行映射,而不是折叠操作,所以我们不应该使用Seq.fold或Seq.scan方法。我开始使用Seq.scan编写代码,它看起来很奇怪而且很奇怪。
无论你选择哪种方法,一旦你的列表被映射,很容易在线性时间内选择一个随机加权的项目:
let rnd = new System.Random()
let randomPick probSeq =
let probMap =
[
let offset = ref 0.0
for (item, probability) in probSeq do
yield (item, probability + !offset)
offset := !offset + probability
]
let max = Seq.maxBy snd probMap |> snd
let rndNumber = rnd.NextDouble() * max
Seq.pick (fun (item, prob) -> if rndNumber <= prob then Some(item) else None) probMap
答案 3 :(得分:1)
我会使用Seq.to_list
将输入序列转换为列表,然后使用基于列表的方法。引用的列表足够短,不应该是一个不合理的开销。
答案 4 :(得分:1)
最简单的解决方案是使用ref在Seq模块的任何合适函数的迭代器调用之间存储状态:
let probabilitySeq = seq [ ("a", 0.7); ("b", 0.6); ("c", 0.5); ("d", 0.1) ]
let randomPick probSeq =
let sum = Seq.fold (fun s (_,v) -> s + v) 0.0 probSeq
let random = ref (System.Random().NextDouble() * sum)
let aux = function
| _,v when !random >= v ->
random := !random - v
None
| s,_ -> Some s
match Seq.first aux probSeq with
| Some r -> r
| _ -> fst (Seq.hd probSeq)
答案 5 :(得分:0)
我会使用你的基于列表的功能版本,但要使用它来使用F#PowerPack中的LazyList
。使用LazyList.of_seq
将为您提供列表的道德等价物,但不会立即评估整个事物。您甚至可以使用LazyList
模式在LazyList.(|Cons|Nil|)
上模式匹配。
答案 6 :(得分:0)
我认为 cfern 的建议实际上是最简单的(?=最佳)解决方案。
需要对整个输入进行评估,因此无论如何都会失去seq的按需按需优势。最简单的似乎是将序列作为输入并将其同时转换为列表和总和。然后使用列表作为算法的基于列表的部分(列表将以相反的顺序,但这与计算无关)。
let randomPick moveList =
let sum, L = moveList
|> Seq.fold (fun (sum, L) dir -> sum + snd dir, dir::L) (0.0, [])
let rec pick_aux p list =
match p, list with
| gt, h::t when gt >= snd h -> pick_aux (p - snd h) t
| lt, h::t when lt < snd h -> fst h
| _, _ -> failwith "Some error"
pick_aux (rand.NextDouble() * sum) L
感谢你们的解决方案,特别是朱丽叶和约翰(我实际上已经读过几次了。)
: - )