Difference between revisions of "IO, partible-style"
Jump to navigation
Jump to search
m (Link to current SML definition; other minor changes) |
(Page superseded by "IO in action") Tag: New redirect |
||
(15 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
+ | #redirect [[IO in action]] |
||
− | <i><code>IO</code> is the [monadic type] you cannot avoid.</i> |
||
+ | [[Category:Pages to be removed]] |
||
− | * <tt>[https://image.slidesharecdn.com/functionalconf2019-whyishaskellsohard2-191116135003/95/why-is-haskell-so-hard-and-how-to-deal-with-it-53-638.jpg Why Haskell is so HARD? (And how to deal with it)]; Saurabh Nanda.</tt> |
||
− | |||
− | ::...but you kept looking anyway, and here you are! |
||
− | <br> |
||
− | <i>[...] the input/output story for purely-functional languages was weak and unconvincing, let alone error recovery, concurrency, etc. Over the last few years, a surprising solution has emerged: the monad. I say "surprising" because anything with as exotic a name as "monad" - derived from category theory, one of the most abstract branches of mathematics - is unlikely to be very useful to red-blooded programmers. But one of the joys of functional programming is the way in which apparently-exotic theory can have a direct and practical application, and the monadic story is a good example.</i> |
||
− | |||
− | * <tt>[https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.13.9123&rep=rep1&type=pdf Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell], Simon Peyton Jones.</tt> |
||
− | |||
− | ::...with that (''ahem'') "joy" leading to more than a few [[Monad tutorials timeline|helpful guides about the topic]] - that monadic interface: it's abstract alright! |
||
− | <br> |
||
− | <i>[...] And I still don't believe in monads. :-)</i> |
||
− | |||
− | * <tt>[https://web.archive.org/web/20160912053551/http://www.coyotos.org/pipermail/bitc-dev/2012-March/003300.html Retrospective Thoughts on BitC]; Jonathan S. Shapiro, ''bitc-dev'' mailing list.</tt> |
||
− | |||
− | ::'''You are not alone.''' |
||
− | <br> |
||
− | __TOC__ |
||
− | <sub> </sub> |
||
− | ---- |
||
− | |||
− | === <code>IO</code>, <u>using</u> <code>OI</code> === |
||
− | |||
− | Our definition of <code>IO</code> is a type synonym: |
||
− | |||
− | <haskell> |
||
− | type IO a = OI -> a |
||
− | </haskell> |
||
− | |||
− | with <code>OI</code> being 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> |
||
− | |||
− | Borrowing 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]: |
||
− | |||
− | <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> |
||
− | |||
− | Wadler also provides an [https://www.smlnj.org/sml.html SML] version: |
||
− | |||
− | <pre> |
||
− | val echoML : unit -> unit |
||
− | fun echoML () = let val c = getcML () in |
||
− | if c = #"\n" then |
||
− | () |
||
− | else |
||
− | (putcML c; echoML ()) |
||
− | end |
||
− | </pre> |
||
− | |||
− | in which we replace SML's sequencing operator <code>;</code>: |
||
− | |||
− | <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> |
||
− | |||
− | If we compare it to our Haskell version: |
||
− | |||
− | {| |
||
− | |<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> |
||
− | |<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> |
||
− | |} |
||
− | ...we can now see just how similar the two versions of <code>echo</code> really are: apart from the obvious changes of syntax, the Haskell version replaces all use of <code>unit</code>-values with <code>OI</code>-values, and adds an extra call to <code>parts</code> to provide them. |
||
− | |||
− | So there you have it: 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 SML] has been available since 1997, there should be plenty of I/O tutorials to choose from... |
||
− | |||
− | At this point, you may be tempted to try something like: |
||
− | |||
− | <pre> |
||
− | type IO a = () -> a |
||
− | |||
− | primitive might_get_Char :: () -> Char |
||
− | primitive might_put_Char :: Char -> () |
||
− | ⋮ |
||
− | </pre> |
||
− | |||
− | While this ''might'' work in some situations, it's unreliable in general. Why? |
||
− | |||
− | * ''Short answer'': unlike SML, Haskell's nonstrict evaluation means expressions should be 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, then perhaps you'll be happier programming in [https://www.smlnj.org/sml.html SML]... |
||
− | |||
− | |||
− | ...you're still here: ''nice!'' Now for a small example - here's <code>interact</code>, using those <code>OI</code>-based definitions: |
||
− | |||
− | <haskell> |
||
− | interact :: (String -> String) -> OI -> () |
||
− | interact f v = let !(u1, u2) = part v |
||
− | gets = map primGetChar $ parts u1 |
||
− | puts s = foldr (\!_ -> id) () $ zipWith primPutChar s $ parts u2 |
||
− | in |
||
− | puts $ f $ gets |
||
− | </haskell> |
||
− | <sub> </sub> |
||
− | ---- |
||
− | === <u>Some annoyances</u> === |
||
− | |||
− | * ''Extra parameters and arguments'' - As noted in Paul Hudak and Raman S. Sundaresh's [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.49.695&rep=rep1&type=pdf On the Expressiveness of Purely Functional I/O Systems], 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: |
||
− | |||
− | :<haskell> |
||
− | instance Monad ((->) OI) where |
||
− | return x = \u -> case part u of !_ -> x |
||
− | m >>= k = \u -> case part u of |
||
− | (u1, u2) -> case m u1 of |
||
− | !x -> k x u2 |
||
− | </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 x = \u -> case part u of !_ -> x |
||
− | m >>= k = \u -> case part u of |
||
− | (u1, u2) -> case m u1 of |
||
− | !x -> k x u2 |
||
− | |||
− | 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 |
||
− | |||
− | 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 the <code>IO</code> type now made abstract, the only way to use <code>IO</code>-values 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). |
||
− | |||
− | :The key here is the type of <code>(>>=)</code>, for <code>IO</code>-values: |
||
− | |||
− | :<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 the function'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>-value, 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) = parts u |
||
− | !c = primGetChar u1 |
||
− | in (IOS u2, c) |
||
− | |||
− | putChar :: Char -> IO () |
||
− | putChar c = IO $ \(IOS u) -> let !(u1, u2) = parts 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: