Difference between revisions of "IO in action"
(Selected content relocated to "Output/Input") |
(Content rewritten) |
||
Line 1: | Line 1: | ||
<div style="border-left:1px solid lightgray; padding: 1em" alt="blockquote"> |
<div style="border-left:1px solid lightgray; padding: 1em" alt="blockquote"> |
||
− | 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. The <code>IO</code> type is abstract: no constructors are visible to the user. [...] |
<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> |
||
+ | Instead of the conventional approach: |
||
− | So what are I/O actions? |
||
− | <sup> </sup> |
||
− | |||
− | == A false start == |
||
− | |||
− | Unlike most other programming languages, Haskell's [[Non-strict semantics|non-strict semantics]] and thus its focus on [[referential transparency]] means the common approach to I/O <b>won't work</b>. Even if there was some way to actually introduce it: |
||
<haskell> |
<haskell> |
||
+ | data IO -- abstract |
||
− | # cat NoDirectIO.hs |
||
− | module NoDirectIO where |
||
+ | getChar :: IO Char |
||
− | foreign import ccall unsafe "c_getchar" getchar :: () -> Char |
||
− | + | putChar :: Char -> IO () |
|
+ | ⋮ |
||
− | # |
||
− | # 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. |
||
− | # |
||
</haskell> |
</haskell> |
||
+ | describe <code>IO</code> using other types, ones with no visible constructors: |
||
− | ...such entities (because they are certainly <i>not</i> regular Haskell functions!) are practically useless. For example, what should the output of this be in Haskell? |
||
<haskell> |
<haskell> |
||
+ | data (->) a b -- abstract |
||
− | let |
||
− | + | data OI -- also abstract |
|
+ | |||
− | g x y = h y y |
||
− | + | type IO a = OI -> a |
|
+ | getChar :: OI -> Char -- an I/O action |
||
− | in f (putstr "hello ") (putstr "world\n") |
||
+ | putChar :: Char -> (OI -> ()) -- a function with one parameter, whose result is an I/O action |
||
+ | ⋮ |
||
</haskell> |
</haskell> |
||
+ | == Starting up == |
||
− | <small>(That's just one of the counter-examples from section 3.1 (page 43 of 210) in Claus Reinke's [https://macau.uni-kiel.de/servlets/MCRFileNodeServlet/macau_derivate_00002884/1998_tr04.pdf Functions, Frames and Interactions]!)</small> |
||
+ | <code>Main.main</code> is also an I/O action: |
||
− | For a language like Haskell, there are two options: |
||
+ | <haskell> |
||
− | * avoid I/O altogether and be [[Denotative|denotative]]; |
||
+ | main :: OI -> () |
||
+ | </haskell> |
||
+ | Therefore some internal subroutine in Haskell implementation provides each running program with an initial <code>OI</code> value. However, most programs will require more than just one: |
||
− | * use existing language features to build a framework and adapt I/O-centric entities to work within it: a model of I/O. |
||
+ | <haskell> |
||
− | == Actions and functions == |
||
+ | getLine :: OI -> [Char] |
||
+ | 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 :: [Char] -> OI -> () |
||
− | In Haskell, functions have their basis in mathematics, not subroutines. It requires all functions to obey this essential rule: |
||
+ | putLine (c:cs) u = let (u1, u2) = partOI u in |
||
+ | let !_ = putChar c u1 in |
||
+ | putLine cs u2 |
||
+ | putLine [] u = putChar '\n' u |
||
+ | </haskell> |
||
+ | So another I/O action is needed in order to access that same internal subroutine from Haskell: |
||
− | * if a function's result changes, it is <b>only</b> because it's arguments have changed. |
||
− | |||
− | So if <code>getchar</code> and <code>putchar</code> were applied to a different value at each call site, they <i>could</i> be used like functions: |
||
<haskell> |
<haskell> |
||
+ | partOI :: OI -> (OI, OI) |
||
− | foreign import ccall unsafe "c_getchar" getchar :: ... -> Char |
||
− | foreign import ccall unsafe "c_putchar" putchar :: Char -> ... -> () |
||
</haskell> |
</haskell> |
||
+ | If more than two new <code>OI</code> values are needed: |
||
− | These curious values need types: |
||
+ | <haskell> |
||
− | * the requirement for different values is now extended to avoid having two different types - each value can only be used as an argument <i>once</i>: |
||
+ | partsOI :: OI -> [OI] |
||
− | |||
+ | partsOI u = let !(u1, u2) = partOI u in u1 : partsOI u2 |
||
− | :<haskell> |
||
− | let u = ... in |
||
− | let !c1 = getchar u in -- invalid: |
||
− | let !c2 = getchar u in -- reusing u |
||
− | in [c1, c2] |
||
</haskell> |
</haskell> |
||
+ | But why are these abstract <code>OI</code> values needed at all - what purpose do they serve? |
||
− | :<haskell> |
||
− | let [c1, c2] = ... in |
||
− | let u = ... in |
||
− | let !_ = putchar c1 u in -- invalid |
||
− | let !_ = putchar c2 u in -- too |
||
− | in () |
||
− | </haskell> |
||
− | |||
− | :<haskell> |
||
− | let u = ... in |
||
− | let !c = getchar u in -- invalid |
||
− | let !_ = putchar c u in -- again |
||
− | in () |
||
− | </haskell> |
||
+ | == Actions and functions == |
||
− | :This extended requirement will also apply to any other <code>OI</code>-based entities, primitive or otherwise. |
||
+ | Looking more closely at <code>getLine</code>, <code>getLine</code> and <code>partsOI</code> reveals an interesting fact: |
||
− | * since outside interactions are involved, let's [https://www.interaction-design.org/literature/article/kiss-keep-it-simple-stupid-a-design-principle keep it simple]: |
||
+ | * each <code>OI</code> value is only used once (if at all). |
||
− | :<haskell> |
||
− | data OI -- abstract |
||
− | getChar :: OI -> Char |
||
− | putChar :: Char -> OI -> () |
||
− | </haskell> |
||
+ | Why is this important? Because in Haskell, functions have their basis in mathematics. That imposes certain requirements on function, including this one: |
||
− | Having previously referred to them as <i>"entities"</i>, these new type signatures make for more useful descriptions: |
||
+ | * if a function's result changes, it is <b>only</b> because one or more of it's arguments has changed. |
||
− | <haskell> |
||
− | ⋮ |
||
− | getChar :: (OI -> Char) -- this is an I/O action |
||
− | putChar :: Char -> (OI -> ()) -- this resembles a function returning an I/O action |
||
− | </haskell> |
||
+ | If they're always used with different <code>OI</code> values then I/O actions can be used like functions, even if they're defined using subroutines: |
||
− | == An example in action == |
||
− | |||
− | Since the origin of <code>OI</code> values are unspecified, let's start with some pseudo-code: |
||
<haskell> |
<haskell> |
||
− | + | foreign import "oi_part" partOI :: OI -> (OI, OI) |
|
+ | foreign import "oi_getchar" getChar :: OI -> Char |
||
− | putLine :: [Char] -> (OI -> ()) -- also resembles a function returning an I/O action |
||
+ | foreign import "oi_putchar" putChar :: Char -> OI -> () |
||
− | |||
− | 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' ... |
||
</haskell> |
</haskell> |
||
− | + | The need for an <code>OI</code> value also helps to prevent I/O actions from being used as subroutines: |
|
<haskell> |
<haskell> |
||
+ | trace :: [Char] -> a -> a |
||
− | ⋮ |
||
+ | trace msg x = case putLine msg of !_ -> x -- how is this supposed to work? |
||
+ | </haskell> |
||
+ | == Monadic actions == |
||
− | getLine u = let (u1, u2) = ... in |
||
− | let !c = getChar u1 in |
||
− | if c == '\n' then |
||
− | [] |
||
− | else |
||
− | let !cs = getLine u2 |
||
− | in c:cs |
||
+ | The monadic interface: |
||
− | putLine (c:cs) u = let (u1, u2) = ... in |
||
− | let !_ = putChar c u1 in |
||
− | putLine cs u2 |
||
− | putLine [] u = putChar '\n' u |
||
− | </haskell> |
||
− | |||
− | Those new local bindings <code>u1</code> and <code>u2</code> in <code>getLine</code> must be defined somehow, and there's only one parameter available: |
||
<haskell> |
<haskell> |
||
+ | instance Monad ((->) OI) |
||
− | ⋮ |
||
+ | return x = \ u -> let !_ = partOI u in x |
||
− | |||
− | + | m >>= k = \ u -> let !(u1, u2) = partOI u in |
|
− | let ! |
+ | let !x = m u1 in |
− | + | let !y = k x u2 in |
|
− | + | y |
|
− | else |
||
− | let !cs = getLine u2 |
||
− | in c:cs |
||
− | |||
− | ⋮ |
||
</haskell> |
</haskell> |
||
+ | allows <code>getLine</code> and <code>getLine</code> to be defined more compactly: |
||
− | Now for [[Partible|an extra]] abstraction in the form of another primitive, to complete the new local bindings: |
||
<haskell> |
<haskell> |
||
+ | getLine :: OI -> [Char] |
||
− | ⋮ |
||
+ | getLine = do c <- getChar |
||
− | partOI :: (OI -> (OI, OI)) -- also an I/O action |
||
+ | if c == '\n' then |
||
+ | return [] |
||
+ | else |
||
+ | do cs <- getLine |
||
+ | return (c:cs) |
||
+ | putLine :: [Char] -> OI -> () |
||
− | getLine u = let (u1, u2) = partOI u in |
||
+ | putLine [] = putChar '\n' |
||
− | let !c = getChar u1 in |
||
+ | putLine (c:cs) = putChar c >> putLine cs |
||
− | 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 |
||
</haskell> |
</haskell> |
||
− | + | and conceals the use of all those <code>OI</code> values. But not all definitions will benefit from being monadic: |
|
+ | * <haskell> |
||
− | * suggests the existence of a single ancestral <code>OI</code> value in the entire program: |
||
+ | partsOI :: OI -> [OI] |
||
− | |||
+ | partsOI = do (u1, u2) <- partOI; return (u1 : partsOI u2) |
||
− | :<haskell> |
||
− | main :: (OI -> ()) -- a program is an I/O action |
||
</haskell> |
</haskell> |
||
+ | * <haskell> |
||
− | * and clearly shows that the only <code>safe</code> way to use an I/O action is from within the definition of another I/O action: |
||
+ | partsOI :: OI -> [OI] |
||
− | |||
+ | partsOI u = let !(u1, u2) = partOI u in u1 : partsOI u2 |
||
− | :<haskell> |
||
− | trace :: [Char] -> a -> a |
||
− | trace msg x = let u = ... in -- how's this going to work? |
||
− | let !_ = putLine msg u in x |
||
</haskell> |
</haskell> |
||
Line 206: | Line 139: | ||
* [[Output/Input]] goes into more detail about the type <code>OI -> a</code>. |
* [[Output/Input]] goes into more detail about the type <code>OI -> a</code>. |
||
− | * For those who prefer it, John Launchbury and Simon Peyton Jones's [https:// |
+ | * For those who prefer it, John Launchbury and Simon Peyton Jones's [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.52.3656&rep=rep1&type=pdf State in Haskell] explains the state-passing approach currently in widespread use. |
[[Category:Tutorials]] |
[[Category:Tutorials]] |
Revision as of 20:27, 24 August 2022
The IO
type serves as a tag for operations (actions) that interact with the outside world. The IO
type is abstract: no constructors are visible to the user. [...]
The Haskell 2010 Report (page 95 of 329).
Instead of the conventional approach:
data IO -- abstract
getChar :: IO Char
putChar :: Char -> IO ()
⋮
describe IO
using other types, ones with no visible constructors:
data (->) a b -- abstract
data OI -- also abstract
type IO a = OI -> a
getChar :: OI -> Char -- an I/O action
putChar :: Char -> (OI -> ()) -- a function with one parameter, whose result is an I/O action
⋮
Starting up
Main.main
is also an I/O action:
main :: OI -> ()
Therefore some internal subroutine in Haskell implementation provides each running program with an initial OI
value. However, most programs will require more than just one:
getLine :: OI -> [Char]
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 :: [Char] -> OI -> ()
putLine (c:cs) u = let (u1, u2) = partOI u in
let !_ = putChar c u1 in
putLine cs u2
putLine [] u = putChar '\n' u
So another I/O action is needed in order to access that same internal subroutine from Haskell:
partOI :: OI -> (OI, OI)
If more than two new OI
values are needed:
partsOI :: OI -> [OI]
partsOI u = let !(u1, u2) = partOI u in u1 : partsOI u2
But why are these abstract OI
values needed at all - what purpose do they serve?
Actions and functions
Looking more closely at getLine
, getLine
and partsOI
reveals an interesting fact:
- each
OI
value is only used once (if at all).
Why is this important? Because in Haskell, functions have their basis in mathematics. That imposes certain requirements on function, including this one:
- if a function's result changes, it is only because one or more of it's arguments has changed.
If they're always used with different OI
values then I/O actions can be used like functions, even if they're defined using subroutines:
foreign import "oi_part" partOI :: OI -> (OI, OI)
foreign import "oi_getchar" getChar :: OI -> Char
foreign import "oi_putchar" putChar :: Char -> OI -> ()
The need for an OI
value also helps to prevent I/O actions from being used as subroutines:
trace :: [Char] -> a -> a
trace msg x = case putLine msg of !_ -> x -- how is this supposed to work?
Monadic actions
The monadic interface:
instance Monad ((->) OI)
return x = \ u -> let !_ = partOI u in x
m >>= k = \ u -> let !(u1, u2) = partOI u in
let !x = m u1 in
let !y = k x u2 in
y
allows getLine
and getLine
to be defined more compactly:
getLine :: OI -> [Char]
getLine = do c <- getChar
if c == '\n' then
return []
else
do cs <- getLine
return (c:cs)
putLine :: [Char] -> OI -> ()
putLine [] = putChar '\n'
putLine (c:cs) = putChar c >> putLine cs
and conceals the use of all those OI
values. But not all definitions will benefit from being monadic:
partsOI :: OI -> [OI] partsOI = do (u1, u2) <- partOI; return (u1 : partsOI u2)
partsOI :: OI -> [OI] partsOI u = let !(u1, u2) = partOI u in u1 : partsOI u2
Further reading
- Output/Input goes into more detail about the type
OI -> a
.
- For those who prefer it, John Launchbury and Simon Peyton Jones's State in Haskell explains the state-passing approach currently in widespread use.