And those libraries aren't just "hacks" to deal with laziness or purity. They're wonderful APIs for writing constant-memory streaming programs. I always wish other languages had libraries on their level.
And Haskell is cutting-edge in more ways than laziness so I don't think this is a big deal. It's _technically_ "why it exists" but in practice there's a variety of other reasons people pick Haskell nowadays besides laziness.
That’s the kind of thing you would use “if” for right?
This should be read from right to left: it sorts an (implicit) list, then reads the first 10 elements from the beginning of the now-sorted list, and throws away the rest.
Except under the hood in Haskell, sort is not a function that takes a list and returns a list. It is a function that takes a list, and returns the smallest element of that list and a function to fetch the rest of the sorted list. (A function which actually returns the second-smallest element and a function that if called returns the third smallest element and another function, etc.)
These ephemeral functions / continuations / "thunks" are allowed to have internal state and thereby perform more complex sorting operations such as quicksort, and might actually be represented by an internal tree of branch points and their returned value or unexecuted thunks.
But all of this complexity is hidden by the compiler. It's not some special property of `sort` either, which might look like this:
sort [] = []
sort (x:xs) = sort small ++ (x : sort large)
where small = [y | y <- xs, y <= x]
large = [y | y <- xs, y > x]
"The sort of an empty list is the empty list. The sort of a non-empty list is the elements of the tail less than or equal to the head of the list, sorted, followed by the head of the list, followed by the elements of the tail greater than the head of the lest, sorted."The Haskell compiler does the magic of turning this into a lazy-optimized implementation which in the case of `take 10 . sort` doesn't bother to calculate any remaining `sort large` partitions once the first 10 smallest elements have been found.
Is your k here representing “take k . sort”?
In other words, the very nature of Haskell's lazy-by-default semantics makes the straight-forward, seemingly strict implementation of quicksort, or other sorting algorithm, automatically optimized to improve performance through lazy execution.
Laziness enables many compositions to work efficiently, whereas with a strict language, you'd have to fuse the operations to get the same efficiency. More examples here: http://augustss.blogspot.com/2011/05/more-points-for-lazy-ev...
At the risk of catching HN's ire... this is not a good thing.
Haskell's type signature tells you almost nothing about whether this use-case is supported. If it's not, you've got yourself a nonlinear slowdown, potentially wrecking your performance. Readers can't know this without literally reading the documentation, except the documentation doesn't even tell you if this is OK.
Yet because people are being ‘clever’ like this, Haskell has ended up stuck with a sort a factor ~ten slower than Python (!!), and an incremental sort that's still a factor ~2 slower than Python's heapq-based incremental sort.
So you're using a sort in a way that you have no explicit language-level guarantees for, seemingly no documented guarantees for at all, and in a way that's incredibly harmful to the common case, but also—and this is a curse that's almost unique to Haskell—the language is also prevented from utilizing future improvements to sorting algorithms!
Because of this, and despite Haskell's ‘purity’, the internals of Haskell's sort are more observable and less open to change than is the case for almost any other language that I know of!
Back in reality of course, Haskell has fast, generic, linear-time sorting: https://hackage.haskell.org/package/discrimination
I've never seriously programmed Haskell, so honest question: I understand the first point, but how is the second possible? Isn't the point of passing RealWorld around that you enforce the order of execution through data dependencies rather than expression order? It always seemed like a very elegant (and incredibly impractical :) ) solution to me.
Almost nobody uses lazy IO anymore though. Most IO is done w/ strict IO functions from the bytestring library and often with stream processing libraries like conduit or pipes.
... in anything large. If I'm writing something small of the form "read stuff from stdin", lazily consuming the results of getContents is a perfectly fine way of shipping data through the program. As soon as it starts getting complicated, though, it becomes important to refactor (which is often super easy in Haskell).
I actually treat unsafePerformIO pretty similar, though with more of a requirement that the project be expected to stay tiny. Worth noting that it's much more important you understand Haskell's evaluation model in that case. ... assuming what you're doing matters. If you're just having fun, it can be a good way to learn a little more about Haskell's evaluation model.
fd <- open "/some/path"
s <- readContentsLazy fd
close fd
pure $ processString s
Now, processString is getting a string with the file's contents, right? Nope, you have a cons cell that probably contains the first character of the file, and maybe even a few more up to the first page that got read from disk, but eventually as you're processing that string, you'll hit a point where your pure string processing actually tries to do IO on the file that isn't open anymore, and your perfect sane and pure string processing code will throw an exception. So, that's gross.That's a real issue that will hit beginners. There's been a lot of work done to make ergonomic and performant libraries that handle this without issues; I think that right now pipes[0] and conduit[1] are the big ones, but it's a space that people like to play with.
[0] - https://hackage.haskell.org/package/pipes [1] - https://github.com/snoyberg/conduit
Because the program we're compiling needs to work on actual computers, running under actual (usually at least vaguely POSIX) operating systems. In that context, it's unavoidable that the set of open file descriptors sometimes matters. It can matter because of resource limits. It can also change whether another process gets an SIGPIPE versus blocking forever. It can affect locking.
i guess i would ask why it's possible to close a file that's going to be used after it's closed? will linear types[1] solve this?
I don't think there's much reason to want to do it, but it's not obvious how to enforce that while still retaining the flexibility we'd want.
Linear types expand the solution space, to be sure. Whether they "solve this" depends a bit on exactly what we consider the problem to be.
That is, the abstractions don't do what non-Haskell abstractions would lead you to expect.
In other words the problem seems to be (in this example) that the standard library mixes lazy and strict semantics. A better library wouldn’t carry that flaw.
That's probably doable. It's true that when the only reference to the handle in question is the one buried in the thunk pointed at by the lazy input, it should be safe to close it when a thunk evaluates to end-of-input (or an error, for that matter).
I'm not sure whether or not it'd be applicable enough to be worth doing. The immediate issues I spot are that a lot of input streams aren't consumed all the way to the end, and that you'd have to be careful not to capture a reference anywhere else (or you'll be waiting for GC to remove that reference before the count falls to zero).
These are sometimes useful for optimization but in an unfortunate bit of history, some developers decided to use these to "simulate" Haskell's lazy semantics for side-effecting I/O operations, the so-called "lazy I/O".
Fortunately this is just a few functions in the standard library which you can simply avoid and serious Haskell codebases should use a linter to prevent people from calling them.
> `unsafeInterleaveIO` allows an IO computation to be deferred lazily. When passed a value of type IO a, the IO will only be performed when the value of the a is demanded. This is used to implement lazy file reading, see `hGetContents`.
The fact that lazy IO behaves so unintuitively and causes significant problems strongly signals that that particular abstraction shouldn't be broken.
Also, there's a link to a page about laziness on the left.