4 ms·
> lazy I/O which can introduce horrible bugs or straight up mess with execution order of critical code I've never seriously programmed Haskell, so honest quest
by nothrabannosir 7y ago
> lazy I/O which can introduce horrible bugs or straight up mess with execution order of critical code
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.
- almostdeadguy 7y agoMonadic IO allows you to reliably sequence IO operations, but the results of those operations are not produced until their results are forced. Here's an example of the problems with this: http://dev.stephendiehl.com/hask/#lazy-io http://dev.stephendiehl.com/hask/#lazy-io . It's not really a language flaw, but more of a design flaw of the functions in base. 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.
- dllthomas 7y ago> Almost nobody uses lazy IO ... 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.
- tsuraan 7y agoOP is somewhat conflating two different things: non-strict function evaluation and lazy IO. With lazy IO, you can get, for example, a String from a file. That string is actually a lazily-constructed chain of Cons cells, so if you're into following linked lists and processing files one char at a time, then it's fun to use. The dangerous bit comes in when you close the file after evaluating its contents as a string: 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 https://hackage.haskell.org/package/pipes [1] - https://github.com/snoyberg/conduit https://github.com/snoyberg/conduit
- whatshisface 7y agoWhy is it possible to "close" a file? You could have a function that mapped open files to closed files, but the open files would still be there... I think the reason why this weird behavior is cropping up is that the entire language is designed around functions, and here you are reaching into the internal datastructures, mutating state.
- dllthomas 7y ago> Why is it possible to "close" a file? 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.
- mlevental 7y ago> Why is it possible to "close" a file? 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? [1] https://gitlab.haskell.org/ghc/ghc/wikis/linear-types https://gitlab.haskell.org/ghc/ghc/wikis/linear-types
- dllthomas 7y ago> i guess i would ask why it's possible to close a file that's going to be used after it's closed? 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.
- AnimalMuppet 7y agoThe file isn't going to be used after it's closed. The string from the file is going to be used after the file is closed. But with lazy IO, you don't have (all of) the string from the file yet, even though you've "read" it. That is, the abstractions don't do what non-Haskell abstractions would lead you to expect.
- PhineasRex 7y agoYou're absolutely correct however Haskell has a couple of function that let you circumvent it's normal evaluation rules which you can see in https://hackage.haskell.org/package/base-4.12.0.0/docs/System-IO-Unsafe.html#v:unsafePerformIO https://hackage.haskell.org/package/base-4.12.0.0/docs/Syste.... 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.
- uryga 7y agorelevant quote from the linked docs: > `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`.
- tikhonj 7y agoLazy IO refers to a set of functions like readFile that explicitly break that abstraction, tying observable IO to the order expressions are evaluated. Under the hood, the lazy IO functions rely on a GHC function called unsafeInterleaveIO, whose name hints at how it works and that it's not safe. The fact that lazy IO behaves so unintuitively and causes significant problems strongly signals that that particular abstraction shouldn't be broken.