Difference between revisions of "Haskell IO for Imperative Programmers"

From HaskellWiki
Jump to navigation Jump to search
(resurrected an ancient tutorial)
 
m
 
(5 intermediate revisions by 2 users not shown)
Line 11: Line 11:
 
</haskell>
 
</haskell>
   
About the only way that could be easier would be if there were a “Hello, world!” primitive built into the language, and that would be silly. Stop and think about the basic premise — the whole point of running a program is to have side-effects — no one in their right mind is going to design a language that makes that hard to do. Who here thinks Erik Meijer is a nincompoop? Show of hands? It’s not like he’s brilliant as long as he’s working on VB and then turns into a moron the minute he starts thinking about Haskell.
+
About the only way that could be easier would be if there were a <code>“Hello, world!”</code> primitive built into the language, and that would be silly. Stop and think about the basic premise — the whole point of running a program is to have side-effects — no one in their right mind is going to design a language that makes that hard to do. Who here thinks Erik Meijer is a nincompoop? Show of hands? It’s not like he’s brilliant as long as he’s working on VB and then turns into a moron the minute he starts thinking about Haskell.
   
Before proceeding please allow yourself to consider the possibility that IO in Haskell might not be difficult. That said, I will concede that it is also not obvious, but for reasons that don’t have anything to do with category theory. There is a fundamental tension in functional languages. On the one hand you must have side-effects in order to justify all the oxygen you’re consuming, but on the other hand you don’t want side effects because they make it harder to write correct and composable programs. The way to resolve that tension is to think differently. Instead of thinking about IO as something you do, you need to shift gears and think about IO as a result that you achieve. The programming task then becomes one of describing the program’s interaction with the outside world. (That turns out to be a much more powerful idea, the full consequences of which are beyond the scope of this post.) The catch, of course, is that “thinking differently” is one of those things that much easier to say than to do.
+
Before proceeding please allow yourself to consider the possibility that I/O in Haskell might not be difficult. That said, I will concede that it is also not obvious, but for reasons that don’t have anything to do with category theory. There is a fundamental tension in functional languages:
  +
* On the one hand you must have side-effects in order to justify all the oxygen you’re consuming,
  +
* but on the other hand you don’t want side effects because they make it harder to write correct and composable programs.
  +
  +
The way to resolve that tension is to think differently. Instead of thinking about I/O as something you do, you need to shift gears and think about I/O as a result that you achieve. The programming task then becomes one of describing the program’s interaction with the outside world. (That turns out to be a much more powerful idea, the full consequences of which are beyond the scope of this post.) The catch, of course, is that “thinking differently” is one of those things that much easier to say than to do.
   
 
== “Hello, world!” Reloaded ==
 
== “Hello, world!” Reloaded ==
   
Returning to “Hello, world!” — it’s cute, but it’s not a practical example. It doesn’t do any useful work. What is “useful work”? Your most basic utility program (say, to analyze a log file) needs to read some input line-by-line, process it, and write some output. Anything less than that is a waste of time. Coming from an imperative background you’re used to writing programs with this basic template
+
Returning to <code>“Hello, world!”</code> — it’s cute, but it’s not a practical example. It doesn’t do any useful work. What is “useful work”? Your most basic utility program (say, to analyze a log file) needs to read some input line-by-line, process it, and write some output. Anything less than that is a waste of time. Coming from an imperative background you’re used to writing programs with this basic template
   
<code><pre>
+
<pre>
 
while (<>) {
 
while (<>) {
 
&processIt($_);
 
&processIt($_);
 
}
 
}
</pre></code>
+
</pre>
   
 
So you naturally expect to write something in Haskell that looks like this
 
So you naturally expect to write something in Haskell that looks like this
Line 34: Line 38:
 
</haskell>
 
</haskell>
   
This will work (mostly), but you’re already in trouble. You just don’t know it yet. There are a couple of problems here, so let’s examine the thinking behind it. First, you’ve written a recursive function. You did that because you know Haskell doesn’t have any looping constructs, which is annoying but no big deal so you get over it and get on with your work. Second, there is a very real muddying of concerns. There’s a routine that does input that calls a processing routine that, by implication, if the program is to actually do anything, must be calling an output routine. These are separate issues that should be handled separately, but it’s not clear at this stage how to accomplish that.
+
This will work (mostly), but you’re already in trouble. You just don’t know it yet. There are a couple of problems here, so let’s examine the thinking behind it:
  +
* First, you’ve written a recursive function. You did that because you know Haskell doesn’t have any looping constructs, which is annoying but no big deal so you get over it and get on with your work.
  +
* Second, there is a very real muddying of concerns. There’s a routine that does input that calls a processing routine that, by implication, if the program is to actually do anything, must be calling an output routine. These are separate issues that should be handled separately, but it’s not clear at this stage how to accomplish that.
   
Let’s talk about the recursion. Recursion can be used, among other things, to express an iteration. I have often stated that you simply don’t write iterative constructs in Haskell. That’s not literally true, I’ve written almost as many as I can count on one hand, but it’s certainly not something you do every day. Not the way every third statement is a for or while in an imperative language. It’s all handled by library functions. If you define a recursive data structure you will provide a collection of routines that iterate over it and no one else ever needs to know the details. If you’re writing an “iterative” program that doesn’t involve an explicit data structure, odds are it can be expressed as an operation over a list
+
Let’s talk about the recursion. Recursion can be used, among other things, to express an iteration. I have often stated that you simply don’t write iterative constructs in Haskell. That’s not literally true, I’ve written almost as many as I can count on one hand, but it’s certainly not something you do every day. Not the way every third statement is a <code>for</code> or <code>while</code> in an imperative language. It’s all handled by library functions. If you define a recursive data structure you will provide a collection of routines that iterate over it and no one else ever needs to know the details. If you’re writing an “iterative” program that doesn’t involve an explicit data structure, odds are it can be expressed as an operation over a list
   
 
<haskell>
 
<haskell>
Line 58: Line 64:
 
</haskell>
 
</haskell>
   
I could go on about monadic contexts and encapsulation and how once you enter a monad you don’t come out… but I won’t, because I’d like you to continue reading. Instead I’d like to call your attention to something I call “IO creep”. <hask>print</hask> lives inside <hask>IO</hask> and therefore, in this example, <hask>processIt</hask> also lives inside <hask>IO</hask>. There’s no way around it, the type signatures just make it obvious. This is the type system’s way of trying to sound warning bells. Some things need to happen inside <hask>IO</hask>, do them there, but don’t do anything else. Haskell provides the tools you need to factor your code appropriately and avoid this sort of muddying of concerns. If you see <hask>IO</hask> starting to creep throughout your program, that’s another sign that you’re thinking imperatively. Stop and refactor.
+
I could go on about monadic contexts and encapsulation and how once you enter a monad you don’t come out...but I won’t, because I’d like you to continue reading. Instead I’d like to call your attention to something I call “IO creep”. <code>print</code> lives inside <code>IO</code> and therefore, in this example, <code>processIt</code> also lives inside <code>IO</code>. There’s no way around it, the type signatures just make it obvious. This is the type system’s way of trying to sound warning bells. Some things need to happen inside <code>IO</code>, do them there, but don’t do anything else. Haskell provides the tools you need to factor your code appropriately and avoid this sort of muddying of concerns. If you see <code>IO</code> starting to creep throughout your program, that’s another sign that you’re thinking imperatively. Stop and refactor.
   
So where are we? We wrote the obvious program to try to actually do something useful with this !@#$% language and instead of saying “see, it’s just as easy as Perl” I told you that it contained two big flashing red lights warning us we’re not using the language correctly. Disgusting, isn’t it?
+
So where are we? We wrote the obvious program to try to actually do something useful with this '''''!@#$%''''' language and instead of saying “see, it’s just as easy as Perl” I told you that it contained two big flashing red lights warning us we’re not using the language correctly. Disgusting, isn’t it?
   
 
It’s important to realize at this point that the problem isn’t monads. I won’t lie to you, monads are not the easiest thing in the world to wrap your head around and you will need to grok them someday in order to become an effective Haskell programmer, but they don’t even come into the picture at this point. I only mention them because all the other tutorials do and so you’re expecting it. That said, forget about them.
 
It’s important to realize at this point that the problem isn’t monads. I won’t lie to you, monads are not the easiest thing in the world to wrap your head around and you will need to grok them someday in order to become an effective Haskell programmer, but they don’t even come into the picture at this point. I only mention them because all the other tutorials do and so you’re expecting it. That said, forget about them.
   
The problem is imperative-think. We wrote an imperative program in Haskell. See, it is possible to write imperative programs in Haskell! Easy, even. Now that you know you can, don’t. But it does no good for me to say, “stop thinking imperatively.” Habits of thought die hard. I have to give you something to replace it with. Haskell provides the means for you to organize your programs in new and exciting ways that really bear very little resemblance to what you’re used to doing in imperative languages. That could be a book. For right now, though, one of the things that makes IO seem difficult at first glance is a failure to appreciate the virtues of laziness.
+
The problem is imperative-think. We wrote an imperative program in Haskell. See, it is possible to write imperative programs in Haskell! Easy, even. Now that you know you can, don’t. But it does no good for me to say, “stop thinking imperatively.” Habits of thought die hard. I have to give you something to replace it with. Haskell provides the means for you to organize your programs in new and exciting ways that really bear very little resemblance to what you’re used to doing in imperative languages. That could be a book. For right now, though, one of the things that makes I/O seem difficult at first glance is a failure to appreciate the virtues of laziness.
   
 
== Laziness ==
 
== Laziness ==
Line 70: Line 76:
 
I’m sure you’ve seen some attempts to explain laziness by way of apparently contrived examples involving infinite lists, but if you’re coming to Haskell from an imperative background you don’t get it. Trust me, you don’t. It is a simple idea with far-reaching implications and it is one of the defining features of the language. After learning Haskell, I’d rather gnaw off my own arm than try to live without laziness. So, what is it?
 
I’m sure you’ve seen some attempts to explain laziness by way of apparently contrived examples involving infinite lists, but if you’re coming to Haskell from an imperative background you don’t get it. Trust me, you don’t. It is a simple idea with far-reaching implications and it is one of the defining features of the language. After learning Haskell, I’d rather gnaw off my own arm than try to live without laziness. So, what is it?
   
First let’s deal with the notion of strictness. A language is strict if it evaluates the arguments to a function before it evaluates the function. A non-strict language doesn’t. For example, define
+
First let’s deal with the notion of ''strictness'':
  +
* A language is '''strict''' if it evaluates the arguments to a function before it evaluates the function.
  +
* A '''non-strict''' language doesn’t. For example, define
   
 
<haskell>
 
<haskell>
Line 92: Line 100:
 
Other languages have the occasional non-strict construct. Short-circuit booleans and ternary operators are very common and they let you write code such as
 
Other languages have the occasional non-strict construct. Short-circuit booleans and ternary operators are very common and they let you write code such as
   
<code><pre>
+
<pre>
 
if (i < x.size() && x[i] > 0) {
 
if (i < x.size() && x[i] > 0) {
</pre></code>
+
</pre>
   
which would otherwise explode. It’s a lot more convenient than nested ifs. Many languages let you create your own non-strict constructs, generally with macros,
+
which would otherwise explode. It’s a lot more convenient than nested <code>if</code>s. Many languages let you create your own non-strict constructs, generally with macros,
   
<code><pre>
+
<pre>
 
#define first(x, a, b) if (a > 0) x = a; else x = b
 
#define first(x, a, b) if (a > 0) x = a; else x = b
   
 
first(x, a++, b++);
 
first(x, a++, b++);
</pre></code>
+
</pre>
 
 
In this horribly contrived example, <code>b</code> may never be incremented, depending on the value of <code>a</code>. That is, presumably, the programmer’s intent. There is also the obvious bug that <code>a</code> may well get incremented twice. Presumably, not the programmer’s intent. The example is deliberately sloppy to leave the door open for all kinds of other bugs the details of which aren’t really relevant. The point is that, in an imperative language, this kind of thing is very dangerous. This is why the use of macros is generally discouraged. The “solution” here is to use an inline function (or Lisp). The inline would eliminate all the bugs, but it would also enforce strict semantics — that is, it will fail to do the thing we had hoped to achieve by using a macro in the first place.
 
In this horribly contrived example, <code>b</code> may never be incremented, depending on the value of <code>a</code>. That is, presumably, the programmer’s intent. There is also the obvious bug that <code>a</code> may well get incremented twice. Presumably, not the programmer’s intent. The example is deliberately sloppy to leave the door open for all kinds of other bugs the details of which aren’t really relevant. The point is that, in an imperative language, this kind of thing is very dangerous. This is why the use of macros is generally discouraged. The “solution” here is to use an inline function (or Lisp). The inline would eliminate all the bugs, but it would also enforce strict semantics — that is, it will fail to do the thing we had hoped to achieve by using a macro in the first place.
   
  +
There are a couple of points here:
There are a couple of points here. One is that while the notion of a non-strict operation may not seem mind-bending, coming from an imperative language it’s something you haven’t used much and therefore haven’t really internalized. The other is the relationship between laziness and side-effects. Clearly, they’re two great tastes that don’t go well together, like macaroni and blue-cheese dressing. Haskell can’t have side effects because the laziness ensures that you don’t know what’s happening or when. That’s a great thing! Wonderful! There’s no reason to get bogged down in those sorts of low-level details unless you absolutely need to.
 
  +
* One is that while the notion of a non-strict operation may not seem mind-bending, coming from an imperative language it’s something you haven’t used much and therefore haven’t really internalized.
  +
* The other is the relationship between laziness and side-effects. Clearly, they’re two great tastes that don’t go well together, like macaroni and blue-cheese dressing.
  +
  +
Haskell can’t have side effects because the laziness ensures that you don’t know what’s happening or when. That’s a great thing! Wonderful! There’s no reason to get bogged down in those sorts of low-level details unless you absolutely need to.
   
== IO ==
+
== I/O ==
   
What does this have to do with IO? While Hoogle-ing to find the <hask>getLine</hask> routine used in our original program you may have run across the functions <hask>getContents</hask> and <hask>lines</hask>. <hask>getContents</hask> “returns all user input as a single string” and <hask>lines</hask> “breaks a string up into a list of strings at newline characters”. It’s clear that you could easily get a list of all the lines in the input using
+
What does this have to do with I/O? While Hoogle-ing to find the <code>getLine</code> routine used in our original program you may have run across the functions <code>getContents</code> and <code>lines</code>. <code>getContents</code> “returns all user input as a single string” and <code>lines</code> “breaks a string up into a list of strings at newline characters”. It’s clear that you could easily get a list of all the lines in the input using
   
 
<haskell>
 
<haskell>
Line 118: Line 129:
 
It’s the equivalent of
 
It’s the equivalent of
   
<code><pre>
+
<pre>
 
@lines = <>;
 
@lines = <>;
</pre></code>
+
</pre>
   
 
But no way do you want to do that. Slurp in and split an entire file, not knowing how big it is? That’s crazy talk, man!
 
But no way do you want to do that. Slurp in and split an entire file, not knowing how big it is? That’s crazy talk, man!
Line 126: Line 137:
 
Or is it?
 
Or is it?
   
It’s probably clear from context that, no, it’s not. <hask>lines</hask> returns the lines from its input on demand. <hask>getContents</hask> reads from the input on demand. Every piece of the puzzle is doing its job as lazily as it possibly can. Even though you’ve written code that looks like it’s constructing a huge list of strings in memory, that’s not what’s happening. You’ve actually written ''instructions for generating a sequence of strings as you need them.''
+
It’s probably clear from context that, no, it’s not. <code>lines</code> returns the lines from its input on demand. <code>getContents</code> reads from the input on demand. Every piece of the puzzle is doing its job as lazily as it possibly can. Even though you’ve written code that looks like it’s constructing a huge list of strings in memory, that’s not what’s happening. You’ve actually written ''instructions for generating a sequence of strings as you need them.''
   
 
So rather than writing code that interleaves input with processing with output as you’re used to, you can write code that does input and then does processing and then does output, and all the messy interleaving is handled by the lazy semantics of the language. It’s as if you’ve defined a collection of cooperating processes, producers and consumers, all running in lockstep. The solution is much more modular, and your program will still run in constant space and linear time. It’s the closest thing to magic that I know of.
 
So rather than writing code that interleaves input with processing with output as you’re used to, you can write code that does input and then does processing and then does output, and all the messy interleaving is handled by the lazy semantics of the language. It’s as if you’ve defined a collection of cooperating processes, producers and consumers, all running in lockstep. The solution is much more modular, and your program will still run in constant space and linear time. It’s the closest thing to magic that I know of.
Line 141: Line 152:
 
</haskell>
 
</haskell>
   
We’ve taken <hask>processIt</hask> out of the <hask>IO</hask> monad. Now, because functions are first-class values
+
We’ve taken <code>processIt</code> out of the <code>IO</code> monad. Now, because functions are first-class values
   
 
<haskell>
 
<haskell>
Line 151: Line 162:
 
</haskell>
 
</haskell>
   
And because <hask>interact</hask> is actually a library function
+
And because <code>interact</code> is actually a library function
   
 
<haskell>
 
<haskell>
Line 165: Line 176:
 
</haskell>
 
</haskell>
   
That’s an idiomatic Haskell program for doing simple IO. You can find several more good examples [http://haskell.org/haskellwiki/Simple_Unix_tools here]. To write these programs you don’t have to know a thing about monads. You do have to rethink your overall strategy for solving the problem. No more can you think about reading this and writing that, and all of that busy, busy “doing stuff.” Now you need to think in terms of describing the overall result that you want. That’s hard. Or, rather, it requires some practice. Once you’ve succeeded in thinking clearly in terms of the result you want to achieve without letting yourself get bogged down in the details of how to go about it, step by step, then the mechanics of making it happen are, frankly, trivial.
+
That’s an idiomatic Haskell program for doing simple I/O. You can find several more good examples [[Simple_Unix_tools|here]]. To write these programs you don’t have to know a thing about monads. You do have to rethink your overall strategy for solving the problem. No more can you think about reading this and writing that, and all of that busy, busy “doing stuff.” Now you need to think in terms of describing the overall result that you want. That’s hard. Or, rather, it requires some practice. Once you’ve succeeded in thinking clearly in terms of the result you want to achieve without letting yourself get bogged down in the details of how to go about it, step by step, then the mechanics of making it happen are, frankly, trivial.
   
 
== More ==
 
== More ==
   
  +
I’m anticipating two possible reactions to the above code:
I’m anticipating two possible reactions to the above code. You may think that it looks so simple that you have a hard time believing that it actually does anything. It’s deceptive, because the end result is actually easier than the corresponding code in an imperative language. Probably not what you were expecting when you came here. If this is the case, then reread the last few code blocks until you can see the flow. Alternately, you may think that this still counts as trivial, not really significantly different from “Hello, world”, and you want your money back. In that case, let’s crank up the heat. The rest of this will be more examples of increasing complexity and decreasing explanation.
+
* You may think that it looks so simple that you have a hard time believing that it actually does anything. It’s deceptive, because the end result is actually easier than the corresponding code in an imperative language. Probably not what you were expecting when you came here. If this is the case, then reread the last few code blocks until you can see the flow.
  +
* Alternately, you may think that this still counts as trivial, not really significantly different from “Hello, world”, and you want your money back. In that case, let’s crank up the heat. The rest of this will be more examples of increasing complexity and decreasing explanation.
   
 
There’s more to life than standard input. Can’t we let the user specify input files on the command-line?
 
There’s more to life than standard input. Can’t we let the user specify input files on the command-line?
   
 
<haskell>
 
<haskell>
  +
import System.Environment
  +
 
main = do
 
main = do
 
args <- getArgs
 
args <- getArgs
Line 186: Line 201:
   
 
<haskell>
 
<haskell>
  +
import System.Environment
  +
 
main = do
 
main = do
 
args <- getArgs
 
args <- getArgs
Line 195: Line 212:
 
</haskell>
 
</haskell>
   
This is a hard program to write. It’s hard because never in a million years would you dream of opening an arbitrary list of files, all at once, loading the entire contents of every file into memory, glomming it all into one huge freaking string, breaking that string into an equally huge list of lines (a list, not even an array!), doing something to each element of that list to produce yet another giant list, concatenating all those output lines into still another massive string, and then, finally, writing that string out. That’s insanity! Of course, that’s not at all what’s happening — it’s an abstraction made possible by laziness. As for actually doing the IO, there’s <hask>readFile</hask> and <hask>putStr</hask> and …, well, that’s it. That’s all you need to know. Monads be damned.
+
This is a hard program to write. It’s hard because never in a million years would you dream of opening an arbitrary list of files, all at once, loading the entire contents of every file into memory, glomming it all into one huge freaking string, breaking that string into an equally huge list of lines (a list, not even an array!), doing something to each element of that list to produce yet another giant list, concatenating all those output lines into still another massive string, and then, finally, writing that string out. That’s insanity! Of course, that’s not at all what’s happening — it’s an abstraction made possible by laziness. As for actually doing the I/O, there’s <code>readFile</code> and <code>putStr</code> and …, well, that’s it. That’s all you need to know. Monads be damned.
   
 
Don’t let it bother you. Nobody ever makes their first jump.
 
Don’t let it bother you. Nobody ever makes their first jump.
   
But aren’t both these examples just so much hooey? There’s no error handling, and you never know what a user is going to type in. I suppose we could substitute for <hask>readFile</hask> with
+
But aren’t both these examples just so much hooey? There’s no error handling, and you never know what a user is going to type in. I suppose we could substitute for <code>readFile</code> with
   
 
<haskell>
 
<haskell>
Line 223: Line 240:
 
</haskell>
 
</haskell>
   
That may make you a little cross-eyed. Sorry. I’m not going to dissect it, but I do want to call your attention to the fact that it is all about the what and not at all about the how. This is an IO intensive program, but you wouldn’t know it to look at it because there’s only one word — <hask>interact</hask> — that even looks like it has anything to do with IO. You could take the argument to interact and drop it into either of the earlier programs as an argument to <hask>interactFile</hask> or <hask>interactFiles</hask> and get all the benefits of their approaches to IO, without touching the processing code. That’s what decoupling is all about.
+
That may make you a little cross-eyed. Sorry. I’m not going to dissect it, but I do want to call your attention to the fact that it is all about the what and not at all about the how. This is an I/O‑intensive program, but you wouldn’t know it to look at it because there’s only one word — <code>interact</code> — that even looks like it has anything to do with I/O. You could take the argument to interact and drop it into either of the earlier programs as an argument to <code>interactFile</code> or <code>interactFiles</code> and get all the benefits of their approaches to I/O, without touching the processing code. That’s what decoupling is all about.
   
 
So far, so good. It’s easy to see how you can do simple file processing in Haskell. This is adequate for, say, reading and summarizing log files. What about something more interesting, such as configuration files? A configuration file is likely to have some sort of include mechanism
 
So far, so good. It’s easy to see how you can do simple file processing in Haskell. This is adequate for, say, reading and summarizing log files. What about something more interesting, such as configuration files? A configuration file is likely to have some sort of include mechanism
Line 229: Line 246:
 
In an imperative language
 
In an imperative language
   
<code><pre>
+
<pre>
 
sub processFile {
 
sub processFile {
 
open FH, shift;
 
open FH, shift;
Line 241: Line 258:
 
close FH;
 
close FH;
 
}
 
}
</pre></code>
+
</pre>
   
 
vs.
 
vs.
Line 261: Line 278:
 
</haskell>
 
</haskell>
   
Two mutually recursive IO functions… You’ll note that this isn’t a simple use of recursion to express an iteration, as even the imperative solution is naturally expressed recursively. What’s much more important is that these functions are only doing IO. Processing is still “somewhere else,” not buried three levels deep inside an IO routine. This means that our new include file mechanism can be freely mixed with all the other techniques we’ve been developing with no loss of modularity.
+
Two mutually‑recursive I/O functions...you’ll note that this isn’t a simple use of recursion to express an iteration, as even the imperative solution is naturally expressed recursively. What’s much more important is that these functions are only doing I/O. Processing is still “somewhere else,” not buried three levels deep inside an I/O routine. This means that our new include file mechanism can be freely mixed with all the other techniques we’ve been developing with no loss of modularity.
   
 
== Monads are Gravy ==
 
== Monads are Gravy ==
   
The thing that makes IO in Haskell seem hard is learning to think in terms of a pure, lazy, functional language. If you can unlearn some old habits of thought it can be even easier than what you’re used to doing in imperative languages. I realize that’s like saying you can catch a bird if you put salt on it’s tail so I’ve tried to provide some help in that direction.
+
The thing that makes I/O in Haskell seem hard is learning to think in terms of a pure, lazy, functional language. If you can unlearn some old habits of thought it can be even easier than what you’re used to doing in imperative languages. I realize that’s like saying you can catch a bird if you put salt on its tail so I’ve tried to provide some help in that direction.
   
The secret to success is to start thinking about IO as a result that you achieve rather than something that you do. Monadic IO is a formalization of that idea. We didn’t cover monads because they obscure the real challenges of just learning to think functionally, but they’re there to make your life easier. They add more power, letting you think about actions as tangible objects that you can manipulate and even letting you define your own control structures. You don’t need all of that power in order to just get started writing practical programs and doing useful work. Using just the techniques covered above (and maybe a dash of [http://www.serpentine.com/blog/2007/02/27/a-haskell-regular-expression-tutorial/ regular expressions]) you can start to rock and roll.
+
The secret to success is to start thinking about I/O as a result that you achieve rather than something that you do. Monadic I/O is a formalization of that idea. We didn’t cover monads because they obscure the real challenges of just learning to think functionally, but they’re there to make your life easier. They add more power, letting you think about actions as tangible objects that you can manipulate and even letting you define your own control structures. You don’t need all of that power in order to just get started writing practical programs and doing useful work. Using just the techniques covered above (and maybe a dash of [http://www.serpentine.com/blog/2007/02/27/a-haskell-regular-expression-tutorial/ regular expressions]) you can start to rock and roll.

Latest revision as of 06:35, 5 January 2022


My goal is to help experienced procedural programmers make the transition to Haskell, not to provide a tour of language features. I don’t want to show you how to do imperative programming in Haskell; I want to show you how not to.

Introduction

There is a widespread misconception that basic input and output is hard to do in Haskell and that therefore the language is not suitable for writing practical programs. I’ve even heard it stated that one must master category theory in order to so much as write to standard output. Nothing could be further from the truth. Witness,

main = putStrLn "Hello, world!"

About the only way that could be easier would be if there were a “Hello, world!” primitive built into the language, and that would be silly. Stop and think about the basic premise — the whole point of running a program is to have side-effects — no one in their right mind is going to design a language that makes that hard to do. Who here thinks Erik Meijer is a nincompoop? Show of hands? It’s not like he’s brilliant as long as he’s working on VB and then turns into a moron the minute he starts thinking about Haskell.

Before proceeding please allow yourself to consider the possibility that I/O in Haskell might not be difficult. That said, I will concede that it is also not obvious, but for reasons that don’t have anything to do with category theory. There is a fundamental tension in functional languages:

  • On the one hand you must have side-effects in order to justify all the oxygen you’re consuming,
  • but on the other hand you don’t want side effects because they make it harder to write correct and composable programs.

The way to resolve that tension is to think differently. Instead of thinking about I/O as something you do, you need to shift gears and think about I/O as a result that you achieve. The programming task then becomes one of describing the program’s interaction with the outside world. (That turns out to be a much more powerful idea, the full consequences of which are beyond the scope of this post.) The catch, of course, is that “thinking differently” is one of those things that much easier to say than to do.

“Hello, world!” Reloaded

Returning to “Hello, world!” — it’s cute, but it’s not a practical example. It doesn’t do any useful work. What is “useful work”? Your most basic utility program (say, to analyze a log file) needs to read some input line-by-line, process it, and write some output. Anything less than that is a waste of time. Coming from an imperative background you’re used to writing programs with this basic template

while (<>) {
  &processIt($_);
}

So you naturally expect to write something in Haskell that looks like this

main = do
  line <- getLine
  processIt line
  main

This will work (mostly), but you’re already in trouble. You just don’t know it yet. There are a couple of problems here, so let’s examine the thinking behind it:

  • First, you’ve written a recursive function. You did that because you know Haskell doesn’t have any looping constructs, which is annoying but no big deal so you get over it and get on with your work.
  • Second, there is a very real muddying of concerns. There’s a routine that does input that calls a processing routine that, by implication, if the program is to actually do anything, must be calling an output routine. These are separate issues that should be handled separately, but it’s not clear at this stage how to accomplish that.

Let’s talk about the recursion. Recursion can be used, among other things, to express an iteration. I have often stated that you simply don’t write iterative constructs in Haskell. That’s not literally true, I’ve written almost as many as I can count on one hand, but it’s certainly not something you do every day. Not the way every third statement is a for or while in an imperative language. It’s all handled by library functions. If you define a recursive data structure you will provide a collection of routines that iterate over it and no one else ever needs to know the details. If you’re writing an “iterative” program that doesn’t involve an explicit data structure, odds are it can be expressed as an operation over a list

fac n = product [1 .. n]

Writing Haskell you want to learn to think in terms of operations on aggregates: “do this to that collection.” If you’re sweating the details then you’re probably thinking procedurally. That’s why if you write an iteration it should be taken as a warning sign that you’re not using the language appropriately. Stop and think.

As for separation of concerns, let’s flesh out the example a little more and add type signatures.

main :: IO ()
main = do
  line <- getLine
  processIt line
  main

processIt   :: String -> IO ()
processIt s = do
  print (length s)

I could go on about monadic contexts and encapsulation and how once you enter a monad you don’t come out...but I won’t, because I’d like you to continue reading. Instead I’d like to call your attention to something I call “IO creep”. print lives inside IO and therefore, in this example, processIt also lives inside IO. There’s no way around it, the type signatures just make it obvious. This is the type system’s way of trying to sound warning bells. Some things need to happen inside IO, do them there, but don’t do anything else. Haskell provides the tools you need to factor your code appropriately and avoid this sort of muddying of concerns. If you see IO starting to creep throughout your program, that’s another sign that you’re thinking imperatively. Stop and refactor.

So where are we? We wrote the obvious program to try to actually do something useful with this !@#$% language and instead of saying “see, it’s just as easy as Perl” I told you that it contained two big flashing red lights warning us we’re not using the language correctly. Disgusting, isn’t it?

It’s important to realize at this point that the problem isn’t monads. I won’t lie to you, monads are not the easiest thing in the world to wrap your head around and you will need to grok them someday in order to become an effective Haskell programmer, but they don’t even come into the picture at this point. I only mention them because all the other tutorials do and so you’re expecting it. That said, forget about them.

The problem is imperative-think. We wrote an imperative program in Haskell. See, it is possible to write imperative programs in Haskell! Easy, even. Now that you know you can, don’t. But it does no good for me to say, “stop thinking imperatively.” Habits of thought die hard. I have to give you something to replace it with. Haskell provides the means for you to organize your programs in new and exciting ways that really bear very little resemblance to what you’re used to doing in imperative languages. That could be a book. For right now, though, one of the things that makes I/O seem difficult at first glance is a failure to appreciate the virtues of laziness.

Laziness

I’m sure you’ve seen some attempts to explain laziness by way of apparently contrived examples involving infinite lists, but if you’re coming to Haskell from an imperative background you don’t get it. Trust me, you don’t. It is a simple idea with far-reaching implications and it is one of the defining features of the language. After learning Haskell, I’d rather gnaw off my own arm than try to live without laziness. So, what is it?

First let’s deal with the notion of strictness:

  • A language is strict if it evaluates the arguments to a function before it evaluates the function.
  • A non-strict language doesn’t. For example, define
f x y = x * y

In a strict language a call to f would be evaluated as

f (1+2) (3+4) = f 3 7 = 3 * 7 = 21

In a non-strict language that same call would be evaluated as

f (1+2) (3+4) = (1+2) * (3+4) = 3 * 7 = 21

It looks like a small difference. To say that Haskell is lazy means that it is aggressively non-strict — it never evaluates anything before it absolutely needs to (unless you tell it otherwise, but that’s another story). This changes the way you code.

Other languages have the occasional non-strict construct. Short-circuit booleans and ternary operators are very common and they let you write code such as

if (i < x.size() && x[i] > 0) {

which would otherwise explode. It’s a lot more convenient than nested ifs. Many languages let you create your own non-strict constructs, generally with macros,

#define first(x, a, b) if (a > 0) x = a; else x = b

first(x, a++, b++);

In this horribly contrived example, b may never be incremented, depending on the value of a. That is, presumably, the programmer’s intent. There is also the obvious bug that a may well get incremented twice. Presumably, not the programmer’s intent. The example is deliberately sloppy to leave the door open for all kinds of other bugs the details of which aren’t really relevant. The point is that, in an imperative language, this kind of thing is very dangerous. This is why the use of macros is generally discouraged. The “solution” here is to use an inline function (or Lisp). The inline would eliminate all the bugs, but it would also enforce strict semantics — that is, it will fail to do the thing we had hoped to achieve by using a macro in the first place.

There are a couple of points here:

  • One is that while the notion of a non-strict operation may not seem mind-bending, coming from an imperative language it’s something you haven’t used much and therefore haven’t really internalized.
  • The other is the relationship between laziness and side-effects. Clearly, they’re two great tastes that don’t go well together, like macaroni and blue-cheese dressing.

Haskell can’t have side effects because the laziness ensures that you don’t know what’s happening or when. That’s a great thing! Wonderful! There’s no reason to get bogged down in those sorts of low-level details unless you absolutely need to.

I/O

What does this have to do with I/O? While Hoogle-ing to find the getLine routine used in our original program you may have run across the functions getContents and lines. getContents “returns all user input as a single string” and lines “breaks a string up into a list of strings at newline characters”. It’s clear that you could easily get a list of all the lines in the input using

do { s <- getContents ; return $ lines s }

It’s the equivalent of

@lines = <>;

But no way do you want to do that. Slurp in and split an entire file, not knowing how big it is? That’s crazy talk, man!

Or is it?

It’s probably clear from context that, no, it’s not. lines returns the lines from its input on demand. getContents reads from the input on demand. Every piece of the puzzle is doing its job as lazily as it possibly can. Even though you’ve written code that looks like it’s constructing a huge list of strings in memory, that’s not what’s happening. You’ve actually written instructions for generating a sequence of strings as you need them.

So rather than writing code that interleaves input with processing with output as you’re used to, you can write code that does input and then does processing and then does output, and all the messy interleaving is handled by the lazy semantics of the language. It’s as if you’ve defined a collection of cooperating processes, producers and consumers, all running in lockstep. The solution is much more modular, and your program will still run in constant space and linear time. It’s the closest thing to magic that I know of.

That’s what laziness buys you. Laziness enables a new kind of modularity that you may not have seen before. With laziness, you can easily separate generation from processing. When generation and processing are less tightly coupled both are freer to evolve. Let’s evolve the example.

main = do
  s <- getContents
  let r = map processIt (lines s)
  putStr (unlines r)

processIt s = show (length s)

We’ve taken processIt out of the IO monad. Now, because functions are first-class values

main = interact (unlines . map processIt . lines)

interact f = do
  s <- getContents
  putStr (f s)

And because interact is actually a library function

main = interact (unlines . map processIt . lines)

or

main = io (map processIt)

io f = interact (unlines . f . lines)

That’s an idiomatic Haskell program for doing simple I/O. You can find several more good examples here. To write these programs you don’t have to know a thing about monads. You do have to rethink your overall strategy for solving the problem. No more can you think about reading this and writing that, and all of that busy, busy “doing stuff.” Now you need to think in terms of describing the overall result that you want. That’s hard. Or, rather, it requires some practice. Once you’ve succeeded in thinking clearly in terms of the result you want to achieve without letting yourself get bogged down in the details of how to go about it, step by step, then the mechanics of making it happen are, frankly, trivial.

More

I’m anticipating two possible reactions to the above code:

  • You may think that it looks so simple that you have a hard time believing that it actually does anything. It’s deceptive, because the end result is actually easier than the corresponding code in an imperative language. Probably not what you were expecting when you came here. If this is the case, then reread the last few code blocks until you can see the flow.
  • Alternately, you may think that this still counts as trivial, not really significantly different from “Hello, world”, and you want your money back. In that case, let’s crank up the heat. The rest of this will be more examples of increasing complexity and decreasing explanation.

There’s more to life than standard input. Can’t we let the user specify input files on the command-line?

import System.Environment

main = do
  args <- getArgs
  mapM_ (interactFile (unlines . map processIt . lines)) args

interactFile f fileName = do
  s <- readFile fileName
  putStr (f s)

That processes each file individually, but maybe we’d rather do something more Perl-esque and treat all the command-line arguments as one big virtual file.

import System.Environment

main = do
  args <- getArgs
  interactFiles (unlines . map processIt . lines) args

interactFiles f fileNames = do
  ss <- mapM readFile fileNames
  putStr $ f (concat ss)

This is a hard program to write. It’s hard because never in a million years would you dream of opening an arbitrary list of files, all at once, loading the entire contents of every file into memory, glomming it all into one huge freaking string, breaking that string into an equally huge list of lines (a list, not even an array!), doing something to each element of that list to produce yet another giant list, concatenating all those output lines into still another massive string, and then, finally, writing that string out. That’s insanity! Of course, that’s not at all what’s happening — it’s an abstraction made possible by laziness. As for actually doing the I/O, there’s readFile and putStr and …, well, that’s it. That’s all you need to know. Monads be damned.

Don’t let it bother you. Nobody ever makes their first jump.

But aren’t both these examples just so much hooey? There’s no error handling, and you never know what a user is going to type in. I suppose we could substitute for readFile with

safeRead s = catch (readFile s) $ \_ -> return ""

Contrasting this with Java is left as an exercise for the reader.

Maybe line-by-line processing isn’t your thing. We could, just as an example, add up all the rows in our input

main = interact $ show . sum . map read . lines

Even by column.

main = interact $ show . foldl addRows (repeat 0) . map row . lines

addRows = zipWith (+)

row = map read . words

That may make you a little cross-eyed. Sorry. I’m not going to dissect it, but I do want to call your attention to the fact that it is all about the what and not at all about the how. This is an I/O‑intensive program, but you wouldn’t know it to look at it because there’s only one word — interact — that even looks like it has anything to do with I/O. You could take the argument to interact and drop it into either of the earlier programs as an argument to interactFile or interactFiles and get all the benefits of their approaches to I/O, without touching the processing code. That’s what decoupling is all about.

So far, so good. It’s easy to see how you can do simple file processing in Haskell. This is adequate for, say, reading and summarizing log files. What about something more interesting, such as configuration files? A configuration file is likely to have some sort of include mechanism

In an imperative language

sub processFile {
  open FH, shift;
  while () {
    if (&isInclude($_)) {
      &processFile(&includeFile($_));
    } else {
      # other stuff
    }
  }
  close FH;
}

vs.

readWithIncludes :: String -> IO [String]
readWithIncludes f = do
  s <- readFile f
  ss <- mapM expandIncludes (lines s)
  return (concat ss)

expandIncludes :: String -> IO [String]
expandIncludes s =
  if isInclude s
    then
      readWithIncludes (includeFile s)
    else
      return [s]

Two mutually‑recursive I/O functions...you’ll note that this isn’t a simple use of recursion to express an iteration, as even the imperative solution is naturally expressed recursively. What’s much more important is that these functions are only doing I/O. Processing is still “somewhere else,” not buried three levels deep inside an I/O routine. This means that our new include file mechanism can be freely mixed with all the other techniques we’ve been developing with no loss of modularity.

Monads are Gravy

The thing that makes I/O in Haskell seem hard is learning to think in terms of a pure, lazy, functional language. If you can unlearn some old habits of thought it can be even easier than what you’re used to doing in imperative languages. I realize that’s like saying you can catch a bird if you put salt on its tail so I’ve tried to provide some help in that direction.

The secret to success is to start thinking about I/O as a result that you achieve rather than something that you do. Monadic I/O is a formalization of that idea. We didn’t cover monads because they obscure the real challenges of just learning to think functionally, but they’re there to make your life easier. They add more power, letting you think about actions as tangible objects that you can manipulate and even letting you define your own control structures. You don’t need all of that power in order to just get started writing practical programs and doing useful work. Using just the techniques covered above (and maybe a dash of regular expressions) you can start to rock and roll.