在这种情况下可以使用不可变的集合吗?

时间:2019-12-19 15:16:47

标签: f# immutability immutable-collections

我已经将代码从python转换为F#,但我希望使用更惯用的语法。 我认为有两个大障碍

  • 效果不佳(考虑到n很大)
  • 不够强大的抽象

我当前拥有的(有效)代码为

let valuefolded =
    System.Collections.Generic.List(
        [0..( (n - (offset % input.Length) - 1) / input.Length)]
        |> List.fold
            (fun acc _ -> List.append acc input)
            (input
            |> List.skip (offset % input.Length)))
for repeat in [0..99] do  
    let mutable acc = 0
    for i in ([0..(n-1)] |> List.rev) do
        valuefolded.[i] <- Math.Abs(acc + valuefolded.[i]) % 10
        acc <- valuefolded.[i]

(在笔记本电脑上获取实际数据大约需要一分钟时间)

如果有可能,并且有道理,我希望有两个可变的变量。

  1. mutable acc(已完成,请参见下文)
  2. System.Collections.Generic.List包装器

我尝试过的

第一个目标显得微不足道,而我怀疑第二个目标要困难得多。

我浪费了几个小时而没有有意义的结果。

好吧,我在几分钟内轻松实现了第一个目标(没有mutable acc

[0..99]
|> List.iter (
    fun repeat ->
        [0..(n-1)] 
        |> List.rev
        |> List.fold (fun acc i ->
            valuefolded.[i] <- Math.Abs(acc + valuefolded.[i]) % 10
            valuefolded.[i]) 0
        |> ignore
    ) 

参考

f#代码摘录了我对AoC day 16 part 2的解决方案。我自己做了第一部分,但是我从最初评论中提到的python脚本中复制了第二部分(此问题的对象)。

3 个答案:

答案 0 :(得分:0)

似乎您仅使用Generic.List来获得可以按索引访问的集合。您可以改用内置的Array。 这是代码

let valuefolded =        
        [0..( (n - (offset % input.Length) - 1) / input.Length)]
        |> List.fold
            (fun acc _ -> List.append acc input)
            (input
            |> List.skip (offset % input.Length))
        |> Array.ofList

此外,我建议您自己检查一下,但请注意以下参数

let offset = 1
    let input = [1..10000000]
    let n = 3

表明它的性能比原始版本要快。

答案 1 :(得分:0)

管理摘要

与可变数据类型的O(1)相比,可以在O(n)的时间内使用功能性编程不可变模式对N个元素的不可变集合进行更新。 在这个程度上,先前接受的答案很困惑,因为它专注于访问和 read 情况,即不同,因为在这种情况下,不可变数组可以为O(1 )与列表读取访问权限(即O(n))进行比较。回到这一点,这里我们需要修改集合的元素,因此-AFAIK-我们无法逃脱O(n)限制,这将进一步提高包装折痕内的O(n ^ 2)。 因此,由于问题与大量n输入有关,因此很遗憾,答案为否,这在有效时间内是不可行的。

无用的代码

正如评论中已经提到的那样,我通常能够找到一个不变的版本,而在性能方面没有明显的差异,但是在这种情况下,我投降了,因为第二次更改后性能太差了。但是我能够找到一个不变的(即使不够快)版本。 指出该代码无用的原因只是更新部分,该部分被翻译为O(n)映射(List.mapi或更好的Array.mapi,区别在于完全无关紧要)。

对于簿记,如果其他人提出了更好(和性能更高)的代码(与我的预期相反),则可以作为比较参考。

let answer2_all =
    [|0..99|]
    |> Array.fold (
        fun (state:int[]) repeat ->
                printfn "repeat %d" repeat
                [|0..n-1|] 
                |> Array.rev
                |> Array.fold
                    (fun (previous: int[], acc) i ->
                        let next = 
                            previous
                            |> Array.mapi(fun j t ->
                                if j <> i then t else
                                Math.Abs(acc + previous.[i]) % 10)
                        (next, next.[i])
                    ) (state, 0)
                |> fst
        ) (valuefolded0 |> Array.take n)

就语法而言,关键是先前可变List的每次更新都是在外部不可变List.foldArray.fold的状态更新中进行转换的。但是,正如我已经写的那样,内部的List.mapiArray.mapi代码段(用于更新很长的列表中的一项)会破坏性能(差得多)。

与Haskell和最终有用的代码进行比较

现在,如果我看一下Haskell分支,并且在历史的开始,我可以找到这个fundamental implementation

day16b :: String -> [String]
day16b (map digitToInt . filter isDigit -> input)
  | offset + 8 < extendedLength && extendedLength <= 2 * offset
  = map intToDigit . Vector.toList . Vector.take 8 <$> iterate g v0
  | otherwise = error "unimplemented!" where
    offset = foldl' (\a b -> 10 * a + b) 0 $ take 7 input
    realLength = length input
    extendedLength = realLength * 10000
    v0 = Vector.generate @Unboxed.Vector (extendedLength - offset) $
        (input !!) . (`mod` realLength) . (offset +)
    g = Vector.map ((`mod` 10) . abs) . Vector.postscanr' (+) 0

注意最后一行

    g = Vector.map ((`mod` 10) . abs) . Vector.postscanr' (+) 0

如果我们查看O(n)Vector.postscanr'中的source

-- | /O(n)/ Right-to-left scan with strict accumulator
postscanr' :: (Vector v a, Vector v b) => (a -> b -> b) -> b -> v a -> v b
{-# INLINE postscanr' #-}
postscanr' f z = unstreamR . inplace (S.postscanl' (flip f) z) id . streamR

它会进行安全复制,但是会使用可变的数据结构来更新元素就地,因此可以保持O(n)而不会升级为O(n ^ 2)

这是最终正确的F#等价物

 let answer2_all =
     [|0..99|]
     |> Array.fold (
         fun (state:int[]) repeat ->
             let next = Array.copy state
             [|0..n-1|] 
             |> Array.rev
             |> Array.fold
                 (fun acc i ->
                    next.[i] <- Math.Abs(acc + next.[i]) % 10
                    next.[i]
                 ) 0
             |> ignore
             next
         ) (valuefolded0 |> Array.take n)

 let answer2 = answer2_all |> Seq.take 8

 printfn "Answer Part 2 is %s" (String.Join("",answer2))

请注意,还有第二种可能的正确版本,在该版本中我们不使用可变的Array.set,但仍然需要可变的累加器:有趣的是有人是否想挑战最终结果,并尝试消除最后一个mutable ...:-)

let answer2_all =
    [|0..99|]
    |> Array.fold (
        fun (state:int[]) repeat ->
                [|
                    let mutable acc = 0
                    for i in 0..n - 1  do
                        acc <- Math.Abs(acc + state.[n - 1 - i]) % 10
                        yield acc
                |] |> Array.rev
        ) (valuefolded0 |> Array.take n)

let answer2 = answer2_all |> Seq.take 8

printfn "Answer Part 2 is %s" (String.Join("",answer2))

答案 2 :(得分:0)

挑战

  

请注意,还有第二种可能的正确版本,其中我们不使用可变的Array.set,但仍然需要可变的累加器:有趣的是要知道有人是否要挑战最终结果并尝试消除最后的结果易变...:-)

挑战被接受并赢得: 有Array.scan 对应于最后一段中的第二个正确版本。

具有不变集合的高性能代码

我们要去了:此更改为不可变集合,并且速度更快

let answer2_all =
    [|0..99|]
    |> Array.fold (
        fun (state:int[]) repeat ->

                Array.scanBack 
                    (fun state_i acc -> Math.Abs(acc + state_i) % 10) 
                    state 0

        ) (valuefolded0 |> Array.take n)

致谢

comment on reddit阐明了Haskell scanl也可以使用,因此是我的F#翻译。