问题
Problem
I'm trying to implement the modified Dragon Curve from AoC Day 16 as an infinite list in Haskell.
The list is composed of True
and False
. We start with some list s0
:
s1 = s0 ++ [False] ++ (map not . reverse) s0
s2 = s1 ++ [False] ++ (map not . reverse) s1
s3 = s2 ++ [False] ++ (map not . reverse) s2
Generally
sn = s(n-1) ++ [0] ++ (map not . reverse) s(n-1)
= s0 ++ [0] ++ (f s0) ++ [0] ++ (f (s0 ++ [0] ++ (f s0))) ++ ...
where f = (map not . reverse)
Attempted Implementation
I can get sn
quite easily using the iterate
function.
modifiedDragonCurve :: [Bool] -> Int -> [Bool]
modifiedDragonCurve s n = (iterate f s)!!n
where f s = s ++ [False] ++ (map not . reverse) s
This gives me a list [s0, s1, s2, ...]
. However, since s(n-1)
is a prefix of sn
this could be built as an infinite list, but i cannot figure out how to approach it. I think I need something along the lines of
modifiedDragonCurve :: [Bool] -> [Bool]
modifiedDragonCurve s = s ++ [False] ++ (map not . reverse) listSoFar
But cannot figure out how to refer to the already generated list (listSoFar
).
Any suggestions would be greatly appreciated.
回答1:
I played with this myself while solving the AoC problem, too. I found a remarkable solution that does not require reverse, hence is more memory-friendly and speedy than the other solutions listed here. It's also beautiful! The dragon curve itself is a nice short two-liner:
merge (x:xs) ys = x:merge ys xs
dragon = merge (cycle [False, True]) dragon
It can be extended to use a "seed" as the AoC problem demands just by alternating between the seed and the bits of the true dragon curve:
infinite bs = go bs (map not (reverse bs)) dragon where
go bs bs' (d:ds) = bs ++ [d] ++ go bs' bs ds
(This does call reverse
once -- but unlike other solutions, it is called just once on a chunk of data about the size of the input, and not repeatedly on chunks of data about as large as the part of the list you consume.) Some timings to justify my claims; all versions used to produce 2^25 elements with an empty seed, compiled with ghc -O2
, and timed with /usr/bin/time
.
freestyle's solution takes 11.64s, ~1.8Gb max resident
David Fletcher's solution takes 10.71s, ~2Gb max resident
luqui's solution takes 9.93s, ~1GB max resident
my solution takes 8.87s, ~760MB max resident
The full test program was
main = mapM_ print . take (2^25) . dragon $ []
with dragon
replaced by each implementation in turn. A carefully crafted consumer can lower memory usage even further: my best solution to the second-star problem so far runs in 5Mb real residency (i.e. including all the space GHC allocated from the OS for its multiple generations, slack space, and other RTS overhead), 60Kb GHC-reported residency (i.e. just the space used by not-yet-GC'd objects, regardless of how much space GHC has allocated from the OS).
For raw speed, though, you can't beat an unboxed mutable vector of Bool
: a coworker reports that his program using such ran in 0.2s, using about 35Mb memory to store the complete expanded (but not infinite!) vector.
回答2:
Here's one way. We make a list not of the s0, s1 etc but of only the new part at each step, then we can just concat them together.
dragonCurve :: [Bool]
dragonCurve = concatMap f [0..]
where
f n = False : (map not . reverse) (take (2^n-1) dragonCurve)
(This assumes s0 = []. If it can be something else you'll have to modify the length calculation.)
I can't think of a nice way to both be self-referential and not deal with prefix lengths. Here's a non-self-referencing solution, still using the idea of making a list of non-overlapping parts.
dragonCurve' :: [Bool]
dragonCurve' = concat (unfoldr f [])
where
f soFar = Just (part, soFar ++ part)
where
part = False : (map not . reverse) soFar
回答3:
You totally nerd sniped me with this. It's not a self-referencing list, but I did manage to come up with a "wasteless" solution -- one where we're not dropping or forgetting about anything we've computed.
dragon :: [Bool] -> [Bool]
dragon = \s -> s ++ gen [] s
where
inv = map not . reverse
gen a b =
let b' = inv b
r = b' ++ a
in False:r ++ gen r (False:r)
gen a b
accepts as input all the data of the current sequence, such that the current sequence is inv a ++ b
. Then we generate the remainder in r
, output it and recursively continue generating the remainder. I accept a
inverted because then all I need to do is prepend b'
at each step (which does not even examine a
), and we don't need to reverse more than we have to.
Being nerdsniped I investigated quite a number of other data structures, imagining that a linked list is probably not the best fit for this problem, including DList
, Data.Sequence
(finger trees), free monoid (which ought to be good at being reversed), and a custom tree that cancels reverses. To my surprise, list still performed the best of all of these and I'm still bewildered by that. In case you are curious, here is my code.
回答4:
For example:
dragon s0 = s0 ++ concatMap ((False :) . inv) seq
where
inv = map not . reverse
seq = iterate (\s -> s ++ False : inv s) s0
来源:https://stackoverflow.com/questions/41183330/infinite-self-referencing-list