16

I'm just starting to take a look at Haskell (my previous FP experience is in Scheme), and I came across this code:

do { putStrLn "ABCDE" ; putStrLn "12345" }

To me, this is procedural programming, if anything -- especially because of the consecutive nature of side effects.

Would someone please explain how this code is "functional" in any respect?

Will Ness
  • 70,110
  • 9
  • 98
  • 181
user541686
  • 205,094
  • 128
  • 528
  • 886
  • as I don't know much about haskell, this is completely chinese to me, but maybe it makes sense to you: http://www.engr.mun.ca/~theo/Misc/haskell_and_monads.htm – Karoly Horvath Jun 19 '11 at 00:19
  • 4
    Side effects in haskell are captured by special structures called Monads. Monads maintain referential transparency. The code you posted lives in the `IO Monad` and is thus theoretically pure, though it's sometimes referred to as impure. – is7s Jun 19 '11 at 00:20
  • To a first order of approximation, read "technically pure" as "non-IO code is prevented from observing any impurity". There are various ways of looking at it more intuitively, but my preference is to treat something with the type `IO a` as a pure value that *holds* an imperative procedure, with `do` blocks used to construct new procedures from existing ones, and the final value of `main` being an imperative procedure that the runtime executes. – C. A. McCann Jun 19 '11 at 00:54
  • @yi_H: I took a look, but that's all Greek to me (perhaps because I'm not familiar with the syntax yet)... will take a look later if I become more familiar. @is7s, @camccann: Hm... but for all I know, my code is performing something sequentially, isn't it? I could *think* about what's happening differently, but isn't the *code itself* mentioning something procedural (whether or not it's syntactic sugar for something else)? – user541686 Jun 19 '11 at 01:38
  • 1
    @Mehrdad, you're right, that's procedural, not functional. You could probably write a whole procedural program in Haskell (or any other functional language that supports I/O for that matter), but you shouldn't. This exists mainly as a compromise, because there are some things (such as I/O) that must be done sequentially. – rid Jun 19 '11 at 02:03
  • @Radu: But if I can write procedural programs in Haskell then that means Haskell isn't "purely functional", is it? It looks as though Haskell is just Scheme with a different syntax (and static typing)... – user541686 Jun 19 '11 at 02:12
  • 1
    @Mehrdad, if the language did not have the `do` syntax, then you would need to write what Don Stewart answered by hand. And, as you can see, that *is* functional, and, therefore, pure. `do` is simply a convenience to make such tasks easier for when you *really* need functionality that can *only* be expressed procedurally. – rid Jun 19 '11 at 02:14
  • @Mehrdad, this is different from Scheme in the fact that Scheme allows side effects as part of the language itself, and it can interpret such constructs. Haskell does not. It can only interpret funcional constructs. `do` is a kind of preprocessor directive, nothing more. – rid Jun 19 '11 at 02:16
  • @Radu: Hm... I'm still thinking about these, but thanks for the info, I'll try to keep them in mind while learning Haskell. – user541686 Jun 19 '11 at 02:19
  • @Mehrdad If you look at how monads are implemented you'll understand that it's not really sequential in the imperative sense as Don pointed out...it's rather a group of functions combined together in a legally functional manner...the gate to understanding this is understanding the basics of Monads..I recommend this tutorial [Understanding Haskell Monads](http://ertes.de/articles/monads.html) – is7s Jun 19 '11 at 02:21
  • @Mehrdad I disagree with @Radu's first comment it's not procedural at all, it's still purely functional – is7s Jun 19 '11 at 02:23
  • 3
    @Mehrdad: Haskell itself *is* pure, and functional by nature. The `do` notation is just syntactic sugar that lets you write code in imperative *style*, that gets translated to lambdas. What you put inside the `do` block is "procedural and imperative code" in exactly the same way that rolling your own vtable and inheritance system and whatnot in C lets you write "object-oriented code". – C. A. McCann Jun 19 '11 at 02:50
  • @Mehrdad: You can check my answer here: http://stackoverflow.com/questions/3117583/ – sdcvvc Jun 19 '11 at 11:13
  • 2
    @Mehrdad Even if Haskell has a procedural language for IO there is a big difference between Haskell and Scheme. Say that you write `[putStrLn "ABCDE", putStrLn "12345"]` in Haskell. This will *not* do any IO. It's a list of two IO computations, but they have to "get in contact" with main to actually execute. So IO values really do behave like any other values in Haskell, except that `main` is special. – augustss Jun 19 '11 at 14:04
  • @sdcvvc: Thanks for the link, looking at it right now. @augustss: I don't see how the fact that everything is delayed makes Haskell purely functional... if you delay it, it's still going to happen, but it's just later than when you think. How does that make any difference? – user541686 Jun 19 '11 at 15:55
  • @Mehrdad: The key is that it's not "delayed": it's not so much that it needs to get in "contact" with `main` (though it's the truth), but that the actions are actually first class values, which need to be bound in the `IO` monad (which enforces a kind of sequence, per comments above) to actually have an effect on the world. `putStrLn "xyz"` doesn't actually cause an effect when it's evaluated, it returns an action, a first-class object. That action is then strung together in the `IO` monad, starting at `main`, and the resulting "I/O program" is what's executed by your runtime. – Asherah Jun 30 '11 at 02:45
  • @Mehrdad: note that this here ("delaying", for lack of a better term) has nothing to do with Haskell being a lazy language. – Asherah Jun 30 '11 at 02:47
  • In Scheme terms, it is as functional as the Scheme code `(list 'DO '({ putStrLn "ABCDE" ; putStrLn "12345" }))` which it reallly is all it is, conceptually. A *value* in your language, which is "run" / interpreted by the runtime behind the scenes. – Will Ness Mar 04 '22 at 04:44

6 Answers6

22

While it appears to be a procedural program, the above syntax is translated into a functional program, like so:

   do { putStrLn "ABCDE" ; putStrLn "12345" }
=>
   IO (\ s -> case (putStrLn "ABCDE" s) of
                  ( new_s, _ ) -> case (putStrLn "12345" new_s) of
                                      ( new_new_s, _) -> ((), new_new_s))

That is, a series of nested functions that have a unique world parameter threaded through them, sequencing calls to primitive functions "procedurally". This design supports an encoding of imperative programming into a functional language.

The best introduction to the semantic decisions underlying this design is "The Awkward Squad" paper,

enter image description here

Don Stewart
  • 137,316
  • 36
  • 365
  • 468
  • 8
    I'm not thrilled by the state monad version of IO as an explanation, because that's not the only way to implement IO. IO really us abstract. Opening up the abstraction is dangerous, because what's inside the IO monad in ghc is not normal Haskell; the World token must be used linearly. – augustss Jun 19 '11 at 01:00
  • 1
    @augustss Agreed. While I think it gives a reasonable intuition fairly quickly, obviously there's the small matter of the denotation of those GHC primitives. For that, something like the "abstract computation builder" model might be more complete. – Don Stewart Jun 19 '11 at 01:11
  • 5
    One advantage of the state monad explanation is that, by implicit analogy to the real, more transparent `State`, it makes it clearer that I/O indeed behaves as a monad, rather than simply saying "`IO` is a monad, use it to do I/O". – C. A. McCann Jun 19 '11 at 01:16
  • 1
    This makes sense (so I accepted it), but at the same time, I can't help but ask: Aren't you *reinterpreting something procedural as something functional*? Can't you say the same thing in every other language? The *syntax* looks procedural -- whether or not you can explain it in a mathematically functional manner, I'm still learning... but *to the programmer* this is procedural, isn't it? – user541686 Jun 19 '11 at 02:20
  • Yes, @Mehrdad, to the programmer there is an imperative language layered into Haskell that you can use. That is the point, as some problems have non-functional solutions. – Don Stewart Jun 19 '11 at 02:39
  • 4
    Please be careful. This explanation of IO is a persistent and misleading myth. See [my comments on "How do functional programming languages work?"](http://stackoverflow.com/questions/2751313/how-do-functional-programming-languages-work/2754244#2754244). – Conal Jun 20 '11 at 07:47
  • 4
    Has anyone ever shown `IO` to be a monad? Or is it just unsubstantiated folklore. Since `IO` cannot be accurately explained as `State`, the monadness of `State` doesn't help. – Conal Jun 20 '11 at 07:52
  • 2
    @Conal: I would argue that IO is "just" an algebra with variable substitution (and so a free monad) describing tasks we can give a computer to perform, and as such it is a monad by construction (assuming the language generated is complete so all values, including FFI, can in principle be expanded to terms in the language). Is the question more along the lines of "Can the IO algebra be implemented on a real computer?" In that case it seems to me that if not, high level language is doomed from the start. IO is already a much simpler algebra than most ASTs, so if it can't work here... – mokus Jun 20 '11 at 13:27
  • 3
    For the current status of the "What is IO, and is it a monad" roconnor's recent post gives a good summary, http://r6.ca/blog/20110520T220201Z.html – Don Stewart Jun 20 '11 at 13:55
  • 2
    @Conal: It's not so much a myth as a convenient fiction. `State` gives a model of imperative computation as a monad which matches the *naive concept* of `IO`; common questions from newcomers about whether `IO` is "functional" or not often apply equally (if not moreso) to `State`. The issues you raise, I would argue, are better taken as higher rungs on [Wittgenstein's ladder](http://c2.com/cgi/wiki?WittgensteinsLadder). – C. A. McCann Jun 20 '11 at 16:16
  • Don: I've had a few detailed chats with Russell on this topic and have read & re-read his post, and I haven't been able to extract how it relates to the original questions of (a) what does (Haskell's current) `IO` *denote* and (b) is this `IO` a monad. – Conal Aug 15 '11 at 20:42
  • C.A.McC: Maybe you mean by "convenient fiction" is what I mean by "myth". I'd like to add that besides the convenience, this fiction also has inconveniences resulting from its fictiousness. I don't automatically dislike myths. This one in particular troubles me. – Conal Aug 15 '11 at 20:44
  • Conal's views on the subject he mentioned are incorrect, and are mathematically proven incorrect as long as the compiler doesn't duplicate redexes (which no compiler does) – Jeremy List Apr 24 '14 at 15:13
13

I don't think we can answer this question clearly, because "functional" is a fuzzy notion, and there are contradictory ideas out there of what it means. So I prefer Peter Landin's suggested replacement term "denotative", which is precise and substantive and, for me, the heart & soul of functional programming and what makes it good for equational reasoning. See these comments for some pointers to Landin's definition. IO is not denotative.

Conal
  • 18,517
  • 2
  • 37
  • 40
  • Of course, values of some type `IO a` aren't themselves special, and can be reasoned about in all the ways that apply to values of arbitrary type; `const x` has the same semantics regardless of whether `x` has type `Bool` or `IO String` or anything else. – C. A. McCann Jun 20 '11 at 15:49
  • 1
    camccann: I'm not sure what you're trying to say here. It's `const` that has the same semantics in those three cases, while the meaning of `x` (and thus `const x`) differs from one case to the next. The type of a meaning is the meaning of the type, so where types differ, meanings do also. – Conal Jun 20 '11 at 18:50
  • Just trying to emphasize that any problems with `IO` don't "escape" to poison surrounding expressions. `const x y` has the same meaning as `x` and this remains true even if `x` doesn't actually have a well-defined meaning. This sounds trivial at first but it wouldn't necessarily be true in other languages; I guess you could say that Haskell's semantics have non-strict semantics. – C. A. McCann Jun 20 '11 at 19:05
5

Think about it this way. It doesn't actually "execute" the IO instructions. The IO monad is a pure value that encapsulates the "imperative computation" to be done (but it doesn't actually carry it out). You can put monads (computations) together into a bigger "computation" in a pure way using the monad operators and constructs like "do". Still, nothing is "executed" per se. In fact, in a way the whole purpose of a Haskell program is to put together a big "computation" that is its main value (which has type IO a). And when you run the program, it is this "computation" that is run.

newacct
  • 119,665
  • 29
  • 163
  • 224
3

This is a monad. Read about the do-notation for an explanation of what goes on behind the covers.

rid
  • 61,078
  • 31
  • 152
  • 193
  • 5
    And weep with joy seeing all those lambdas. –  Jun 19 '11 at 00:21
  • 1
    I kind of understand, but I kind of don't... isn't "what goes on behind the covers" an implementation detail? For all I understand, my program is performing sequential actions; whether that's indeed syntactic sugar for something else, I'm not sure yet, but in any case, *my source code* is clearly sequential, isn't it? – user541686 Jun 19 '11 at 01:33
  • @delnan: Lol, I don't mind lambdas, they're pretty awesome. :) – user541686 Jun 19 '11 at 01:42
2

Would someone please explain how this code

do { putStrLn "ABCDE" ; putStrLn "12345" }

is "functional" in any respect?

This is how I see the current situation with I/O in Haskell; the usual disclaimers apply >_<

Right now (2020 Jun), how "functional" I/O is depends on your Haskell implementation. But that wasn't always the case - in fact, the Haskell language's original model of I/O really was functional!

Time for a trip back to the early days of Haskell, helped along by Philip Wadler's How to Declare an Imperative:

import Prelude hiding (IO)
import qualified Prelude (IO)

import Control.Concurrent.Chan(newChan, getChanContents, writeChan) 
import Control.Monad((<=<))


 -- pared-back emulation of retro-Haskell I/O
 --
runDialogue :: Dialogue -> Prelude.IO ()
runDialogue d =
  do ch <- newChan
     l <- getChanContents ch
     mapM_ (writeChan ch <=< respond) (d l)

respond :: Request -> Prelude.IO Response
respond Getq     = fmap Getp getChar
respond (Putq c) = putChar c >> return Putp

main = runDialogue (retro_main :: Dialogue)

{-
          implementation side
  -----------------------------------
  ========== retro-Haskell ==========
  -----------------------------------
             language side
-}

 -- pared-back definitions for retro-Haskell I/O
 -- from page 14 of Wadler's paper
 --
data Request = Getq | Putq Char
data Response = Getp Char | Putp

type Dialogue = [Response] -> [Request]

(Extending it to all of retro-Haskell I/O is left as an exercise for very keen readers ;-)

There you go: plain "ol' school " functional I/O! The responses are streamed to main retro_main, which then streams the requests back:

retro-Haskell program interacting with its surroundings

With all that classic elegance, you could happily define:

 -- from page 15 of Wadler's paper
echoD :: Dialogue
echoD p =
  Getq :
    case p of
      Getp c : p' ->
        if (c == '\n') then
          []
        else
          Putq c :
            case p' of
              Putp : p'' -> echoD p''

You look confused - that's alright; you'll get the hang of it :-D

Here's a more-sophisticated example from page 24 of A History of Haskell:

{-

main ~(Success : ~((Str userInput) : ~(Success : ~(r4 : _))))
  = [ AppendChan stdout "enter filename\n",
      ReadChan stdin,
      AppendChan stdout name,
      ReadFile name,
      AppendChan stdout
          (case r4 of
              Str contents -> contents
              Failure ioerr -> "can't open file")
    ] where (name : _) = lines userInput

-}

Are you still there?

Is that a garbage bin next to you? Huh? You were ill? Darn.

Alright then - perhaps you'll find it a bit easier with a more-recognisable interface:

 -- from page 12 of Wadler's paper
 --
echo  :: IO ()
echo  =  getc >>= \ c ->
         if (c == '\n') then
           done
         else
           putc c >>
           echo


 -- from pages 3 and 7
 --
puts  :: String -> IO ()
puts []    = done
puts (c:s) = putc c >> puts s

done :: IO ()
done = return ()


 -- based on pages 16-17
 --
newtype IO a = MkIO { enact :: Reality -> (Reality, a) }
type Reality = ([Response], [Request])

bindIO    :: IO a -> (a -> IO b) -> IO b
bindIO m k =  MkIO $ \ (p0, q2) -> let ((p1, q0), x) = enact m     (p0, q1)
                                       ((p2, q1), y) = enact (k x) (p1, q2)
                                   in
                                       ((p2, q0), y)


unitIO :: a -> IO a
unitIO x = MkIO $ \ w -> (w, x)

putc :: Char -> IO ()
putc c  = MkIO $ \ (p0, q1) -> let q0        = Putq c : q1
                                   Putp : p1 = p0
                               in
                                   ((p1, q0), ())

getc :: IO Char
getc    = MkIO $ \ (p0, q1) -> let q0          = Getq : q1
                                   Getp c : p1 = p0
                               in
                                   ((p1, q0), c)

mainD :: IO a -> Dialogue
mainD main = \ p0 -> let ((p1, q0), x) = enact main (p0, q1)

                         q1            = []
                     in
                         q0

 -- making it work
instance Monad IO where
    return = unitIO
    (>>=)  = bindIO

I've also included your sample code; maybe that'll help:

 -- local version of putStrLn
putsl :: String -> IO ()
putsl s = puts s >> putc '\n'

 -- bringing it all together
retro_main :: Dialogue
retro_main = mainD $ do { putsl "ABCDE" ; putsl "12345" }

Yes: this is all still simple functional I/O; check the type of retro_main.

Apparently, dialogue-based I/O ended up being about as popular as a skunk in a space station. Stuffing it inside a monadic interface only confined the stench (and its source) to one small section of the station - by then, Haskellers wanted that lil' stinker gone!

So the abstract monadic interface for I/O in Haskell was made the standard - that small section and its pungent occupant was detached from the space station and hauled back to Earth, where fresh air is more plentiful. The atmosphere on the space station improved, and most Haskellers went on to do other things.

But a few had some questions about this new, abstract model of I/O:


Regarding Haskell being functional - if the model is based on an abstraction, in this case:

  • an abstract type of I/O actions: IO
  • an abstract function for constructing simple I/O actions: return
  • the abstract functions for combining I/O actions: (>>=), catch, etc
  • the abstract functions for specific I/O actions: getArgs, getEnv, etc

then how these entities are actually defined will be specific to each implementation of Haskell. What should now be asked is this:

So the answer to your question:

Would someone please explain how this code

do { putStrLn "ABCDE" ; putStrLn "12345" }

is "functional" in any respect?

now depends on which implementation of Haskell you're using.


As for Haskell being denotative - moving effects from the language into the implementation (and under the control of algorithms) has worked in the past:

[...] Underneath the implementation of our current functional abstractions (numbers, strings, trees, functions, etc), there are imperative mechanisms, such as memory allocation & deallocation, stack frame modification, and thunk overwriting (to implement laziness). [...]

Stack and register munging and jump/GOTO are implementations of the semantically simpler notion of function application. [...]

Conal Elliott.

...so also relocating the effects of I/O in that way seems entirely reasonable.

But there's a crucial difference: unlike those other mechanisms which use the computer's memory, the simplest of I/O is device-based and the vast majority of I/O devices do not behave like the memory of a computer e.g. turning off your computer after printing an SVG file doesn't erase the image from the paper.

Haskell was intended to be a stable foundation for real applications development - presumably that includes applications which use I/O, and need it to work reliably. Whether a future version Haskell could be made completely denotative remains a subject of study...

atravers
  • 455
  • 4
  • 8
0

It isn't functional code. Why would it be?

Antoine Latter
  • 1,545
  • 10
  • 13
  • It is. Every code is functional. Not every functional code is imperative. The example is a piece of functional code written in an imperative style; the example does not show what functional programming can do better than imperative. – comonad Jun 30 '11 at 21:26