开发者

I/O in Haskell is Functional?

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?


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,

I/O in Haskell is Functional?


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.


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.


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


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:

I/O in Haskell is Functional?

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:

  • was Haskell no longer functional?
  • was it really necessary - could Haskell just be denotative?

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:

  • Is I/O in your implementation of Haskell referentially transparent?

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...


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

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜