Difference between revisions of "IO, partible-style"

From HaskellWiki
Jump to: navigation, search
m (Minor clean-up)
(Page superseded by "IO in action")
(Tag: New redirect)
 
(5 intermediate revisions by the same user not shown)
Line 1: Line 1:
<div style="border-left:1px solid lightgray; padding: 1em" alt="blockquote">
+
#redirect [[IO in action]]
It is interesting that novices in lazy functional programming in general expect that there is some direct (side-effecting) I/O using a function call.
 
  
<tt>[https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.55.1076&rep=rep1&type=pdf A Partial Rehabilitation of Side-Effecting I/O:], Manfred Schmidt-Schauß.</tt>
+
[[Category:Pages to be removed]]
</div>
 
 
 
...like how I/O works in [https://www.smlnj.org/sml.html Standard ML]?
 
 
 
<pre>
 
val echoML    : unit -> unit
 
fun echoML () = let val c = getcML () in
 
                if c = #"\n" then
 
                  ()
 
                else
 
                  let val _ = putcML c in
 
                  echoML ()
 
                  end
 
                end
 
</pre>
 
 
 
Alright, now look at this:
 
 
 
<haskell>
 
echo        :: OI -> ()
 
echo u      =  let !(u1:u2:u3:_) = parts u
 
                    !c            = primGetChar u1 in
 
                if c == '\n' then
 
                  ()
 
                else
 
                  let !_ = primPutChar c u2 in
 
                  echo u3
 
</haskell>
 
 
 
So how is this possible?
 
<br>
 
<br>
 
__TOC__
 
<sub> </sub>
 
----
 
=== <u>Wadler's </u><code>echo</code> ===
 
 
 
Those two versions of that small program are based on the running example from Philip Wadler's [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.91.3579&rep=rep1&type=pdf How to Declare an Imperative]. If we compare the two:
 
 
 
{|
 
|<pre>
 
val echoML    : unit -> unit
 
fun echoML () =
 
                let val c = getcML () in
 
                if c = #"\n" then
 
                  ()
 
                else
 
                  let val _ = putcML c in
 
                  echoML ()
 
                  end
 
                end
 
</pre>
 
|<haskell>
 
echo  :: OI -> ()
 
echo u =  let !(u1:u2:u3:_) = parts u
 
              !c            = primGetChar u1 in
 
          if c == '\n' then
 
            ()
 
          else
 
            let !_ = primPutChar c u2 in
 
            echo u3
 
 
 
--
 
</haskell>
 
|}
 
 
 
...we can see just how similar the two versions of <code>echo</code> really are: apart from the obvious changes of syntax and names:
 
* the Haskell version replaces the <code>unit</code> arguments for <code>echoML</code> and <code>getcML</code>,
 
* and provides an extra argument for <code>putcML</code>,
 
* with the replacement parameter <code>u</code> being used to define the new local bindings <code>u1</code>, <code>u2</code> and <code>u3</code> as the result of a call to <code>parts</code>.
 
 
 
So for the price of some extra calls and bindings, we can have SML-style I/O in Haskell. Furthermore, as the prevailing [https://smlfamily.github.io/sml97-defn.pdf definition for Standard ML] has been available since 1997, there should be plenty of I/O tutorials to choose from...
 
 
 
----
 
=== <u>Resisting temptation</u> ===
 
 
 
If you're now thinking about using something like:
 
 
 
<pre>
 
primitive might_get_Char :: () -> Char
 
primitive might_put_Char :: Char -> ()
 
</pre>
 
 
 
to achieve a more direct translation...'''don't''' - it ''might'' for this small program, but it just isn't reliable in general. Why?
 
 
 
* ''Short answer'': unlike SML, Haskell's nonstrict evaluation means expressions should be [[Referential transparency|referentially transparent]].
 
* ''Long answer'': read section 2.2 (pages 4-5) of Wadler's [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.91.3579&rep=rep1&type=pdf paper].
 
* ''Longer answer'': read Lennart Augustsson's [https://augustss.blogspot.com/2011/05/more-points-for-lazy-evaluation-in.html More points for lazy evaluation].
 
* ''Extended answer'': read John Hughes's [https://www.cse.chalmers.se/~rjmh/Papers/whyfp.pdf Why Functional Programming Matters].
 
 
 
But, if after all that, you're still not convinced...maybe [https://www.smlnj.org/sml.html Standard ML] really is the programming language for you :-)
 
 
 
----
 
===<code>OI</code><u>: what is it?</u> ===
 
 
 
<code>OI</code> is an abstract [[Plainly partible|partible]] type:
 
 
 
<haskell>
 
data OI a
 
primitive primPartOI :: OI -> (OI, OI)
 
 
 
instance Partible OI where
 
    part = primPartOI
 
</haskell>
 
 
 
Like <code>primPartOI</code>, most other primitives for the <code>OI</code> type also accept an <code>OI</code>-value as their last (or only) argument e.g:
 
 
 
<haskell>
 
primitive primGetChar :: OI -> Char
 
primitive primPutChar :: Char -> OI -> ()
 
        ⋮
 
</haskell>
 
 
 
For consistency, the last argument of a <code>OI</code>-based definition should also be an <code>OI</code>-value:
 
 
 
<haskell>
 
interact      :: (String -> String) -> OI -> ()
 
interact d u  =  let !(u1, u2) = part u in
 
                putStr (d $ getContents u1) u2
 
 
 
putStr        :: String -> OI -> ()
 
putStr s u    =  foldr (\(!_) -> id) () $ zipWith primPutChar s $ parts u
 
 
 
getContents  :: OI -> String
 
getContents u =  case map getChar (parts u) of
 
                  l@(!c:_) -> l
 
                  l        -> l
 
</haskell>
 
<sub> </sub>
 
----
 
=== <code>IO</code>, <u>using</u> <code>OI</code> ===
 
 
 
So how do we get from <code>IO</code> to <code>OI</code>?
 
* Haskell is now used far and wide, so good ol' ''"search and replace"'' is a non-starter!
 
* There are some who [https://www.humprog.org/~stephen//research/papers/kell17some-preprint.pdf still prefer C], and there are others who are content with <code>IO</code> - convincing them to switch will probably take a lot more than a solitary page on some wiki!
 
 
 
Fortunately, it's quite easy to define <code>IO</code> with <code>OI</code>:
 
 
 
<haskell>
 
type IO a = OI -> a
 
</haskell>
 
 
 
...provided you followed that hint about putting the <code>OI</code> argument last:
 
 
 
<haskell>
 
interact      :: (String -> String) -> IO ()
 
putStr        :: String -> IO ()
 
getContents  :: IO String
 
 
 
primitive primGetChar :: IO Char
 
primitive primPutChar :: Char -> IO ()
 
        ⋮
 
</haskell>
 
 
 
Of course, a realistic implementation of <code>IO</code> in Haskell requires [[Monad|''that'']] interface:
 
 
 
<haskell>
 
unitIO    :: a -> IO a
 
unitIO x  =  \ u -> let !_ = part u in x
 
 
 
bindIO    :: IO a -> (a -> IO b) -> IO b
 
bindIO m k =  \ u -> let !(u1, u2) = part u in
 
                    let !x = m u1 in
 
                    let !y = k x u2 in
 
                    y
 
</haskell>
 
 
 
You didn't put the <code>OI</code> argument last? Oh well, there's always the [[Applicative functor|applicative]] interface...
 
 
 
----
 
=== <u>Some annoyances</u> ===
 
 
 
* ''Extra parameters and arguments'' - As noted by Sigbjørn Finne and Simon Peyton Jones in [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.46.1260&rep=rep1&type=pdf Programming Reactive Systems in Haskell], passing around all those <code>OI</code>-values ''correctly'' can be tedious for large definitions.
 
 
 
* ''Polymorphic references'' - It's been known for a very long time in the SML community that naive declarations for operations using ''mutable references'' breaks type safety:
 
 
 
:{|
 
|<pre>
 
primitive newPolyRef :: a -> OI -> PolyRef a
 
primitive readPolyRef :: PolyRef a -> OI -> a
 
primitive writePolyRef :: PolyRef a -> a -> OI -> ()
 
 
 
kah_BOOM u = let …
 
                !vehicle = newPolyRef undefined u1
 
                !_      = writePolyRef ("0" :: [Char]) u2
 
                !crash  = readPolyRef vehicle u3
 
                burn    = 1 :: Int
 
            in
 
                crash + burn
 
</pre>
 
|}
 
 
 
:SML's solution is to make all mutable references ''monomorphic'' through the use of dedicated syntax:
 
 
 
:{|
 
|<pre>
 
let val r = ref (…)
 
        ⋮
 
</pre>
 
|}
 
 
 
:One alternative for Haskell would be to extend type signatures to support [[Monomorphism by annotation of type variables|monomorphic type-variables]]:
 
 
 
:<haskell>
 
primitive newIORef  :: monomo a . a -> OI -> IORef a
 
primitive readIORef  :: monomo a . IORef a -> OI -> a
 
primitive writeIORef :: monomo a . IORef a -> a -> OI -> ()
 
 
 
{- would be rejected by the extended type system:
 
kah_BOOM u = let !(u1:u2:u3:_) = parts u
 
                !vehicle      = newIORef undefined u1          -- vehicle :: monomo a . IORef a
 
                !_            = writeIORef ("0" :: [Char]) u2  -- vehicle :: IORef [Char]
 
                !crash        = readIORef vehicle u3          -- vehicle :: IORef [Char] ≠ IORef Int
 
                burn          = 1 :: Int
 
            in
 
                crash + burn
 
-}
 
</haskell>
 
 
 
:In standard Haskell, one of the few places this already occurs (albeit implicitly) is the parameters of a function:
 
 
 
:<haskell>
 
{- will be rejected by the standard Haskell type system
 
 
 
ker_plunk f = (f True, f 'b')
 
 
 
-}
 
</haskell>
 
----
 
=== <u>One solution</u> ===
 
 
 
* ''Extra parameters and arguments'' - What is needed is a succinct interface to "hide the plumbing" used to pass around <code>OI</code>-values. Here's one we prepared earlier:
 
 
 
:<haskell>
 
unitIO    :: a -> IO a
 
unitIO x  =  \ u -> let !_ = part u in x
 
 
 
bindIO    :: IO a -> (a -> IO b) -> IO b
 
bindIO m k =  \ u -> let !(u1, u2) = part u in
 
                    let !x = m u1 in
 
                    let !y = k x u2 in
 
                    y
 
</haskell>
 
 
 
* ''Polymorphic references'' - we now make <code>IO</code> into an ''abstract data type'':
 
 
 
:<haskell>
 
module Abstract.IO
 
(
 
    Monad (..),
 
    getChar, putChar, …
 
    newIORef, readIORef, writeIORef,
 
                ⋮
 
)
 
where
 
 
 
instance Monad ((->) OI) where
 
    return = unitIO
 
    (>>=)  = bindIO
 
 
 
 
 
getChar    :: IO Char
 
getChar    =  primGetChar
 
 
 
putChar    :: Char -> IO ()
 
putChar    =  primPutChar
 
 
 
newIORef  :: a -> IO (IORef a)
 
newIORef  =  primNewIORef
 
 
 
readIORef  :: IORef a -> IO a
 
readIORef  =  primReadIORef
 
 
 
writeIORef :: IORef a -> a -> IO ()
 
writeIORef =  primWriteIORef
 
 
 
 
 
-- these are now local, private entities --
 
type IO a = OI -> a
 
 
 
unitIO    :: a -> IO a
 
unitIO x  =  \ u -> let !_ = part u in x
 
 
 
bindIO    :: IO a -> (a -> IO b) -> IO b
 
bindIO m k =  \ u -> let !(u1, u2) = part u in
 
                    let !x = m u1 in
 
                    let !y = k x u2 in
 
                    y
 
 
 
data OI a
 
primitive primPartOI  :: OI -> (OI, OI)
 
 
 
primitive primGetChar :: OI -> Char
 
primitive primPutChar :: Char -> OI -> ()
 
        ⋮
 
 
 
data IORef
 
primitive primNewIORef    :: a -> OI -> IORef a
 
primitive primReadIORef  :: IORef a -> OI -> a
 
primitive primWriteIORef  :: IORef a -> a -> OI -> ()
 
        ⋮
 
 
 
</haskell>
 
 
 
:With <code>IO</code> now abstract, the only way to use <code>IO</code>-actions is by using:
 
 
 
:* the visible <code>IO</code> operations: <code>getChar</code>, <code>putChar</code>, etc.
 
:* the monadic interface - <code>Monad(return, (>>=), …)</code> (or via Haskell's <code>do</code>-notation).
 
 
 
:So how does making <code>IO</code> an ADT prevent polymophic references? It's all to do with the type of <code>(>>=)</code> when used with <code>IO</code>-actions:
 
 
 
:<haskell>
 
(>>=) :: IO a -> (a -> IO b) -> IO b
 
</haskell>
 
 
 
:in particular, the type of the second argument:
 
 
 
::{|
 
|<haskell>
 
(a -> IO b)
 
</haskell>
 
|}
 
 
 
:...it's a function, so the value it receives will be rendered monomorphic in it's result (of type <code>IO b</code>).
 
 
 
:As <code>(>>=)</code> is now the only <code>IO</code> operation which can retrieve a result from an <code>IO</code>-action, mutable references (<code>IORef …</code>) simply cannot be used polymorphically.
 
----
 
=== <u>GHC's solution</u> ===
 
<sub> </sub>
 
<haskell>
 
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
 
</haskell>
 
 
 
...you may have noticed that we've already made liberal use of one Haskell extension - [https://downloads.haskell.org/~ghc/7.8.4/docs/html/users_guide/bang-patterns.html bang-patterns] - and it would be useful to stay as close as possible to standard Haskell, so we'll simplify matters:
 
 
 
<haskell>
 
newtype IO a = IO (IOState -> (IOState, a))  -- unboxed-tuple replaced by standard one
 
 
 
type IOState = State# RealWorld
 
</haskell>
 
 
 
Now to make the changes:
 
 
 
* ''to the type'' - <code>IOState</code> uses an <code>OI</code>-value:
 
 
 
:<haskell>
 
newtype IOState = IOS OI
 
</haskell>
 
 
 
* ''to the I/O-specific operations'' - each one will use the <code>OI</code>-value in the initial state to provide two new <code>OI</code>-values: one to make up the final state; the other being used by the <code>OI</code>-primitive:
 
 
 
:<haskell>
 
getChar  :: IO Char
 
getChar  =  IO $ \(IOS u) -> let !(u1, u2) = part u
 
                                  !c        = primGetChar u1
 
                              in  (IOS u2, c)
 
 
 
putChar  :: Char -> IO ()
 
putChar c =  IO $ \(IOS u) -> let !(u1, u2) = part u
 
                                  !t        = primPutChar c u1
 
                              in  (IOS u2, t)
 
 
 
-- etc.
 
</haskell>
 
 
 
* ''to the overloaded operations'' - you've probably seen it all before:
 
 
 
:<haskell>
 
instance Monad IO where
 
    return x  = IO $ \(!s) -> (s, x)
 
    IO m >>= k = IO $ \(!s) -> let !(s', x) = m s
 
                                  !(IO w)  = k x
 
                              in  w s'
 
</haskell>
 
 
 
:(...if you haven't: it's ye ol' <del>''pass-the-planet''</del> state-passing technique.)
 
 
 
One aspect which doesn't change is <code>IO</code> and its operations being abstract. In fact, the need is even more pressing: in addition to preventing the misuse of certain <code>OI</code>-operations, being an abstract data type prevents <code>IOState</code>-values from being erroneously reused.
 
----
 
=== <u>Conclusions</u> ===
 
 
 
* ''Why is Haskell I/O monadic'' - to avoid having to use extra arguments and parameters ''everywhere''.
 
 
 
* ''Why is Haskell I/O abstract'' - to ensure I/O works as intended, by preventing the misuse of internal data.
 
 
 
* ''Why is Haskell I/O unusual'' - because of Haskell's ''nonstrict evaluation'' and thus its focus on ''referential transparency'', contrary to most other programming languages.
 
 
 
----
 
=== <u>Further reading</u> ===
 
 
 
If you've managed to get all the way to here, [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.52.3656&rep=rep1&type=pdf State in Haskell] by John Launchbury and Simon Peyton Jones is also worth reading, if you're interested in how GHC eventually arrived at its current definition of <code>IO</code>.
 
 
 
[[Category:Tutorials]]
 

Latest revision as of 21:27, 14 June 2022

Redirect to: