Mathematically, there's no problem at all with functions that take or return other functions. The standard set-theory definition of a function from set S to set T is just:
f ∈ S → T means that f ? S ? T and two conditions hold:
- If s ∈ S, then (s, t) ∈ f for some t, and
- if both (s, t) ∈ f and (s, t') ∈ f, then t = t'.
We write f(s) = t as a convenient notational shorthand for (s, t) ∈ f.
So writing S → T just denotes a specific set, and therefore (A → B) → C and A → (B → C) are again just specific sets.
Of course, for efficiency, we do not represent functions internally in memory as the set of input-output pairs like this, but this is a decent first approximation that you can use if you want a mathematical intuition. (The second approximation takes a lot more work to set up properly, because it uses structures you probably haven't already experienced very much to deal with laziness and recursion in a careful, principled way.)
IO actions are a bit trickier. How you want to think of them may depend a bit on your particular mathematical bent.
One persuasion of mathematician might like to define IO actions as an inductive set, something like:
- If
x :: a
, then pure x :: IO a
.
- If
f :: a -> b
, then fmap f :: IO a -> IO b
.
- If
x :: IO a
and f :: a -> IO b
, then x >>= f :: IO b
.
putStrLn :: String -> IO ()
forkIO :: IO a -> IO ThreadId
- ...and a thousand other base cases.
- We quotient over a few equalities:
fmap id = id
fmap f . fmap g = fmap (f . g)
pure x >>= f
= f x
x >>= pure . f
= fmap f x
- (and a slightly complicated-to-read one that just says that
>>=
is associative)
In terms of defining the meaning of a program, that's enough to specify what "values" the IO family of types can hold. You might recognize this style of definition from the standard way of defining natural numbers:
- Zero is a natural number.
- If n is a natural number, then Succ(n) is a natural number.
Of course, there are some things about this way of defining things that aren't super satisfying. Like: what does any particular IO action mean? Nothing in this definition says anything about that. (Though see "Tackling the Awkward Squad" for an elucidation of how you could say what an IO action means even if you take this kind of inductive definition of the type.)
Another kind of mathematician might like this kind of definition better:
An IO action is isomorphic to a stateful function on a phantom token representing the current state of the universe:
IO a ~= RealWorld -> (RealWorld, a)
There are attractions to this kind of definition, too; though, notably, it gets a lot harder to say what the heck forkIO
does with that kind of definition.
...or you could take the GHC definition, in which case an IO a
is secretly an a
if you dig under the covers enough. But, shhh!!, don't tell the inexperienced programmers who just want to escape IO
and write an IO a -> a
function because they don't understand how to program using the IO
interface yet!