I am trying to understand functional programming from first principles, yet I am stuck on the interface between the pure functional world and the impure real world that has
IO
is a data structure. E.g. here's a very simple model of IO
:
data IO a = Return a | GetLine (String -> IO a) | PutStr String (IO a)
Real IO
can be seen as being this but with more constructors (I prefer to think of all the IO
"primitives" in base
as such constructors). The main
value of a Haskell program is just a value of this data structure. The runtime (which is "external" to Haskell) evaluates main
to the first IO
constructor, then "executes" it somehow, passes any values returned back as arguments to the contained function, and then executes the resulting IO
action recursively, stopping at the Return ()
. That's it. IO
doesn't have any strange interactions with functions, and it's not actually "impure", because nothing in Haskell is impure (unless it's unsafe). There is just an entity outside of your program that interprets it as something effectful.
Thinking of functions as tables of inputs and outputs is perfectly fine. In mathematics, this is called the graph of the function, and e.g. in set theory it's often taken as the definition of a function in the first place. Functions that return IO
actions fit just fine into this model. They just return values of the data structure IO
; nothing strange about it. E.g. putStrLn
might be defined as so (I don't think it actually is, but...):
putStrLn s = PutStr (s ++ "\n") (Return ())
and readLn
could be
-- this is actually read <$> getLine; real readLn throws exceptions instead of returning bottoms
readLn = GetLine (\s -> Return (read s))
both of which have perfectly sensible interpretations when thinking of functions as graphs.
Your other question, about how to interpret higher-order functions, isn't going to get you very far. Functions are values, period. Modeling them as graphs is a good way to think about them, and in that case higher order functions look like graphs which contain graphs in their input or output columns. There's no "simplifying view" that turns a function taking a function or returning a function into a function that takes just values and returns just values. Such a process is not well-defined and is unnecessary.
(Note: some people might try to tell you that IO
can be seen as a function taking the "real world" as input and outputting a new version of the world. That's really not a good way to think about it, in part because it conflates evaluation and execution. It's a hack that makes implementing Haskell simpler, but it makes using and thinking about the language a bit of a mess. This data structure model is IMO easier to deal with.)