5 ms·
I love haskell and understand your point. Laziness is beautiful and allows to express things in a simple way and also improve the reusability of the code etc.
by ithkuil 16y ago
I love haskell and understand your point. Laziness is beautiful and allows to express things in a simple way and also improve the reusability of the code etc.
However I think that you didn't pick the best example.
I tried out:
>head $ sort [1 .. 10000000]
6 secs, using ~2g of heap! (actually is should be a reverse sort)
and the "hand coded loop":
>let mx (x:xs) m = if x > m then mx xs x else mx xs m; mx [] m = m
>mx [1 .. 10000000] 0
3 secs, heap usage stays negligible low and constant.
Something is clearly not behaving as you depicted.
(of course my 'loop' code is not the exact equivalent of the last.sort composition, since it requires a 'minimum' parameter to be passed in advance, which not all types have. On the other hand it works also for the empty list)
I also have the feeling that, unless special compiler optimization (a very 'specific' one, I fear), the simple application of the 'head' function to a sorted list would stop when the first result element is produced, which is not after O(n). Granted, you don't have to wait for a full sort, since the sorting algorithm could guarantee that the rest of the list contain 'lesser/bigger' elements only and thus stop. But keeping track of all this should be space consuming, in respect to a simple linear scan.
Anyway the space problem of the "lazy" solution is a bigger issue than the number of comparisons.
I'm not a haskell master, but if I got it right, one of the mayor problems of lazy programming is that in some situations it can degenerate to a huge amounts of "unevaluated thunks", which are frozen computations yet to be performed, but which require some state to be held in memory (like function arguments, I guess).
(http://www.haskell.org/haskellwiki/Thunk http://www.haskell.org/haskellwiki/Thunk)
or in this case, the space usage is caused simply because the list has to be materialized in memory instead of be simply traversed and generated on the fly. (but 2g seems slightly too much).
Anyway, the point here is that the two methods are not equivalent.
I think that understanding the impact of laziness on space is an issue that certainly increases the learning curve, as it requires time to master this and other optimization techniques if you want to get predictable performances from haskell.
(BTW, I actually use haskell for work, perhaps in a slightly conterintuitive way. I use haskell for quick prototyping ideas and solutions. Sometimes I get inspired by the solution I end up with haskell and translate it in clojure or java (work requirement), or at other times I have to rewrite it completely, but the possibility to quickly prototype in haskell really helps me a lot. I would love a stable ghc JVM backend.... it would change my life)
- ithkuil 16y agoI found this article referencing the O(n) complexity for the 'head . sort' http://apfelmus.nfshost.com/articles/quicksearch.html http://apfelmus.nfshost.com/articles/quicksearch.html As far as I can understand from the blog and and the cited mailing lists, it works but it requires a carefully coded sorting method, otherwise O(n log n) as expected.
- jrockway 16y agoYes, you have to pick algorithms carefully in any programming language. Haskell is a tool, not a magician.
- loewenskind 16y agoI should have pointed out that my code was theoretical and not actual. My example assumes a (non-tail call) merge sort. Sort specifically was (I felt) a small part of my point but I should have known that on a "hacker" site one must be exact. :)
- ithkuil 16y agoI understand that you wanted to make a simple example, I just wanted to expand a little bit the issue of laziness and make it look slightly less magic. I found a fascinating examples of the expressiveness and beauty of haskell due to lazy evaluation, which I think is better suited for FP evangelism: fibs = 0 : 1 : zipWith (+) fibs (tail fibs) Here we are defining the fibonacci sequence in a declarative way, recursing with a recursive "call". The way the 'fibs' function is coded directly reflects the definition of the fibonacci sequence itself: The 'fibs' list is defined as a list containing the first 2 elements and then, as a tail, the result of the application of the + function on the previous pair of elements. "You can borrow things from the future as long as you don't try to change them"