Difference between revisions of "IO in action"

From HaskellWiki
Jump to navigation Jump to search
m
(One intermediate revision by the same user not shown)
Line 2: Line 2:
 
The <code>IO</code> type serves as a tag for operations (actions) that interact with the outside world. [...]
 
The <code>IO</code> type serves as a tag for operations (actions) that interact with the outside world. [...]
   
<tt>[https://www.haskell.org/definition/haskell2010.pdf The Haskell 2010 Report], (page 95 of 329).</tt>
+
<tt>[https://www.haskell.org/definition/haskell2010.pdf The Haskell 2010 Report] (page 95 of 329).</tt>
 
</div>
 
</div>
   
 
So what are I/O actions?
 
So what are I/O actions?
  +
<sup> </sup>
   
 
== A false start ==
 
== A false start ==
Line 120: Line 121:
   
 
getLine u = let !c = getChar ... in
 
getLine u = let !c = getChar ... in
if c == '\n' then
+
if c == '\n' then
[]
+
[]
else
+
else
let !cs = getLine ...
+
let !cs = getLine ...
in c:cs
+
in c:cs
   
 
putLine (c:cs) u = let !_ = putChar c ... in putLine cs ...
 
putLine (c:cs) u = let !_ = putChar c ... in putLine cs ...
Line 165: Line 166:
 
</haskell>
 
</haskell>
   
Now for some more abstraction in the form of an extra primitive, to complete the new local bindings:
+
Now for [[Partible|an extra]] abstraction in the form of another primitive, to complete the new local bindings:
   
 
<haskell>
 
<haskell>

Revision as of 21:12, 1 July 2022

The IO type serves as a tag for operations (actions) that interact with the outside world. [...]

The Haskell 2010 Report (page 95 of 329).

So what are I/O actions?

A false start

Unlike most other programming languages, Haskell's non-strict semantics and thus its focus on referential transparency means the common approach to I/O won't work. Even if there was some way to actually introduce it:

# cat NoDirectIO.hs
module NoDirectIO where

foreign import ccall unsafe "c_getchar" getchar :: () -> Char
foreign import ccall unsafe "c_putchar" putchar :: Char -> ()
#
# ghci NoDirectIO.hs 
GHCi, version 9.0.1: https://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling NoDirectIO       ( NoDirectIO.hs, interpreted )

NoDirectIO.hs:3:1: error:
     Unacceptable argument type in foreign declaration:
        () cannot be marshalled in a foreign call
     When checking declaration:
        foreign import ccall unsafe "c_getchar" getchar :: () -> Char
  |
3 | foreign import ccall unsafe "c_getchar" getchar :: () -> Char
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Failed, no modules loaded.
ghci> :q
Leaving GHCi.
#

...such entities (because they are certainly not regular Haskell functions!) are practically useless. For example, what should the output of this be in Haskell?

let                                                   
  f x y = g y x
  g x y = h y y
  h x y = "what?"
in f (putstr "hello ") (putstr "world\n")

(That's just one of the counter-examples from section 3.1 (page 43 of 210) in Claus Reinke's Functions, Frames and Interactions!)

For a language like Haskell, there are two options:

  • use existing language features to build a framework and adapt I/O-centric entities to work within it: a model of I/O.

Actions and functions

In Haskell, functions have their basis in mathematics, not subroutines. It requires all functions to obey this essential rule:

  • if a function's result changes, it is only because it's arguments have changed.

So if getchar and putchar were applied to a different value at each call site, they could be used like functions:

foreign import ccall unsafe "c_getchar" getchar :: ... -> Char
foreign import ccall unsafe "c_putchar" putchar :: Char -> ... -> ()

These curious values need types:

  • the requirement for different values is now extended to avoid having two different types - each value can only be used as an argument once:
let u  = ... in
let !c1 = getchar u in  -- invalid:
let !c2 = getchar u in  -- reusing u
in  [c1, c2]
let [c1, c2] = ... in
let u = ... in
let !_  = putchar c1 u in  -- invalid
let !_  = putchar c2 u in  --   too
in  ()
let u = ... in
let !c  = getchar u   in   -- invalid
let !_  = putchar c u in   --  again
in  ()
This extended requirement will also apply to any other OI-based entities, primitive or otherwise.
data OI  -- abstract
getChar :: OI -> Char
putChar :: Char -> OI -> ()

Having previously referred to them as "entities", these new type signatures make for more useful descriptions:

         
getChar ::         (OI -> Char)  -- this is an I/O action
putChar :: Char -> (OI -> ())    -- this resembles a function returning an I/O action

An example in action

Since the origin of OI values are unspecified, let's start with some pseudo-code:

getLine ::           (OI -> [Char])  -- another I/O action
putLine :: [Char] -> (OI -> ())      -- also resembles a function returning an I/O action

getLine u = let !c = getChar ... in
            if c == '\n' then
              []
            else
              let !cs = getLine ...
              in c:cs

putLine (c:cs) u = let !_ = putChar c ... in putLine cs ...
putLine []     u = putChar '\n' ...

Because OI values can only be used once:

         

getLine u = let (u1, u2) = ... in
            let !c = getChar u1 in
            if c == '\n' then
              []
            else
              let !cs = getLine u2
              in c:cs

putLine (c:cs) u = let (u1, u2) = ... in
                   let !_ = putChar c u1 in
                   putLine cs u2
putLine []     u = putChar '\n' u

Those new local bindings u1 and u2 in getLine must be defined somehow, and there's only one parameter available:

         

getLine u = let (u1, u2) = ... u ... in
            let !c = getChar u1 in
            if c == '\n' then
              []
            else
              let !cs = getLine u2
              in c:cs

         

Now for an extra abstraction in the form of another primitive, to complete the new local bindings:

         
partOI  :: (OI -> (OI, OI))  -- also an I/O action

getLine u = let (u1, u2) = partOI u in
            let !c = getChar u1 in
            if c == '\n' then
              []
            else
              let !cs = getLine u2
              in c:cs

putLine (c:cs) u = let (u1, u2) = partOI u in
                   let !_ = putChar c u1 in
                   putLine cs u2
putLine []     u = putChar '\n' u

Noticing the tree-like way in which the various local OI values are being defined and used:

  • suggests the existence of a single ancestral OI value in the entire program:
main :: (OI -> ())  -- a program is an I/O action
  • and clearly shows that the only safe way to use an I/O action is from within the definition of another I/O action:
trace :: [Char] -> a -> a
trace msg x = let u = ... in  -- how's this going to work?
              let !_ = putLine u in x

Other interfaces

The simplicity of the OI-based interface:

data OI
partOI  ::         (OI -> (OI, OI))
getChar ::         (OI -> Char)
putChar :: Char -> (OI -> ())

makes it very adept at implementing other models of I/O:

type C a         =  (a, OI)

extract          :: C a -> a
extract (x, u)   =  let !_ = partOI u in x

duplicate        :: C a -> C (C a)
duplicate (x, u) =  let !(u1, u2) = partOI u in
                    ((x, u1), u2)

extend           :: (C a -> b) -> C a -> C b
extend h (x, u)  =  let !(u1, u2) = partOI u in
                    let !y        = h (x, u1) in
                    (y, u2)
type A b c   =  (OI -> b) -> (OI -> c)

arr          :: (b -> c) -> A b c
arr f        =  \ c' u -> f $! c' u

both         :: A b c -> A b' c' -> A (b, b') (c, c')
f' `both` g' =  \ c' u -> let !(u1:u2:u3:_) = partsOI u in
                          let !(x, x')      = c' u1 in
                          let !y            = f' (unit x) u2 in
                          let !y'           = g' (unit x') u3 in
                          (y, y')                           


unit         :: a -> OI -> a
unit x u     = let !_ = partOI u in x

partsOI      :: OI -> [OI]
partsOI u    = let !(u1, u2) = partOI u in u1 : partsOI u2
type M a   =  OI -> a

unit       :: a -> M a
unit x     =  \ u -> let !_ = partOI u in x 

bind       :: M a -> (a -> M b) -> M b
bind m k   =  \ u -> let !(u1, u2) = partOI u in
                     let !x = m u1 in
                     let !y = k x u2 in
                     y
  • ...and even the state-passing style used by Clean, Single-Assignment C and some Haskell implementations, remembering that OI values can only be used once:
newtype W = W OI

readchar :: W -> (Char, W)
readchar (W u) = let !(u1, u2) = partOI u in
                 let !c = getChar u1 in
                 (c, W u2)

writechar :: Char -> W -> W
writechar c (W u) = let !(u1, u2) = partOI u in
                    let !_ = putChar u1 in
                    W u2

It can also be used to implement the models of I/O used in earlier versions of Haskell:

  • dialogues:
runD :: ([Response] -> [Request]) -> OI -> ()
runD d u = foldr (\(!_) -> id) () $ yet $ \ l -> zipWith respond (d l) (partsOI u)

yet :: (a -> a) -> a
yet f = f (yet f)

partsOI :: OI -> [OI]
partsOI u = let !(u1, u2) = partOI u in u1 : partsOI u2

respond :: Request -> OI -> Response
respond Getq     u = let !c = getChar u in Getp c
respond (Putq c) u = let !_ = putChar c u in Putp

data Request  = Getq | Putq Char
data Response = Getp Char | Putp
  • continuations:
type Answer = OI -> ()

runK :: Answer -> IO -> ()
runK a u = a u

doneK :: Answer
doneK = \ u -> let !_ = partOI u in ()

getcharK :: (Char -> Answer) -> Answer
getcharK k   = \ u -> let !(u1, u2) = partOI u in
                      let !c        = getChar u1 in
                      let !a        = k c in
                      a u2

putcharK :: Char -> Answer -> Answer
putcharK c a = \ u -> let !(u1, u2) = partOI u in
                      let !_        = putChar c u1 in
                      a u2

Further reading

  • For those who prefer it, John Launchbury and Simon Peyton Jones's State in Haskell explains the state-passing approach currently in widespread use.