Really, I think a lot of the confusion about functional programming comes from a lot of programmers beginning in Python or Java, neither of which initially supported tail recursion. Therefore, the idea of programming without for or while loops seems alien.
So it might be a good idea to show a practical example of how Haskell does IO and a main loop at the same time: here is an example of an echo function. If you call it at any point in the main function of a command line program, it will prompt you for text, and write back what you wrote:
Code: Select all
let spam = readLn >>= putStr >> putStr "\n" >> spam
When you call the function, it calls readLn which prompts you for a string. The output is then shoved by the bind operator >>= (think of it like a unix pipe) into putStr, which prints out what you wrote. The >> bind operator is a dummy pipe that schedules an operation after the previous one without passing the outputs to inputs. We used it to schedule a newline to make our program look better, and after that the function calls itself, which loops us back to the beginning.
the >>= and >> pipes are
the IO monad by the way. That's it. Doesn't look too weird when you describe it that way, now does it? These operators happen to satisfy the monad axioms, but you don't need to know that to use them. It just means they'll magically behave super intuitively and just work
whenever you use them because of their deep properties.
The "have the function call itself to make the main IO loop" is one reason why tail recursion is more important in Haskell than in iterative languages: tail recursion is how you handle iterative problems in Haskell. Tail recursive functions behave iteratively. Instead of using a given loop function like for or while, you have the power to make one yourself with a tool (tail recursion) which is of comparable power to a goto.
The >>= and >> operators allow you to schedule IO-related operations, because the compiler sees that "the part on the right depends on the part on the left", so it executes the part on the left first. They allow sandboxing IO from pure functions because (up to syntactic sugar) they are the only way to pass IO data to and between pure functions. Together with tail recursion(possibly calling other functions instead of itself) you can use this to write any iterative IO program.
I think that the statement "there are no side effects in Haskell" scares people off unnecessarily. You can do a lot of side-effect-ish stuff, you just have to make it explicit in the syntax and the variable/function types of your code. You schedule events using data flow instead of time-ordering execution by line number.