Not Everything is a Function in Haskell
Image Attribution:
Debian Logo: Software in the Public Interest, Inc. "The Debian Open Use Logo." 1999. Under a GNU Lesser General Public License (NOT a CC-BY 3.0 License).
Screenshot: cmus Project. "Untitled." 2013. Under a GNU General Public License v3.0 (NOT a CC-BY 3.0 License).
Logo + Screenshot: Shuang, Rimu. "Untitled." Mar 19, 2014. Under a GNU General Public License v3.0 (NOT a CC-BY 3.0 License).
January 24, 2014
moving, django, hakyll
A common misconception in Haskell is that everything is a function (a nice
example of this is seydar on Reddit asking “Would it be heresy to henceforth
call ‘y’ a function with no arguments…”
here.
I’m still learning Haskell, and I found at the very beginning that this was a
nice way of thinking about things in Haskell. After all it tied together in a
very nice way. All of a sudden it made perfect sense to pass functions to other
functions because it was the exact same thing as passing non-function
arguments. The syntax of Haskell also started making a ton of sense. If f x y = z
has type a -> b -> c
and f x = z
has type a -> c
, then naturally f = z
has type c
. So then if we have something such as (+) 3 2
which has type
Int -> Int -> Int
and 3
just has type Int
, then maybe it’s just a nullary
function!
Aha, they’re all the same thing; they’re all functions! But they’re not. Conal Elliott has a nice article about why this isn’t the case and goes into the motivations that he thinks are behind such thinking.
I realized today, as I progress further along the road of learning Haskell, that
the monadic behavior behind what we usually write using do
notation in
Haskell, provides nice justification for why not everything in Haskell is a
function. In particular, if, for example 3
was a function, then in Haskell, we
would represent it as () -> Int
, to describe the fact that it took no
arguments. So what’s wrong with that? Maybe it gunks up our type system a little
to say that Int
and () -> Int
can both be used to describe the same thing
(even though they themselves might be distinct from one another), but Haskell’s
already confusing enough right?
Well let’s take a look at the following short function
counter :: Int -> IO ()
counter x = putStrLn (show x) >>= (\y -> counter (x + 1))
Equivalently we might write
counter :: Int -> IO ()
counter x = do
putStrLn (show x)
counter (x + 1)
This function takes a number and then counts up forever, printing the result of
each computation to the screen, which would yield something like the following
if started at 1
.
$ 1
$ 2
$ 3
$ 4
$ 5
...
Note that (\y -> counter (x + 1))
has type () -> IO ()
. If we were to state
that () -> a
and a
can refer to the same things, then this would mean that
in fact (\y -> counter (x + 1))
could also be said to have type IO ()
. Yet
counter (x + 1)
has type IO ()
and it is most emphatically not (\y -> counter (x + 1))
. In fact you’ll get a type error, and for good reason, because
from the perspective of monads, it makes no sense to bind a result of IO ()
to
something of type IO ()
.
Hence although it might be nice mathematically for everything in Haskell to be a function (and in fact it seems there are mathematical frameworks in which this is possible), this is not the case in real life.