【问题标题】:How to remove imperative code from a function?如何从函数中删除命令式代码?
【发布时间】:2010-12-07 20:32:47
【问题描述】:

我是函数式世界的新手,非常感谢这方面的帮助。

我想从这个简单的函数中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 很容易以命令方式实现,但在功能上?


这是功能性的,但需要 list 而不是 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

【问题讨论】:

  • 我也会选择基于列表的版本(您需要评估整个输入,因此 seq 的按需产量优势丧失了)。您可以修改第二个代码示例以将序列作为输入,并将该序列同时转换为列表和总和。然后将列表用于算法的基于列表的部分。该列表将以相反的顺序排列,但这对于计算无关紧要。代码: let sum,L = probabilitySeq |> Seq.fold (fun (sum,L) dir -> sum + snd dir, dir::L) (0.0,[]).

标签: f# functional-programming imperative-programming


【解决方案1】:

使用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

但在这种情况下,我可能还会使用基于列表的方法,因为无论如何都需要预先计算概率值的总和...

【讨论】:

    【解决方案2】:

    我认为 cfern 的建议实际上是最简单 (?= best) 的解决方案。

    需要评估整个输入,因此 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
    

    感谢您的解决方案,尤其是 Juliet 和 Johan(我已经阅读了几次才能真正理解)。
    :-)

    【讨论】:

      【解决方案3】:

      我会使用您的功能性、基于列表的版本,但将其调整为使用 F# PowerPack 中的 LazyList。使用LazyList.of_seq 会给你一个列表的道德等价物,但不会立即评估整个事情。您甚至可以使用LazyList.(|Cons|Nil|) 模式在LazyLists 上进行模式匹配。

      【讨论】:

        【解决方案4】:

        按照我的理解,你的逻辑是这样的:

        求和所有权重,然后选择介于 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
        

        【讨论】:

          【解决方案5】:

          最简单的解决方案是使用 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)
          

          【讨论】:

            【解决方案6】:

            我将只提供 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"
            

            【讨论】:

              【解决方案7】:

              我会使用Seq.to_list 将输入序列转换为列表,然后使用基于列表的方法。引用的列表足够短,不应该是不合理的开销。

              【讨论】:

                猜你喜欢
                • 2014-03-20
                • 2011-09-30
                • 1970-01-01
                • 1970-01-01
                • 2010-10-29
                • 2014-10-06
                • 1970-01-01
                • 1970-01-01
                • 2012-02-10
                相关资源
                最近更新 更多