如何从函数中删除命令性代码?

时间:2009-10-18 22:22:03

标签: f# functional-programming imperative-programming

我是功能性世界的新手并且非常感谢这个。

我想从这个简单的函数中 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

7 个答案:

答案 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

感谢你们的解决方案,特别是朱丽叶和约翰(我实际上已经读过几次了。)
: - )