4 ms·
Can someone tell me a positive reason to do I/O with monads or try to make it pure-functional? It's always smelled like zealotry to me -- I/O is, definitionall
by refenestrator 6y ago
Can someone tell me a positive reason to do I/O with monads or try to make it pure-functional?
It's always smelled like zealotry to me -- I/O is, definitionally, about creating a side-effect, and papering over that seems to add complexity and unnecessary concepts before eventually just calling the same libc functions anyways at the bottom of the stack.
- Rerarom 6y agoI once asked a FP enthusiast and the answer was it makes formal reasoning about the code easier.
- endgame 6y agoAs a small demonstration of the principle: https://github.com/system-f/fp-course#demonstrate-io-maintains-referential-transparency https://github.com/system-f/fp-course#demonstrate-io-maintai... Scroll past the Haskell example for a moment and look at the Python. The program p2 is the same as p1, except a refactoring is performed: the call to readFile(file) is DRY'd into a variable, and this changes the meaning of the program. In the Haskell example, the same refactoring is performed. Because IO actions are treated as values in their own right, the refactoring does not change the meaning of the program. Executing p1 or p2 will have the same effect.
- ilikebits 6y agoLet's separate two related but distinct concepts here: (1) effect tracking, and (2) monadic IO. --- The concept you're asking about is called "effect tracking", and the argument for effect tracking is very similar to the argument for static types. In a world without static types, functions can be passed values of any type, and can return values of any type. Adding static types makes this code easier to reason about because we can statically enforce (i.e. enforce without needing any runtime checks) that functions only take values of a certain type or return values of a certain type. This makes reasoning about the code easier because it limits the possible things that a function can take or return, so instead of thinking about "what happens if I provide any kind of value to this function?", you can think "I know this function takes an integer, so I only need to worry about understanding how it behaves when provided something that is an integer". In a world without effect tracking (this is most programming languages today), functions can perform any side effect. Adding effect tracking makes this code easier to reason about because we can statically enforce which side effects a function does. For example, effect tracking makes it possible to express "this function takes a callback function as an argument, but that callback argument is not allowed to print anything". Alternatively, you can also look at a function and know for a fact that might do IO, or that it doesn't do IO. When you're debugging, this helps narrow down the scope of places that could be doing something wrong, because now you'll never have a function that _looks_ like it's doing something innocuous, but is secretly writing a log statement to a file, or pinging an API over the network, or drawing something to your GUI window. --- Monads are a way to _implement_ effect tracking within an existing type system, rather than inventing a separate, orthogonal "effect tracking system". This allows us to reuse a lot of the existing tools and theories that we understand about types and type theory, and apply them to how we do effect tracking. Exactly how this implementation works is another topic that gets a bit involved. But note: (1) there are ways to do effect tracking that are _not_ monads, or that do _not_ integrate with the type system that the language uses for values; and (2) monads have applications that are _not_ effect tracking.
- omniscient_oce 6y agoI'm no expert but another example of a non-monadic implementation would be OCaml's new algebraic effects as part of the Multicore project, right?
- Sonata 6y agoThe important thing is that it separates the order of side-effects from the order of evaluation of expressions. This isn't a particularly important distinction in an imperative language, but in a functional language when you introduce things like laziness and memoization, the order in which expressions will be evaluated is not obvious. Because pure I/O preserves referential transparency, it gives the compiler far more ability the optimize the code by changing how, when and whether things are evaluated, while still being able to prove that it has not modified the external behaviour of the program. Haskell doesn't use pure I/O out of zealotry - without it, Haskell couldn't and be lazy-by-default and the compiled code would probably be much slower.
- refenestrator 6y agoThank you, that's a great, practical answer. "We have to do this to make lazy evaluation, memoization, etc work" is very sensible and not ideological at all. That makes sense.
- loopz 6y agoIt's also functional core-imperative shell built-in, and optimized for functional expressions.
- nerdponx 6y agoI'd like to add to the other replies that first-class i/o is less about "papering over" side effects, and more about reining in exactly where side-effects can occur, the kinds of side effects that are allowed, and requiring a full enumeration of possible outcomes (resource exhaustion, you don't have permission, etc). Kind of like checked exceptions, in a way.