There is a well known solution for generating an infinite stream of Hamming numbers (i.e. all positive integers n
where n = 2^i * 3^j * 5^k
). I have implemented this in two different ways in F#. The first method uses seq<int>
. The solution is elegant, but the performance is terrible. The second method uses a custom type where the tail is wrapped in Lazy<LazyList<int>>
. The solution is clunky, but the performance is amazing.
Can someone explain why the performance using seq<int>
is so bad and if there is a way to fix it? Thanks.
Method 1 using seq<int>
.
// 2-way merge with deduplication
let rec (-|-) (xs: seq<int>) (ys: seq<int>) =
let x = Seq.head xs
let y = Seq.head ys
let xstl = Seq.skip 1 xs
let ystl = Seq.skip 1 ys
if x < y then seq { yield x; yield! xstl -|- ys }
elif x > y then seq { yield y; yield! xs -|- ystl }
else seq { yield x; yield! xstl -|- ystl }
let rec hamming: seq<int> = seq {
yield 1
let xs = Seq.map ((*) 2) hamming
let ys = Seq.map ((*) 3) hamming
let zs = Seq.map ((*) 5) hamming
yield! xs -|- ys -|- zs
}
[<EntryPoint>]
let main argv =
Seq.iter (printf "%d, ") <| Seq.take 100 hamming
0
Method 2 using Lazy<LazyList<int>>
.
type LazyList<'a> = Cons of 'a * Lazy<LazyList<'a>>
// Map `f` over an infinite lazy list
let rec inf_map f (Cons(x, g)) = Cons(f x, lazy(inf_map f (g.Force())))
// 2-way merge with deduplication
let rec (-|-) (Cons(x, f) as xs) (Cons(y, g) as ys) =
if x < y then Cons(x, lazy(f.Force() -|- ys))
elif x > y then Cons(y, lazy(xs -|- g.Force()))
else Cons(x, lazy(f.Force() -|- g.Force()))
let rec hamming =
Cons(1, lazy(let xs = inf_map ((*) 2) hamming
let ys = inf_map ((*) 3) hamming
let zs = inf_map ((*) 5) hamming
xs -|- ys -|- zs))
[<EntryPoint>]
let main args =
let a = ref hamming
let i = ref 0
while !i < 100 do
match !a with
| Cons (x, f) ->
printf "%d, " x
a := f.Force()
i := !i + 1
0
seq
-comprehension is so much faster thanSeq.map
. I also tried to get this to work with F# PowerPack, but I haven't been successful so far. – Bouffant